8f30db6
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<Space, AccessError> {
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<Space, AccessError> {
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<Vec<Space>, 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<Uuid> = 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<Option<Member>, AccessError> {
self.by_id(space).await?.member(user).await
}
pub async fn members(&self, space: Uuid, actor: i64) -> Result<Vec<Member>, AccessError> {
self.by_id(space).await?.members(actor).await
}
pub async fn space_id_for_dir(&self, mut id: Uuid) -> Result<Option<Uuid>, 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<Option<Space>, 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<Option<String>, 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<i64, AccessError> {
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<Option<Member>, 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<Member, AccessError> {
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<Vec<Member>, 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<Option<Uuid>, 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<Vec<String>, 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<bool, AccessError> {
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<bool, AccessError> {
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<bool, AccessError> {
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 visible_dirs(
&self,
user: i64,
dir_ids: &[Uuid],
) -> Result<HashSet<Uuid>, 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<Uuid, Option<Uuid>> = HashMap::new();
let mut archived: HashSet<Uuid> = 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<Uuid> = nodes.iter().map(|node| node.id).collect();
let my_groups: HashSet<Uuid> = 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<Uuid, Vec<models::dir::dir_overwrite_group::Model>> =
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<Uuid, Vec<models::dir::dir_overwrite_users::Model>> =
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<String>,
pub alias_avatar_url: Option<String>,
pub message_notifications: crate::MessageNotifications,
pub muted: bool,
pub mute_until: Option<OffsetDateTime>,
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<space_members::Model> 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<crate::errors::SpaceError> 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()),
}
}
}