use cache::Cache; use db::DbConn; use models::repo::repo_collaborations::{self, CollaborationMode}; use models::space::{SpaceStatus, space_extras, space_groups, space_members, spaces}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use time::OffsetDateTime; use uuid::Uuid; use api::AccessError; #[derive(Clone)] pub struct Access { pub(crate) database: DbConn, pub(crate) cache: Cache, } #[derive(Clone)] pub struct Space { pub(crate) record: spaces::Model, access: Access, } #[derive(Clone, Copy)] pub enum Capability { Read, Message, ManageChannels, ManageInvites, RepositoryRead { repository: Uuid, private: bool }, RepositoryWrite { repository: Uuid }, RepositoryAdmin { repository: Uuid }, } impl Access { pub fn new(database: DbConn, cache: Cache) -> Self { Self { database, cache } } pub async fn by_id(&self, id: Uuid) -> Result { let record = spaces::Entity::find_by_id(id) .one(self.database.read()) .await? .filter(|row| { row.deleted_at.is_none() && row.status == SpaceStatus::Active && row.archived_at.is_none() }) .ok_or(AccessError::NotFound)?; Ok(Space { record, access: self.clone(), }) } pub async fn by_slug(&self, slug: &str) -> Result { let record = spaces::Entity::find() .filter(spaces::Column::Slug.eq(slug.trim().to_lowercase())) .one(self.database.read()) .await? .filter(|row| { row.deleted_at.is_none() && row.status == SpaceStatus::Active && row.archived_at.is_none() }) .ok_or(AccessError::NotFound)?; Ok(Space { record, access: self.clone(), }) } pub async fn user_spaces(&self, user: i64) -> Result, AccessError> { let memberships = space_members::Entity::find() .filter(space_members::Column::UserId.eq(user)) .filter(space_members::Column::LeftAt.is_null()) .filter(space_members::Column::BannedAt.is_null()) .all(self.database.read()) .await?; let mut ids: Vec = memberships .into_iter() .map(|row| row.organization_id) .collect(); let owned = spaces::Entity::find() .filter(spaces::Column::OwnerUserId.eq(user)) .all(self.database.read()) .await?; ids.extend(owned.into_iter().map(|row| row.id)); ids.sort_unstable(); ids.dedup(); let mut result = Vec::new(); for id in ids { match self.by_id(id).await { Ok(space) => result.push(space), Err(AccessError::NotFound) => {} Err(error) => return Err(error), } } Ok(result) } pub async fn member(&self, space: Uuid, user: i64) -> Result, AccessError> { self.by_id(space).await?.member(user).await } pub async fn members(&self, space: Uuid, actor: i64) -> Result, AccessError> { self.by_id(space).await?.members(actor).await } } impl Space { pub fn id(&self) -> Uuid { self.record.id } pub fn owner(&self) -> i64 { self.record.owner_user_id } pub fn slug(&self) -> &str { &self.record.slug } pub fn name(&self) -> &str { &self.record.name } pub fn is_private(&self) -> bool { self.record.join_policy == models::space::SpaceJoinPolicy::Private } pub fn created_at(&self) -> OffsetDateTime { crate::authority::to_time(self.record.created_at) } pub async fn avatar_url(&self) -> Result, AccessError> { let extras = space_extras::Entity::find_by_id(self.id()) .one(self.access.database.read()) .await?; Ok(crate::services::dto::resolve_space_image_url( self.slug(), "avatar", extras .and_then(|row| row.avatar_url) .or_else(|| self.record.avatar_url.clone()), )) } pub async fn member_count(&self) -> Result { use sea_orm::PaginatorTrait; Ok(space_members::Entity::find() .filter(space_members::Column::OrganizationId.eq(self.id())) .filter(space_members::Column::LeftAt.is_null()) .filter(space_members::Column::BannedAt.is_null()) .count(self.access.database.read()) .await? as i64) } pub async fn member(&self, user: i64) -> Result, AccessError> { Ok(space_members::Entity::find() .filter(space_members::Column::OrganizationId.eq(self.id())) .filter(space_members::Column::UserId.eq(user)) .one(self.access.database.read()) .await? .map(Member::from)) } pub async fn require_member(&self, user: i64) -> Result { require(self, user, Capability::Read).await?; self.member(user) .await? .filter(|member| member.status() == crate::MemberStatus::Active) .ok_or(AccessError::Forbidden) } pub async fn members(&self, actor: i64) -> Result, AccessError> { require(self, actor, Capability::Read).await?; Ok(space_members::Entity::find() .filter(space_members::Column::OrganizationId.eq(self.id())) .filter(space_members::Column::LeftAt.is_null()) .filter(space_members::Column::BannedAt.is_null()) .all(self.access.database.read()) .await? .into_iter() .map(Member::from) .collect()) } pub async fn group_id_by_name(&self, name: &str) -> Result, AccessError> { Ok(space_groups::Entity::find() .filter(space_groups::Column::SpaceId.eq(self.id())) .filter(space_groups::Column::Name.eq(name)) .one(self.access.database.read()) .await? .map(|row| row.id)) } pub async fn permissions(&self, user: i64) -> Result, AccessError> { Ok(crate::services::permission::effective_for( &self.access.database, &self.access.cache, self.id(), user, ) .await? .into_iter() .filter_map(|permission| permission.strip_prefix("space.").map(str::to_owned)) .collect()) } pub async fn can(&self, user: i64, capability: Capability) -> Result { let repository = match capability { Capability::RepositoryRead { repository, private, } => Some((repository, !private, false, false)), Capability::RepositoryWrite { repository } => Some((repository, false, true, false)), Capability::RepositoryAdmin { repository } => Some((repository, false, false, true)), _ => None, }; if let Some((repository, public, write, admin)) = repository { let current = self.access.by_id(self.id()).await?; if current.owner() == user || public { return Ok(true); } let collaboration = repo_collaborations::Entity::find() .filter(repo_collaborations::Column::RepoId.eq(repository)) .filter(repo_collaborations::Column::UserId.eq(user)) .one(self.access.database.read()) .await?; return Ok(match collaboration.map(|row| row.mode) { Some(CollaborationMode::Admin) => true, Some(CollaborationMode::Write) => !admin, Some(_) => !write && !admin, None => false, }); } let names = self.permissions(user).await?; let allowed = |permission: &str| names.iter().any(|name| name == permission); Ok(match capability { Capability::Read => allowed("view_space") || allowed("read"), Capability::Message => allowed("message"), Capability::ManageChannels => allowed("manage_channels"), Capability::ManageInvites => allowed("manage_invites"), _ => false, }) } } pub async fn require(space: &Space, user: i64, capability: Capability) -> Result<(), AccessError> { if space.can(user, capability).await? { Ok(()) } else { Err(AccessError::Forbidden) } } #[derive(Clone, Debug)] pub struct Member { pub user_id: i64, pub alias_name: Option, pub alias_avatar_url: Option, pub message_notifications: crate::MessageNotifications, pub muted: bool, pub mute_until: Option, pub mobile_push: bool, pub suppress_everyone: bool, pub suppress_roles: bool, pub hide_muted_channels: bool, status: crate::MemberStatus, } impl Member { pub fn status(&self) -> crate::MemberStatus { self.status } } impl From for Member { fn from(row: space_members::Model) -> Self { let status = row.status(); Self { user_id: row.user_id, alias_name: row.alias_name, alias_avatar_url: row.alias_avatar_url, message_notifications: row.message_notifications, muted: row.muted, mute_until: crate::authority::to_time_opt(row.mute_until), mobile_push: row.mobile_push, suppress_everyone: row.suppress_everyone, suppress_roles: row.suppress_roles, hide_muted_channels: row.hide_muted_channels, status, } } } impl From for AccessError { fn from(error: crate::errors::SpaceError) -> Self { use crate::errors::SpaceError; match error { SpaceError::NotFound | SpaceError::SpaceNotFound | SpaceError::SpaceInactive => { Self::NotFound } SpaceError::Unauthorized => Self::Unauthorized, SpaceError::Forbidden | SpaceError::SpaceMemberBanned | SpaceError::SpaceMemberNotFound => Self::Forbidden, SpaceError::BadRequest(message) => Self::InvalidArgument(message), other => Self::Internal(other.to_string()), } } }