use std::collections::HashSet; use db::conn::sea_orm::*; use db::space::space_bans as ban_entity; use db::space::space_member_roles as member_role_entity; use db::space::space_members as member_entity; use db::space::space_role_permissions as role_perm_entity; use db::space::spaces as space_entity; use time::OffsetDateTime; use uuid::Uuid; use crate::error::{SpaceError, SpaceResult}; use crate::features::SpaceService; pub const VIEW_SPACE: &str = "view_space"; pub const READ: &str = "read"; pub const MESSAGE: &str = "message"; pub const MANAGE_MEMBERS: &str = "manage_members"; pub const MANAGE_ROLES: &str = "manage_roles"; pub const MANAGE_INVITES: &str = "manage_invites"; pub const MANAGE_BANS: &str = "manage_bans"; pub const MANAGE_CHANNELS: &str = "manage_channels"; pub const MANAGE_OVERWRITES: &str = "manage_overwrites"; #[derive(Debug, Clone, Default)] pub struct Permissions { granted: HashSet, } impl Permissions { fn all() -> Self { Self { granted: [ VIEW_SPACE, READ, MESSAGE, MANAGE_MEMBERS, MANAGE_ROLES, MANAGE_INVITES, MANAGE_BANS, MANAGE_CHANNELS, MANAGE_OVERWRITES, ] .iter() .map(ToString::to_string) .collect(), } } pub fn has(&self, action: &str) -> bool { self.granted.contains(action) } } /// Resolve the caller's effective permissions for a space. /// /// - Space owner always holds every permission. /// - A non-expired ban denies access regardless of membership. /// - Otherwise the caller must be an active member; permissions are the union /// of all permissions granted by the roles assigned to the caller. pub async fn effective_permissions( db: &C, space_id: Uuid, user_id: Uuid, ) -> SpaceResult<(space_entity::Model, Permissions)> where C: ConnectionTrait, { let space = space_entity::Entity::find_by_id(space_id) .one(db) .await? .ok_or_else(|| SpaceError::NotFound(format!("space {space_id}")))?; if space.owner_id == user_id { return Ok((space, Permissions::all())); } let now = OffsetDateTime::now_utc(); let ban = ban_entity::Entity::find_by_id((space_id, user_id)).one(db).await?; if let Some(b) = ban && b.expires_at.is_none_or(|exp| exp > now) { return Err(SpaceError::Forbidden( "caller is banned from this space".to_string(), )); } let member = member_entity::Entity::find_by_id((space_id, user_id)).one(db).await?; let active = member .map(|m| m.status == member_entity::MemberStatus::Active) .unwrap_or(false); if !active { return Err(SpaceError::Forbidden( "caller is not an active member of this space".to_string(), )); } let role_rows = member_role_entity::Entity::find() .filter(member_role_entity::Column::SpaceId.eq(space_id)) .filter(member_role_entity::Column::UserId.eq(user_id)) .all(db) .await?; let role_ids: Vec = role_rows.into_iter().map(|r| r.role_id).collect(); if role_ids.is_empty() { return Ok((space, Permissions::default())); } let perm_rows = role_perm_entity::Entity::find() .filter(role_perm_entity::Column::RoleId.is_in(role_ids)) .all(db) .await?; let granted = perm_rows.into_iter().map(|r| r.permission).collect(); Ok((space, Permissions { granted })) } /// Require a specific permission for `caller` on `space_id`. pub async fn require_perm( svc: &SpaceService, caller: Uuid, space_id: Uuid, action: &str, ) -> SpaceResult { let db = svc.db.reader(); let (space, perms) = effective_permissions(&db, space_id, caller).await?; if !perms.has(action) { return Err(SpaceError::Forbidden(format!( "caller lacks permission `{action}` for space {space_id}" ))); } Ok(space) }