This commit is contained in:
2026-03-16 00:56:28 +01:00
parent 50e1d4c25f
commit 628582a48b
21 changed files with 418 additions and 353 deletions

View File

@@ -1,6 +1,3 @@
use std::collections::HashMap;
use uuid::Uuid;
/// # Système de Permissions par Bitmask
///
/// Un bitmask (masque de bits) est une manière compacte et efficace de stocker plusieurs
@@ -70,67 +67,113 @@ impl Permission {
}
}
/// Contexte de permissions calculé pour un utilisateur donné.
///
/// Ce contexte regroupe les permissions globales (issues de la fusion de tous les groupes
/// de l'utilisateur) et les surcharges spécifiques par canal (overrides).
#[derive(Debug, Clone)]
pub struct PermissionContext {
/// Union des masques de tous les groupes auxquels appartient l'utilisateur.
pub global: u64,
/// Masques spécifiques par canal (si présents, ils remplacent totalement le masque global
/// pour ce canal spécifique).
pub channel_overrides: HashMap<Uuid, u64>,
pub struct PermissionStack {
/// Permissions au niveau du serveur (None = non configuré)
pub server: Option<u64>,
/// Permissions de TOUS les groupes de l'utilisateur (None = aucun groupe)
pub groups: Option<u64>,
/// Permissions de l'utilisateur au niveau serveur (None = non défini)
pub server_user: Option<u64>,
/// Permissions de TOUS les groupes au niveau serveur (None = non défini)
pub server_groups: Option<u64>,
/// Permissions de l'utilisateur sur ce canal (None = non défini)
pub channel_user: Option<u64>,
/// Permissions de TOUS les groupes sur ce canal (None = non défini)
pub channel_groups: Option<u64>,
}
impl PermissionContext {
/// Crée un nouveau contexte à partir des masques bruts de la base de données.
///
/// - `group_masks` : Liste des permissions de chaque groupe de l'utilisateur.
/// - `channel_permissions` : Liste de (ID Canal, Masque) pour les permissions spécifiques.
pub fn new(group_masks: &[i64], channel_permissions: Vec<(Uuid, i64)>) -> Self {
// On combine tous les groupes avec l'opérateur OR (|)
let global = group_masks.iter().fold(0u64, |acc, &m| acc | m as u64);
let channel_overrides = channel_permissions
.into_iter()
.map(|(id, m)| (id, m as u64))
.collect();
impl PermissionStack {
pub fn new() -> Self {
Self {
global,
channel_overrides,
server: None,
groups: None,
server_user: None,
server_groups: None,
channel_user: None,
channel_groups: None,
}
}
/// Vérifie si une permission spécifique est accordée, optionnellement pour un canal précis.
/// Résout les permissions finales en mode Union
/// On accumule TOUTES les sources définies avec OR
pub fn resolve(&self) -> u64 {
let mut result = 0u64;
if let Some(perms) = self.server {
result |= perms;
}
if let Some(perms) = self.groups {
result |= perms;
}
if let Some(perms) = self.server_user {
result |= perms;
}
if let Some(perms) = self.server_groups {
result |= perms;
}
if let Some(perms) = self.channel_user {
result |= perms;
}
if let Some(perms) = self.channel_groups {
result |= perms;
}
result
}
/// Vérifie si une permission spécifique est accordée.
///
/// Si `channel_id` est fourni et qu'une surcharge existe pour ce canal, la surcharge
/// est utilisée. Sinon, on utilise les permissions globales du serveur.
///
/// ### Exemple d'utilisation :
/// ### Exemple :
/// ```rust
/// if ctx.has(Permission::SendMessage, Some(channel_id)) {
/// // Autorisé !
/// if stack.has(Permission::SendMessage) {
/// println!("L'utilisateur peut envoyer des messages !");
/// }
/// ```
#[inline]
pub fn has(&self, permission: Permission, channel_id: Option<Uuid>) -> bool {
let mask = if let Some(cid) = channel_id {
self.channel_overrides
.get(&cid)
.copied()
.unwrap_or(self.global)
} else {
self.global
};
pub fn has(&self, permission: Permission) -> bool {
let mask = self.resolve();
mask & (permission as u64) != 0
}
/// Méthode utilitaire pour vérifier rapidement l'accès en lecture à un canal.
pub fn can_see_channel(&self, channel_id: Uuid) -> bool {
self.has(Permission::ReadChannel, Some(channel_id))
/// Vérifie si TOUTES les permissions listées sont accordées (Strict).
///
/// Utile pour des actions nécessitant plusieurs droits combinés.
///
/// ### Exemple :
/// ```rust
/// // L'utilisateur doit pouvoir voir ET parler pour rejoindre un vocal
/// let perms = [Permission::ReadChannel, Permission::VoiceSpeak];
/// if stack.has_all(&perms) {
/// join_voice_channel();
/// }
/// ```
pub fn has_all(&self, permissions: &[Permission]) -> bool {
let required = permissions.iter().fold(0u64, |acc, &p| acc | p as u64);
let mask = self.resolve();
(mask & required) == required
}
/// Vérifie si AU MOINS UNE des permissions listées est accordée.
///
/// Utile pour les rôles de modération ou les accès "ou" (OR).
///
/// ### Exemple :
/// ```rust
/// // L'utilisateur peut supprimer le message s'il est modérateur OU admin
/// let moderator_perms = [Permission::DeleteOthersMessage, Permission::ManageChannel];
/// if stack.has_any(&moderator_perms) {
/// delete_message();
/// }
/// ```
pub fn has_any(&self, permissions: &[Permission]) -> bool {
let required = permissions.iter().fold(0u64, |acc, &p| acc | p as u64);
let mask = self.resolve();
(mask & required) != 0
}
}
@@ -139,45 +182,32 @@ mod tests {
use super::*;
#[test]
fn test_permission_context() {
let channel_id = Uuid::new_v4();
let other_channel_id = Uuid::new_v4();
fn test_permission_stack() {
let mut stack = PermissionStack::new();
// On imagine un utilisateur avec 2 groupes
let role_masks = vec![
Permission::ReadChannel as i64 | Permission::SendMessage as i64, // Groupe 1 : lecture + envoi
Permission::VoiceSpeak as i64, // Groupe 2 : vocal
];
// 1. Test des permissions par défaut du serveur
stack.server = Some(Permission::ReadChannel as u64 | Permission::SendMessage as u64);
assert!(stack.has(Permission::ReadChannel));
assert!(stack.has(Permission::SendMessage));
assert!(!stack.has(Permission::VoiceSpeak));
// Et une permission spécifique sur un canal (Lecture + Envoi + Gestion)
let channel_permissions = vec![(
channel_id,
Permission::ReadChannel as i64
| Permission::SendMessage as i64
| Permission::ManageChannel as i64,
)];
// 2. Test de l'union avec les groupes
stack.groups = Some(Permission::VoiceSpeak as u64);
assert!(stack.has(Permission::ReadChannel));
assert!(stack.has(Permission::SendMessage));
assert!(stack.has(Permission::VoiceSpeak));
let ctx = PermissionContext::new(&role_masks, channel_permissions);
// 3. Test du bypass administrateur (Saturation du masque)
stack.server_user = Some(u64::MAX);
assert!(stack.has(Permission::ManageServer));
assert!(stack.has(Permission::ManageRoles));
assert!(stack.has(Permission::KickMember));
// Test des permissions globales
assert!(ctx.has(Permission::ReadChannel, None));
assert!(ctx.has(Permission::SendMessage, None));
assert!(ctx.has(Permission::VoiceSpeak, None));
assert!(!ctx.has(Permission::ManageChannel, None));
// Test des surcharges par canal (L'override remplace le masque global)
assert!(ctx.has(Permission::ReadChannel, Some(channel_id)));
assert!(ctx.has(Permission::SendMessage, Some(channel_id)));
assert!(ctx.has(Permission::ManageChannel, Some(channel_id)));
assert!(!ctx.has(Permission::VoiceSpeak, Some(channel_id))); // Ici on perd le vocal car l'override remplace tout
// Test d'un autre canal (pas de surcharge, on retombe sur le global)
assert!(ctx.has(Permission::ReadChannel, Some(other_channel_id)));
assert!(ctx.has(Permission::VoiceSpeak, Some(other_channel_id)));
assert!(!ctx.has(Permission::ManageChannel, Some(other_channel_id)));
// Test des fonctions sucre
assert!(ctx.can_see_channel(channel_id));
assert!(ctx.can_see_channel(other_channel_id));
// 4. Test spécifique au canal (Sans bypass admin)
stack.server_user = None;
stack.channel_user = Some(Permission::ManageChannel as u64);
assert!(stack.has(Permission::ReadChannel)); // Vient du serveur
assert!(stack.has(Permission::ManageChannel)); // Vient du channel_user
assert!(!stack.has(Permission::ManageRoles)); // Pas défini
}
}