use crate::models::{ channel, channel_role_permission, channel_user_permission, computed_permission, role_user, server_role_permission, server_user, server_user_permission, }; use crate::permissions::{ChannelPermission, ServerPermission}; use crate::repositories::{AnyResult, RepositoryContext}; use sea_orm::{ ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect, Set, TransactionTrait, }; use std::collections::{HashMap, HashSet}; use std::sync::{Arc, OnceLock}; use uuid::Uuid; use crate::models::computed_permission::PermissionScopeType; use crate::repositories::types::PermissionResource; use crate::utils::ScopedLockManager; // Instance globale du manager de verrous scopés par Server ID static PERM_LOCK_MANAGER: OnceLock> = OnceLock::new(); fn lock_manager() -> &'static ScopedLockManager { PERM_LOCK_MANAGER.get_or_init(ScopedLockManager::new) } #[derive(Clone, Debug)] pub struct ComputedPermissionRepository { pub context: Arc, } impl ComputedPermissionRepository { /// Récupère toutes les permissions calculées. pub async fn get_all(&self) -> AnyResult> { Ok(computed_permission::Entity::find() .all(&self.context.db) .await?) } pub async fn get_for_resource( &self, user_id: Uuid, resource: PermissionResource, ) -> AnyResult> { Ok(computed_permission::Entity::find() .filter(computed_permission::Column::UserId.eq(user_id)) .filter(computed_permission::Column::ScopeType.eq(resource.scope_type())) .filter(computed_permission::Column::ResourceId.eq(resource.resource_id())) .one(&self.context.db) .await?) } /// Vérifie si l'utilisateur possède au moins une entrée de permission sur une ressource. pub async fn had_perm_on(&self, user_id: Uuid, resource_id: Uuid) -> AnyResult { Ok(computed_permission::Entity::find() .filter(computed_permission::Column::UserId.eq(user_id)) .filter(computed_permission::Column::ResourceId.eq(resource_id)) .count(&self.context.db) .await? > 0) } // ------------------------------------------------------------------------- // Synchronisations de permissions scopées // ------------------------------------------------------------------------- /// Scope SERVEUR : Recalcule le cache de permissions pour TOUS les utilisateurs du serveur. /// À n'utiliser que pour les opérations lourdes ou structurelles. pub async fn full_sync_server(&self, server_id: Uuid) -> AnyResult<()> { let user_ids = server_user::Entity::find() .filter(server_user::Column::ServerId.eq(server_id)) .select_only() .column(server_user::Column::UserId) .into_tuple::() .all(&self.context.db) .await?; for user_id in user_ids { self.full_sync_user(user_id, server_id).await?; } Ok(()) } /// Répare les membres sans aucune ligne de cache. Les membres déjà synchronisés ne sont pas recalculés. pub async fn repair_missing_members(&self) -> AnyResult> { let members = server_user::Entity::find() .select_only() .column(server_user::Column::UserId) .column(server_user::Column::ServerId) .into_tuple::<(Uuid, Uuid)>() .all(&self.context.db) .await?; let cached: HashSet<(Uuid, Uuid)> = computed_permission::Entity::find() .select_only() .column(computed_permission::Column::UserId) .column(computed_permission::Column::ServerId) .distinct() .into_tuple::<(Uuid, Uuid)>() .all(&self.context.db) .await? .into_iter() .collect(); let mut repaired = Vec::new(); for (user_id, server_id) in members { if cached.contains(&(user_id, server_id)) { continue; } self.full_sync_user(user_id, server_id).await?; repaired.push((user_id, server_id)); } Ok(repaired) } /// Scope RÔLE : Recalcule le cache uniquement pour les membres d'un rôle spécifique. pub async fn sync_role_members(&self, role_id: Uuid, server_id: Uuid) -> AnyResult<()> { let user_ids = role_user::Entity::find() .filter(role_user::Column::RoleId.eq(role_id)) .select_only() .column(role_user::Column::UserId) .into_tuple::() .all(&self.context.db) .await?; for user_id in user_ids { self.full_sync_user(user_id, server_id).await?; } Ok(()) } /// Scope UTILISATEUR : Recalcule le cache de permissions d'un seul utilisateur sur un serveur. /// /// Les permissions effectives sont composées de : /// - permissions serveur accordées aux rôles de l'utilisateur ; /// - permissions serveur accordées directement à l'utilisateur ; /// - permissions de canal accordées aux rôles de l'utilisateur ; /// - permissions directes de l'utilisateur dans les canaux. pub async fn full_sync_user(&self, user_id: Uuid, server_id: Uuid) -> AnyResult<()> { let _server_guard = lock_manager().lock_scope(server_id).await; // --------------------------------------------------------------------- // 1. Rôles de l'utilisateur // --------------------------------------------------------------------- let role_ids = role_user::Entity::find() .filter(role_user::Column::UserId.eq(user_id)) .select_only() .column(role_user::Column::RoleId) .into_tuple::() .all(&self.context.db) .await?; // --------------------------------------------------------------------- // 2. Permissions serveur des rôles // --------------------------------------------------------------------- let mut server_permissions = ServerPermission::empty(); if !role_ids.is_empty() { let role_permissions = server_role_permission::Entity::find() .filter(server_role_permission::Column::ServerId.eq(server_id)) .filter(server_role_permission::Column::RoleId.is_in(role_ids.clone())) .all(&self.context.db) .await?; for permission in role_permissions { server_permissions |= ServerPermission::from_bits_retain(permission.permissions as u64); } } // --------------------------------------------------------------------- // 3. Permissions serveur directes de l'utilisateur // --------------------------------------------------------------------- if let Some(permission) = server_user_permission::Entity::find() .filter(server_user_permission::Column::ServerId.eq(server_id)) .filter(server_user_permission::Column::UserId.eq(user_id)) .one(&self.context.db) .await? { server_permissions |= ServerPermission::from_bits_retain(permission.permissions as u64); } // --------------------------------------------------------------------- // 4. Canaux du serveur // --------------------------------------------------------------------- let channels = channel::Entity::find() .filter(channel::Column::ServerId.eq(server_id)) .all(&self.context.db) .await?; let channel_ids: Vec = channels.iter().map(|c| c.id).collect(); // --------------------------------------------------------------------- // 5. Permissions de rôles pour tous les canaux // --------------------------------------------------------------------- let role_channel_permissions = if role_ids.is_empty() || channel_ids.is_empty() { Vec::new() } else { channel_role_permission::Entity::find() .filter(channel_role_permission::Column::ChannelId.is_in(channel_ids.clone())) .filter(channel_role_permission::Column::RoleId.is_in(role_ids)) .all(&self.context.db) .await? }; let mut permissions_by_channel: HashMap = HashMap::new(); for permission in role_channel_permissions { permissions_by_channel .entry(permission.channel_id) .or_default() .insert(ChannelPermission::from_bits_retain( permission.permissions as u64, )); } // --------------------------------------------------------------------- // 6. Permissions directes de l'utilisateur pour tous les canaux // --------------------------------------------------------------------- let user_channel_permissions = if channel_ids.is_empty() { Vec::new() } else { channel_user_permission::Entity::find() .filter(channel_user_permission::Column::UserId.eq(user_id)) .filter(channel_user_permission::Column::ChannelId.is_in(channel_ids)) .all(&self.context.db) .await? }; for permission in user_channel_permissions { permissions_by_channel .entry(permission.channel_id) .or_default() .insert(ChannelPermission::from_bits_retain( permission.permissions as u64, )); } // --------------------------------------------------------------------- // 7. Construction des modèles à insérer // --------------------------------------------------------------------- let mut computed_permissions = Vec::with_capacity(channels.len().saturating_add(1)); // Permissions au niveau serveur computed_permissions.push(computed_permission::ActiveModel { user_id: Set(user_id), server_id: Set(server_id), scope_type: Set(PermissionScopeType::Server), resource_id: Set(server_id), permissions: Set(server_permissions.bits() as i64), ..Default::default() }); // Permissions au niveau canal for channel in channels { let channel_permissions = permissions_by_channel .remove(&channel.id) .unwrap_or_else(ChannelPermission::empty); computed_permissions.push(computed_permission::ActiveModel { user_id: Set(user_id), server_id: Set(server_id), scope_type: Set(PermissionScopeType::Channel), resource_id: Set(channel.id), permissions: Set(channel_permissions.bits() as i64), ..Default::default() }); } // --------------------------------------------------------------------- // 8. Remplacement atomique du cache en BDD // --------------------------------------------------------------------- self.context .db .transaction::<_, (), anyhow::Error>(|transaction| { Box::pin(async move { computed_permission::Entity::delete_many() .filter(computed_permission::Column::UserId.eq(user_id)) .filter(computed_permission::Column::ServerId.eq(server_id)) .exec(transaction) .await?; if !computed_permissions.is_empty() { computed_permission::Entity::insert_many(computed_permissions) .exec(transaction) .await?; } Ok(()) }) }) .await?; Ok(()) } } #[cfg(test)] mod tests { use crate::http::test_support::{state, user}; use crate::models::computed_permission; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; #[tokio::test] async fn repair_missing_members_is_idempotent() { let state = state().await; let owner = user(&state, false).await; let server = state .services .server .create_server("repair".into(), None, false, owner.id) .await .unwrap(); computed_permission::Entity::delete_many() .filter(computed_permission::Column::UserId.eq(owner.id)) .filter(computed_permission::Column::ServerId.eq(server.id)) .exec(&state.db) .await .unwrap(); let first = state .repositories .computed_permission .repair_missing_members() .await .unwrap(); let second = state .repositories .computed_permission .repair_missing_members() .await .unwrap(); assert!(first.contains(&(owner.id, server.id))); assert!(second.is_empty()); } }