Skip to content
Clone
69f12aa
161 lines 4.2 KB Rust
use std::collections::HashSet;
use export::AppState;
use models::space::space_members::MemberStatus;
use models::space::{space_bans, space_members, spaces};
use sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter};
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<String>,
}
impl Permissions {
pub fn owner() -> Self {
Self {
granted: ALL_PERMISSIONS.into_iter().map(String::from).collect(),
}
}
pub fn from_vec(values: Vec<String>) -> Self {
Self {
granted: values.into_iter().collect(),
}
}
pub fn has(&self, action: &str) -> bool {
self.granted.contains(action)
}
pub fn into_vec(self) -> Vec<String> {
let mut v: Vec<String> = 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<String> {
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<Permissions, SpaceError> {
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<String> = 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(())
}