use cache::Cache; use db::DbConn; use models::dir::dirs; 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 std::collections::{HashMap, HashSet}; use time::OffsetDateTime; use uuid::Uuid; use export::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 } pub async fn space_id_for_dir(&self, mut id: Uuid) -> Result, AccessError> { let mut visited = HashSet::new(); for _ in 0..64 { if !visited.insert(id) { return Err(AccessError::InvalidArgument("directory cycle".to_owned())); } let dir = dirs::Entity::find_by_id(id) .one(self.database.read()) .await? .ok_or(AccessError::NotFound)?; if let Some(space) = dir.space_id { return spaces::Entity::find_by_id(space) .one(self.database.read()) .await? .filter(|row| row.deleted_at.is_none()) .map(|row| Some(row.id)) .ok_or(AccessError::NotFound); } match dir.parent_dir_id { Some(parent) => id = parent, None => return Ok(None), } } Err(AccessError::InvalidArgument( "directory tree exceeds 64 levels".to_owned(), )) } pub async fn for_dir(&self, id: Uuid) -> Result, AccessError> { match self.space_id_for_dir(id).await? { Some(id) => self.by_id(id).await.map(Some), None => Ok(None), } } } 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 object_visible(&self, user: i64, dir: Uuid) -> Result { let Some(node) = dirs::Entity::find_by_id(dir) .one(self.access.database.read()) .await? else { return Ok(false); }; if !node.kind.is_object() { return Ok(false); } self.dir_visible(user, dir).await } pub async fn dir_visible(&self, user: i64, dir: Uuid) -> Result { require(self, user, Capability::Read).await?; if self .access .for_dir(dir) .await? .is_none_or(|space| space.id() != self.id()) { return Ok(false); } crate::share::dir_visibility::is_dir_visible( self.access.database.read(), &self.record, dir, user, ) .await .map_err(AccessError::from) } /// 给定候选用户集合,返回其中能看到该目录的用户(含祖先链、过期覆盖项与归档节点)。 pub async fn dir_visible_users( &self, dir: Uuid, users: &[i64], ) -> Result, AccessError> { if users.is_empty() { return Ok(HashSet::new()); } let db = self.access.database.read(); let nodes = dirs::Entity::find() .filter(dirs::Column::SpaceId.eq(self.id())) .all(db) .await?; let mut parent_map: HashMap> = HashMap::new(); let mut archived: HashSet = HashSet::new(); for node in &nodes { parent_map.insert(node.id, node.parent_dir_id); if node.is_archived { archived.insert(node.id); } } if !parent_map.contains_key(&dir) { return Ok(HashSet::new()); } let node_ids: Vec = nodes.iter().map(|node| node.id).collect(); let member_groups = models::space::space_member_groups::Entity::find() .filter(models::space::space_member_groups::Column::SpaceId.eq(self.id())) .filter(models::space::space_member_groups::Column::UserId.is_in(users.to_vec())) .all(db) .await?; let mut groups_by_user: HashMap> = HashMap::new(); for row in member_groups { groups_by_user .entry(row.user_id) .or_default() .insert(row.group_id); } let now = OffsetDateTime::now_utc(); let mut group_ows: HashMap> = HashMap::new(); for row in models::dir::dir_overwrite_group::Entity::find() .filter(models::dir::dir_overwrite_group::Column::DirId.is_in(node_ids.clone())) .all(db) .await? { group_ows.entry(row.dir_id).or_default().push(row); } let mut user_ows: HashMap> = HashMap::new(); for row in models::dir::dir_overwrite_users::Entity::find() .filter(models::dir::dir_overwrite_users::Column::DirId.is_in(node_ids)) .all(db) .await? { if !crate::share::dir_visibility::user_overwrite_active(&row, now) { continue; } user_ows.entry(row.dir_id).or_default().push(row); } let mut visible = HashSet::new(); for user in users { if self.record.owner_user_id == *user { visible.insert(*user); continue; } let empty = HashSet::new(); let my_groups = groups_by_user.get(user).unwrap_or(&empty); let mut current = Some(dir); let mut seen = HashSet::new(); let mut allowed = true; while let Some(id) = current { if !seen.insert(id) || archived.contains(&id) { allowed = false; break; } let direct = crate::share::dir_visibility::node_visible( *user, my_groups, group_ows.get(&id).map(Vec::as_slice).unwrap_or(&[]), user_ows.get(&id).map(Vec::as_slice).unwrap_or(&[]), ); if !direct { allowed = false; break; } current = parent_map.get(&id).copied().flatten(); } if allowed { visible.insert(*user); } } Ok(visible) } /// 批量计算目录可见性(含祖先链、过期覆盖项与归档节点),供列表类接口一次加载。 pub async fn visible_dirs( &self, user: i64, dir_ids: &[Uuid], ) -> Result, AccessError> { if dir_ids.is_empty() { return Ok(HashSet::new()); } if self.record.owner_user_id == user { return Ok(dir_ids.iter().copied().collect()); } require(self, user, Capability::Read).await?; let db = self.access.database.read(); let nodes = dirs::Entity::find() .filter(dirs::Column::SpaceId.eq(self.id())) .all(db) .await?; let mut parent_map: HashMap> = HashMap::new(); let mut archived: HashSet = HashSet::new(); for node in &nodes { parent_map.insert(node.id, node.parent_dir_id); if node.is_archived { archived.insert(node.id); } } let node_ids: Vec = nodes.iter().map(|node| node.id).collect(); let my_groups: HashSet = models::space::space_member_groups::Entity::find() .filter(models::space::space_member_groups::Column::SpaceId.eq(self.id())) .filter(models::space::space_member_groups::Column::UserId.eq(user)) .all(db) .await? .into_iter() .map(|member| member.group_id) .collect(); let now = OffsetDateTime::now_utc(); let mut group_ows: HashMap> = HashMap::new(); for row in models::dir::dir_overwrite_group::Entity::find() .filter(models::dir::dir_overwrite_group::Column::DirId.is_in(node_ids.clone())) .all(db) .await? { group_ows.entry(row.dir_id).or_default().push(row); } let mut user_ows: HashMap> = HashMap::new(); for row in models::dir::dir_overwrite_users::Entity::find() .filter(models::dir::dir_overwrite_users::Column::DirId.is_in(node_ids)) .all(db) .await? { if !crate::share::dir_visibility::user_overwrite_active(&row, now) { continue; } user_ows.entry(row.dir_id).or_default().push(row); } let mut visible = HashSet::new(); for dir_id in dir_ids { if !parent_map.contains_key(dir_id) { continue; } let mut current = Some(*dir_id); let mut seen = HashSet::new(); let mut allowed = true; while let Some(id) = current { if !seen.insert(id) || archived.contains(&id) { allowed = false; break; } let direct = crate::share::dir_visibility::node_visible( user, &my_groups, group_ows.get(&id).map(Vec::as_slice).unwrap_or(&[]), user_ows.get(&id).map(Vec::as_slice).unwrap_or(&[]), ); if !direct { allowed = false; break; } current = parent_map.get(&id).copied().flatten(); } if allowed { visible.insert(*dir_id); } } Ok(visible) } } 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()), } } }