eeff292
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<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
}
}
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 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()),
}
}
}