Skip to content
Clone
a06673b
131 lines 4.0 KB Rust
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<String>,
}
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<C>(
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<Uuid> = 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<space_entity::Model> {
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)
}