use std::collections::HashSet; use export::AppState; use models::space::{space_bans, spaces}; use sea_orm::{ConnectionTrait, EntityTrait}; use time::OffsetDateTime; use uuid::Uuid; use crate::errors::SpaceError; use crate::services::service::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"; pub const ALL_PERMISSIONS: [&str; 9] = [ VIEW_SPACE, READ, MESSAGE, MANAGE_MEMBERS, MANAGE_ROLES, MANAGE_INVITES, MANAGE_BANS, MANAGE_CHANNELS, MANAGE_OVERWRITES, ]; const PERMISSION_CACHE_TTL_SECS: u64 = 300; #[derive(Debug, Clone, Default)] pub struct Permissions { granted: HashSet, } impl Permissions { pub fn owner() -> Self { Self { granted: ALL_PERMISSIONS.into_iter().map(String::from).collect(), } } pub fn from_vec(values: Vec) -> Self { Self { granted: values.into_iter().collect(), } } pub fn has(&self, action: &str) -> bool { self.granted.contains(action) } pub fn into_vec(self) -> Vec { let mut v: Vec = self.granted.into_iter().collect(); v.sort(); v.dedup(); v } } pub async fn assert_not_banned( db: &impl ConnectionTrait, space_id: Uuid, user_id: i64, ) -> Result<(), SpaceError> { let now = OffsetDateTime::now_utc(); if let Some(ban) = space_bans::Entity::find_by_id((space_id, user_id)) .one(db) .await? && (ban.expires_at.is_none() || ban.expires_at > Some(now)) { return Err(SpaceError::Forbidden); } Ok(()) } fn cache_key(space: &spaces::Model, user_id: i64) -> String { format!( "space:perm:{}:{}:{}", space.id, user_id, space.version ) } fn decode(raw: &str) -> Vec { raw.split(',') .map(str::trim) .filter(|value| !value.is_empty()) .map(String::from) .collect() } pub async fn effective_permissions( state: &AppState, space: &spaces::Model, user_id: i64, ) -> Result { if space.owner_id == user_id { return Ok(Permissions::owner()); } let key = cache_key(space, user_id); if let Ok(Some(raw)) = export::redis::get_string(&state.redis, &key).await { let granted = decode(&raw); if granted.is_empty() { return Err(SpaceError::Forbidden); } return Ok(Permissions::from_vec(granted)); } let client = state .kenauth .as_ref() .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?; let permissions: Vec = ALL_PERMISSIONS .iter() .map(|action| format!("space.{action}")) .collect(); let response = client .check_permissions(user_id, &space.id.to_string(), user_id, permissions) .await?; let mut granted = Vec::new(); for (index, action) in ALL_PERMISSIONS.iter().enumerate() { if response.allowed.get(index).copied().unwrap_or(false) { granted.push((*action).to_string()); } } let encoded = granted.join(","); if let Err(error) = export::redis::set_string( &state.redis, &key, &encoded, PERMISSION_CACHE_TTL_SECS, ) .await { tracing::warn!(%error, "space permission cache write failed"); } if granted.is_empty() { return Err(SpaceError::Forbidden); } Ok(Permissions::from_vec(granted)) } pub async fn require_perm( service: &SpaceService, space: &spaces::Model, caller: i64, action: &str, ) -> Result<(), SpaceError> { let perms = effective_permissions(&service.state, space, caller).await?; if !perms.has(action) { return Err(SpaceError::Forbidden); } Ok(()) }