a06673b
use db::conn::sea_orm::*;
use db::space::space_member_roles as member_role_entity;
use db::space::space_role_permissions as role_perm_entity;
use db::space::space_roles as role_entity;
use db::space::spaces as space_entity;
use time::OffsetDateTime;
use uuid::Uuid;
use crate::error::{SpaceError, SpaceResult};
use crate::features::authz;
use crate::features::SpaceService;
use super::types::{CreateRoleCmd, RolePage, RoleView, UpdateRoleCmd};
async fn owner_space(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
) -> SpaceResult<space_entity::Model> {
let db = svc.db.reader();
let space = space_entity::Entity::find_by_id(space_id)
.one(&db)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("space {space_id}")))?;
if space.owner_id != caller {
return Err(SpaceError::Forbidden(
"only the owner can manage roles".to_string(),
));
}
Ok(space)
}
async fn role_exists<C>(db: &C, role_id: Uuid) -> SpaceResult<()>
where
C: db::conn::sea_orm::ConnectionTrait,
{
role_entity::Entity::find_by_id(role_id)
.one(db)
.await?
.map(|_| ())
.ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))
}
async fn load_permissions<C>(db: &C, role_id: Uuid) -> SpaceResult<Vec<String>>
where
C: db::conn::sea_orm::ConnectionTrait,
{
let rows = role_perm_entity::Entity::find()
.filter(role_perm_entity::Column::RoleId.eq(role_id))
.all(db)
.await?;
Ok(rows.into_iter().map(|r| r.permission).collect())
}
async fn replace_permissions(
txn: &db::conn::sea_orm::DatabaseTransaction,
role_id: Uuid,
permissions: &[String],
) -> SpaceResult<()> {
role_perm_entity::Entity::delete_many()
.filter(role_perm_entity::Column::RoleId.eq(role_id))
.exec(txn)
.await?;
for perm in permissions {
let p: role_perm_entity::ActiveModel = role_perm_entity::ActiveModel::builder()
.set_role_id(role_id)
.set_permission(perm.clone())
.into();
p.insert(txn).await?;
}
Ok(())
}
pub async fn create_role(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
cmd: CreateRoleCmd,
) -> SpaceResult<RoleView> {
let name = cmd.name.trim().to_string();
if name.is_empty() {
return Err(SpaceError::BadRequest("role name cannot be empty".to_string()));
}
if name.chars().count() > svc.config.max_role_name_len {
return Err(SpaceError::BadRequest(format!(
"role name exceeds {} characters",
svc.config.max_role_name_len
)));
}
owner_space(svc, caller, space_id).await?;
let db = svc.db.writer();
let txn = db.begin().await?;
let existing = role_entity::Entity::find()
.filter(role_entity::Column::SpaceId.eq(space_id))
.filter(role_entity::Column::Name.eq(&name))
.one(&txn)
.await?;
if existing.is_some() {
return Err(SpaceError::Conflict(format!(
"role with name {name} already exists in space {space_id}"
)));
}
let now = OffsetDateTime::now_utc();
let role_id = Uuid::new_v4();
let color = cmd.color.clone().unwrap_or_default();
let unicode_emoji = cmd.unicode_emoji.clone();
let icon_hash = cmd.icon_hash.clone();
let position = cmd.position.unwrap_or(0);
let role: role_entity::ActiveModel = role_entity::ActiveModel::builder()
.set_id(role_id)
.set_space_id(space_id)
.set_name(name.clone())
.set_position(position)
.set_color(color.clone())
.set_unicode_emoji(unicode_emoji.clone())
.set_icon_hash(icon_hash.clone())
.set_version(1)
.set_created_at(now)
.set_updated_at(Some(now))
.set_created_by(caller)
.into();
role.insert(&txn).await?;
replace_permissions(&txn, role_id, &cmd.permissions).await?;
txn.commit().await?;
let mut view = RoleView::from(role_entity::Model {
id: role_id,
space_id,
name,
position,
color,
unicode_emoji,
icon_hash,
version: 1,
created_at: now,
updated_at: Some(now),
created_by: caller,
});
view.permissions = cmd.permissions;
Ok(view)
}
pub async fn get_role(
svc: &SpaceService,
caller: Option<Uuid>,
space_id: Uuid,
role_id: Uuid,
) -> SpaceResult<RoleView> {
let db = svc.db.reader();
let role = role_entity::Entity::find_by_id(role_id)
.one(&db)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?;
if role.space_id != space_id {
return Err(SpaceError::NotFound(format!("role {role_id}")));
}
let space = space_entity::Entity::find_by_id(space_id)
.one(&db)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("space {space_id}")))?;
if !space.is_active {
return Err(SpaceError::NotFound(format!("role {role_id}")));
}
if !space.public {
let caller = caller.ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?;
match authz::require_perm(svc, caller, space_id, authz::MANAGE_ROLES).await {
Ok(_) => {}
Err(SpaceError::Forbidden(_)) => {
return Err(SpaceError::NotFound(format!("role {role_id}")));
}
Err(e) => return Err(e),
}
}
let perms = load_permissions(&db, role_id).await?;
let mut view = RoleView::from(role);
view.permissions = perms;
Ok(view)
}
pub async fn update_role(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
role_id: Uuid,
cmd: UpdateRoleCmd,
) -> SpaceResult<RoleView> {
owner_space(svc, caller, space_id).await?;
let db = svc.db.writer();
let txn = db.begin().await?;
let role = role_entity::Entity::find_by_id(role_id)
.one(&txn)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("role {role_id}")))?;
if role.space_id != space_id {
return Err(SpaceError::NotFound(format!("role {role_id}")));
}
let mut active: role_entity::ActiveModel = role.into();
if let Some(name) = cmd.name {
let name = name.trim().to_string();
if name.is_empty() {
return Err(SpaceError::BadRequest("role name cannot be empty".to_string()));
}
if name.chars().count() > svc.config.max_role_name_len {
return Err(SpaceError::BadRequest(format!(
"role name exceeds {} characters",
svc.config.max_role_name_len
)));
}
let dup = role_entity::Entity::find()
.filter(role_entity::Column::SpaceId.eq(space_id))
.filter(role_entity::Column::Name.eq(&name))
.filter(role_entity::Column::Id.ne(role_id))
.one(&txn)
.await?;
if dup.is_some() {
return Err(SpaceError::Conflict(format!(
"role with name {name} already exists in space {space_id}"
)));
}
active.name = Set(name);
}
if let Some(color) = cmd.color {
active.color = Set(color);
}
if let Some(v) = cmd.unicode_emoji {
active.unicode_emoji = Set(v);
}
if let Some(v) = cmd.icon_hash {
active.icon_hash = Set(v);
}
if let Some(position) = cmd.position {
active.position = Set(position);
}
active.version = Set(active.version.take().map_or(1, |v| v + 1));
active.updated_at = Set(Some(OffsetDateTime::now_utc()));
let saved = active.update(&txn).await?;
if let Some(perms) = cmd.permissions {
replace_permissions(&txn, role_id, &perms).await?;
}
txn.commit().await?;
let perms = load_permissions(&svc.db.reader(), role_id).await?;
let mut view = RoleView::from(saved);
view.permissions = perms;
Ok(view)
}
pub async fn delete_role(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
role_id: Uuid,
) -> SpaceResult<()> {
owner_space(svc, caller, space_id).await?;
let db = svc.db.writer();
let txn = db.begin().await?;
role_exists(&txn, role_id).await?;
member_role_entity::Entity::delete_many()
.filter(member_role_entity::Column::RoleId.eq(role_id))
.exec(&txn)
.await?;
role_perm_entity::Entity::delete_many()
.filter(role_perm_entity::Column::RoleId.eq(role_id))
.exec(&txn)
.await?;
role_entity::Entity::delete_by_id(role_id).exec(&txn).await?;
txn.commit().await?;
Ok(())
}
pub async fn list_roles(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
limit: u64,
offset: u64,
) -> SpaceResult<RolePage> {
authz::require_perm(svc, caller, space_id, authz::MANAGE_ROLES).await?;
let db = svc.db.reader();
let limit = std::cmp::min(limit, svc.config.max_pagination);
let roles = role_entity::Entity::find()
.filter(role_entity::Column::SpaceId.eq(space_id))
.order_by_asc(role_entity::Column::Position)
.limit(Some(limit))
.offset(Some(offset))
.all(&db)
.await?;
let total = role_entity::Entity::find()
.filter(role_entity::Column::SpaceId.eq(space_id))
.count(&db)
.await?;
let mut views = Vec::with_capacity(roles.len());
for role in roles {
let perms = load_permissions(&db, role.id).await?;
let mut view = RoleView::from(role);
view.permissions = perms;
views.push(view);
}
Ok(RolePage {
items: views,
total: total as i64,
})
}
pub async fn assign_role(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
user_id: Uuid,
role_id: Uuid,
) -> SpaceResult<()> {
owner_space(svc, caller, space_id).await?;
let db = svc.db.writer();
let txn = db.begin().await?;
role_exists(&txn, role_id).await?;
let mr: member_role_entity::ActiveModel = member_role_entity::ActiveModel::builder()
.set_space_id(space_id)
.set_user_id(user_id)
.set_role_id(role_id)
.set_created_at(OffsetDateTime::now_utc())
.into();
mr.insert(&txn).await?;
txn.commit().await?;
Ok(())
}
pub async fn unassign_role(
svc: &SpaceService,
caller: Uuid,
_space_id: Uuid,
user_id: Uuid,
role_id: Uuid,
) -> SpaceResult<()> {
owner_space(svc, caller, _space_id).await?;
let db = svc.db.writer();
let txn = db.begin().await?;
member_role_entity::Entity::delete_many()
.filter(member_role_entity::Column::UserId.eq(user_id))
.filter(member_role_entity::Column::RoleId.eq(role_id))
.exec(&txn)
.await?;
txn.commit().await?;
Ok(())
}