//! Servicio de administración de roles: listado, CRUD y permisos. use std::collections::HashMap; use pagetop::prelude::*; use pagetop_seaorm::db::{ ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, Order, Paginated, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set, TransactionTrait, dbconn, flatten_txn_err, paginate, }; use crate::entity::{role, role_permission, user_role}; use crate::error::AuthError; use crate::permission; // **< listado >************************************************************************************** #[derive(Clone, Copy, Debug, Default, PartialEq)] pub(crate) enum RoleSortField { #[default] Weight, MachineName, Label, } impl RoleSortField { pub(crate) fn from_query(s: Option<&str>) -> Self { match s { Some("machine_name") => RoleSortField::MachineName, Some("label") => RoleSortField::Label, _ => RoleSortField::Weight, } } pub(crate) fn as_str(self) -> &'static str { match self { RoleSortField::Weight => "weight", RoleSortField::MachineName => "machine_name", RoleSortField::Label => "label", } } } #[derive(Clone, Debug)] pub(crate) struct RoleListItem { pub id: i32, pub machine_name: String, pub label: String, pub locked: bool, pub user_count: u64, } pub(crate) struct RoleListParams { pub sort: RoleSortField, pub dir: SortDir, } /// Devuelve todos los roles ordenados, sin paginar. Usado donde hace falta el catálogo completo /// (p. ej. la lista de roles asignables en la administración de usuarios). pub(crate) async fn list_roles(params: &RoleListParams) -> Result, AuthError> { let order = if params.dir == SortDir::Desc { Order::Desc } else { Order::Asc }; let select = role::Entity::find(); let select = match params.sort { RoleSortField::Weight => select.order_by(role::Column::Weight, order), RoleSortField::MachineName => select.order_by(role::Column::MachineName, order), RoleSortField::Label => select.order_by(role::Column::Label, order), }; let roles = select.all(dbconn()).await?; role_items(roles).await } pub(crate) struct RolePageParams { pub sort: RoleSortField, pub dir: SortDir, pub page: u64, pub per_page: u64, } /// Devuelve una página de roles. Usado por el listado de administración de roles. pub(crate) async fn list_roles_page( params: &RolePageParams, ) -> Result, AuthError> { let order = if params.dir == SortDir::Desc { Order::Desc } else { Order::Asc }; let select = role::Entity::find(); let select = match params.sort { RoleSortField::Weight => select.order_by(role::Column::Weight, order), RoleSortField::MachineName => select.order_by(role::Column::MachineName, order), RoleSortField::Label => select.order_by(role::Column::Label, order), }; paginate(select, params.page, params.per_page) .await? .map_items(role_items) .await } async fn role_items(roles: Vec) -> Result, AuthError> { let role_ids: Vec = roles.iter().map(|r| r.id).collect(); let counts: Vec<(i32, i64)> = user_role::Entity::find() .filter(user_role::Column::RoleId.is_in(role_ids)) .select_only() .column(user_role::Column::RoleId) .column_as(user_role::Column::RoleId.count(), "count") .group_by(user_role::Column::RoleId) .into_tuple() .all(dbconn()) .await?; let counts_by_role: HashMap = counts .into_iter() .map(|(role_id, count)| (role_id, count as u64)) .collect(); Ok(roles .into_iter() .map(|role| RoleListItem { user_count: counts_by_role.get(&role.id).copied().unwrap_or(0), id: role.id, machine_name: role.machine_name, label: role.label, locked: role.locked, }) .collect()) } // **< find_role / role_permission_keys >*************************************************************** pub(crate) async fn find_role(role_id: i32) -> Result { role::Entity::find_by_id(role_id) .one(dbconn()) .await? .ok_or(AuthError::RoleNotFound) } pub(crate) async fn role_permission_keys(role_id: i32) -> Result, AuthError> { let rows = role_permission::Entity::find() .filter(role_permission::Column::RoleId.eq(role_id)) .all(dbconn()) .await?; Ok(rows.into_iter().map(|r| r.permission_key).collect()) } // **< create_role >********************************************************************************* pub(crate) struct NewRoleData<'a> { pub machine_name: &'a str, pub label: &'a str, pub description: Option<&'a str>, pub weight: i32, } // Sólo minúsculas ASCII, dígitos y guiones bajos (ver el texto de ayuda del formulario, // "help-machine-name-immutable"); el machine_name es inmutable tras la creación. fn is_valid_machine_name(name: &str) -> bool { !name.is_empty() && name .bytes() .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_') } pub(crate) async fn create_role(data: NewRoleData<'_>) -> Result { if !is_valid_machine_name(data.machine_name) { return Err(AuthError::InvalidMachineName); } if role::Entity::find() .filter(role::Column::MachineName.eq(data.machine_name)) .one(dbconn()) .await? .is_some() { return Err(AuthError::RoleMachineNameTaken); } let now = Utc::now().naive_utc(); let new_role = role::ActiveModel { id: ActiveValue::NotSet, machine_name: Set(data.machine_name.to_owned()), label: Set(data.label.to_owned()), description: Set(data.description.map(str::to_owned)), weight: Set(data.weight), locked: Set(false), created_at: Set(now), updated_at: Set(now), }; let result = role::Entity::insert(new_role).exec(dbconn()).await?; Ok(result.last_insert_id) } // **< update_role >********************************************************************************* pub(crate) struct RoleUpdateData<'a> { pub label: &'a str, pub description: Option<&'a str>, pub weight: i32, } pub(crate) async fn update_role(role_id: i32, data: RoleUpdateData<'_>) -> Result<(), AuthError> { let role = find_role(role_id).await?; if role.locked { return Err(AuthError::RoleLocked); } let now = Utc::now().naive_utc(); role::ActiveModel { id: Set(role_id), label: Set(data.label.to_owned()), description: Set(data.description.map(str::to_owned)), weight: Set(data.weight), updated_at: Set(now), ..Default::default() } .update(dbconn()) .await?; Ok(()) } // **< delete_role >********************************************************************************* pub(crate) async fn delete_role(role_id: i32) -> Result<(), AuthError> { let role = find_role(role_id).await?; if role.locked { return Err(AuthError::RoleLocked); } let user_count = user_role::Entity::find() .filter(user_role::Column::RoleId.eq(role_id)) .count(dbconn()) .await?; if user_count > 0 { return Err(AuthError::RoleInUse); } role::Entity::delete_by_id(role_id).exec(dbconn()).await?; Ok(()) } // **< set_role_permissions >************************************************************************ /// Reemplaza por completo el conjunto de permisos concedidos a un rol. Permitido aunque el rol /// esté bloqueado (`locked`): los roles de sistema también necesitan permisos gestionables. pub(crate) async fn set_role_permissions( role_id: i32, permission_keys: &[String], ) -> Result<(), AuthError> { find_role(role_id).await?; let registry = permission::registry(); for key in permission_keys { if !registry.has_key(key) { return Err(AuthError::UnknownPermission(key.clone())); } } let now = Utc::now().naive_utc(); let keys = permission_keys.to_vec(); dbconn() .transaction::<_, _, AuthError>(|txn| { Box::pin(async move { role_permission::Entity::delete_many() .filter(role_permission::Column::RoleId.eq(role_id)) .exec(txn) .await?; for key in keys { role_permission::Entity::insert(role_permission::ActiveModel { role_id: Set(role_id), permission_key: Set(key), granted_at: Set(now), }) .exec(txn) .await?; } Ok(()) }) }) .await .map_err(flatten_txn_err) }