use sea_orm::entity::Set; use sea_orm::sea_query::{Expr, ExprTrait}; use sea_orm::{ ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait, }; use uuid::Uuid; use crate::errors::{SpaceError, SpaceResult}; use crate::services::service::{ SpaceService, bump_version, normalize_role_key, normalize_role_name, }; use crate::services::types::{ CreateRoleBody, MemberSummary, PermissionGrant, PermissionGrant as Grant, RoleSummary, SYSTEM_ROLE_ADMIN, SYSTEM_ROLE_MEMBER, SYSTEM_ROLE_OWNER, SetRolePermissionsBody, is_known_permission, }; use crate::services::{error, member}; use events::label; use models::space::{PermissionEffect, space_member_roles, space_role_permissions, space_roles}; /// 内置角色的固有授权。OWNER 不需要显式授权(解析链直接短路), /// ADMIN 的固有集写在 permissions.rs 的白名单里,这里只落 MEMBER 的默认值。 const MEMBER_DEFAULTS: &[&str] = &[ "org.view", "member.view", "role.view", "space.view_space", "space.read", "space.message", ]; pub(crate) async fn seed_system_roles( db: &C, organization_id: Uuid, ) -> SpaceResult { let owner = space_roles::ActiveModel { organization_id: Set(organization_id), key: Set(SYSTEM_ROLE_OWNER.to_owned()), name: Set("Owner".to_owned()), description: Set(Some("full control of the organization".to_owned())), rank: Set(100), system: Set(true), default_role: Set(false), ..Default::default() } .insert(db) .await?; let admin = space_roles::ActiveModel { organization_id: Set(organization_id), key: Set(SYSTEM_ROLE_ADMIN.to_owned()), name: Set("Admin".to_owned()), description: Set(Some("manages members, roles and invites".to_owned())), rank: Set(50), system: Set(true), default_role: Set(false), ..Default::default() } .insert(db) .await?; let member = space_roles::ActiveModel { organization_id: Set(organization_id), key: Set(SYSTEM_ROLE_MEMBER.to_owned()), name: Set("Member".to_owned()), description: Set(Some("default role for every new member".to_owned())), rank: Set(0), system: Set(true), default_role: Set(true), ..Default::default() } .insert(db) .await?; for permission in MEMBER_DEFAULTS { space_role_permissions::ActiveModel { role_id: Set(member.id), permission: Set((*permission).to_owned()), effect: Set(PermissionEffect::Allow), ..Default::default() } .insert(db) .await?; } let _ = admin; Ok(owner.id) } pub(crate) async fn find_by_key( organization_id: Uuid, key: &str, db: &C, ) -> SpaceResult> { Ok(space_roles::Entity::find() .filter(space_roles::Column::OrganizationId.eq(organization_id)) .filter(space_roles::Column::Key.eq(key)) .one(db) .await?) } pub(crate) async fn keys_of_ids( db: &C, role_ids: &[Uuid], ) -> SpaceResult> { if role_ids.is_empty() { return Ok(Vec::new()); } Ok(space_roles::Entity::find() .select_only() .column(space_roles::Column::Key) .filter(space_roles::Column::Id.is_in(role_ids.to_vec())) .order_by_asc(space_roles::Column::Rank) .into_tuple::() .all(db) .await?) } pub async fn role_keys_of(db: &C, member_id: Uuid) -> SpaceResult> { let ids = space_member_roles::Entity::find() .select_only() .column(space_member_roles::Column::RoleId) .filter(space_member_roles::Column::MemberId.eq(member_id)) .into_tuple::() .all(db) .await?; keys_of_ids(db, &ids).await } pub(crate) async fn grants_of( db: &C, role_id: Uuid, ) -> SpaceResult> { let rows = space_role_permissions::Entity::find() .filter(space_role_permissions::Column::RoleId.eq(role_id)) .order_by_asc(space_role_permissions::Column::Permission) .all(db) .await?; Ok(rows .into_iter() .map(|row| Grant { permission: row.permission, effect: row.effect, }) .collect()) } /// 绑定幂等:唯一键冲突直接吞掉,重复授予不该报错。 pub(crate) async fn bind_role( db: &C, member_id: Uuid, role_id: Uuid, granted_by: i64, ) -> SpaceResult<()> { let existing = space_member_roles::Entity::find() .filter(space_member_roles::Column::MemberId.eq(member_id)) .filter(space_member_roles::Column::RoleId.eq(role_id)) .one(db) .await?; if existing.is_some() { return Ok(()); } space_member_roles::ActiveModel { member_id: Set(member_id), role_id: Set(role_id), granted_by: Set(Some(granted_by)), ..Default::default() } .insert(db) .await?; Ok(()) } pub(crate) async fn unbind_role( db: &C, member_id: Uuid, role_id: Uuid, ) -> SpaceResult { let result = space_member_roles::Entity::delete_many() .filter(space_member_roles::Column::MemberId.eq(member_id)) .filter(space_member_roles::Column::RoleId.eq(role_id)) .exec(db) .await?; Ok(result.rows_affected) } pub(crate) async fn clear_bindings(db: &C, member_id: Uuid) -> SpaceResult<()> { space_member_roles::Entity::delete_many() .filter(space_member_roles::Column::MemberId.eq(member_id)) .exec(db) .await?; Ok(()) } pub(crate) async fn load_keys( organization_id: Uuid, keys: &[String], db: &C, ) -> SpaceResult> { if keys.is_empty() { return Ok(Vec::new()); } let mut out = Vec::with_capacity(keys.len()); for key in keys { let row = find_by_key(organization_id, key.trim(), db) .await? .ok_or(SpaceError::SpaceRoleNotFound)?; if row.key == SYSTEM_ROLE_OWNER { return Err(SpaceError::Forbidden); } out.push(row.id); } Ok(out) } async fn replace_grants( db: &C, role_id: Uuid, org_version_holder: &Uuid, grants: &[PermissionGrant], ) -> SpaceResult<()> { space_role_permissions::Entity::delete_many() .filter(space_role_permissions::Column::RoleId.eq(role_id)) .exec(db) .await?; let _ = org_version_holder; for grant in grants { space_role_permissions::ActiveModel { role_id: Set(role_id), permission: Set(grant.permission.clone()), effect: Set(grant.effect), ..Default::default() } .insert(db) .await?; } Ok(()) } impl SpaceService { #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn list_roles( &self, actor_user_id: i64, organization_id: Uuid, request_id: Option<&str>, ) -> SpaceResult> { self.require_permission(actor_user_id, organization_id, "role.view", request_id) .await?; let rows = space_roles::Entity::find() .filter(space_roles::Column::OrganizationId.eq(organization_id)) .order_by_desc(space_roles::Column::Rank) .order_by_asc(space_roles::Column::Key) .all(self.access.database.read()) .await?; let mut out = Vec::with_capacity(rows.len()); for row in rows { out.push(self.role_summary(&row).await?); } Ok(out) } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn create_role( &self, actor_user_id: i64, organization_id: Uuid, body: CreateRoleBody, request_id: Option<&str>, ) -> SpaceResult { self.require_permission(actor_user_id, organization_id, "role.create", request_id) .await?; let key = normalize_role_key(&body.key)?; let name = normalize_role_name(&body.name)?; let grants = Self::validate_grants(&body.permissions)?; self.require_grantable(actor_user_id, organization_id, &grants, request_id) .await?; if find_by_key(organization_id, &key, self.access.database.read()) .await? .is_some() { return Err(SpaceError::SpaceRoleKeyTaken); } let txn = self.access.database.write().begin().await?; let role = space_roles::ActiveModel { organization_id: Set(organization_id), key: Set(key.clone()), name: Set(name.clone()), description: Set(body.description.clone()), rank: Set(body.rank), system: Set(false), default_role: Set(body.default_role), self_assignable: Set(body.self_assignable), ..Default::default() } .insert(&txn) .await .map_err(error::map_insert_conflict)?; replace_grants(&txn, role.id, &organization_id, &grants).await?; let version = bump_version(&txn, organization_id).await?; let refreshed = load_role(&txn, organization_id, role.id).await?; self.emit_role( &txn, organization_id, actor_user_id, serde_json::json!({ "id": role.id, "key": &key, "action": "create", "request_id": request_id, }), request_id, ) .await?; txn.commit().await?; self.invalidate_org(organization_id).await?; let mut summary = self.role_summary(&refreshed).await?; summary.version = version; Ok(summary) } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, role_id = %role_id, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn set_role_permissions( &self, actor_user_id: i64, organization_id: Uuid, role_id: Uuid, body: SetRolePermissionsBody, request_id: Option<&str>, ) -> SpaceResult { self.require_permission( actor_user_id, organization_id, "role.permission.update", request_id, ) .await?; let role = load_role(self.access.database.read(), organization_id, role_id).await?; if role.key == SYSTEM_ROLE_OWNER { return Err(SpaceError::SpaceRoleSystem); } let grants = Self::validate_grants(&body.permissions)?; self.require_grantable(actor_user_id, organization_id, &grants, request_id) .await?; let txn = self.access.database.write().begin().await?; replace_grants(&txn, role_id, &organization_id, &grants).await?; space_roles::Entity::update_many() .col_expr( space_roles::Column::Version, Expr::col(space_roles::Column::Version).add(1), ) .col_expr( space_roles::Column::UpdatedAt, Expr::value(chrono::Utc::now()), ) .filter(space_roles::Column::Id.eq(role_id)) .exec(&txn) .await?; let version = bump_version(&txn, organization_id).await?; let refreshed = load_role(&txn, organization_id, role_id).await?; self.emit_role( &txn, organization_id, actor_user_id, serde_json::json!({ "id": role_id, "key": &refreshed.key, "action": "set_permissions", "permissions": body.permissions.iter().map(|g| &g.permission).collect::>(), "request_id": request_id, }), request_id, ) .await?; txn.commit().await?; self.invalidate_org(organization_id).await?; let mut summary = self.role_summary(&refreshed).await?; summary.version = version; Ok(summary) } #[allow(clippy::too_many_arguments)] #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, role_id = %role_id, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn update_role( &self, actor_user_id: i64, organization_id: Uuid, role_id: Uuid, name: Option, description: Option, rank: Option, default_role: Option, self_assignable: Option, expected_version: i32, request_id: Option<&str>, ) -> SpaceResult { self.require_permission(actor_user_id, organization_id, "role.update", request_id) .await?; let role = load_role(self.access.database.read(), organization_id, role_id).await?; let name = name.as_deref().map(normalize_role_name).transpose()?; if expected_version > 0 && role.version != expected_version { return Err(SpaceError::VersionMismatch { current: role.version, }); } if role.key == SYSTEM_ROLE_OWNER && (rank.is_some() || default_role.is_some() || self_assignable.is_some()) { return Err(SpaceError::SpaceRoleSystem); } // 系统角色的排名、默认身份、自助加入都是结构属性,任何改法都会改变权限基线。 if role.system && (rank.is_some() || default_role.is_some() || self_assignable.is_some()) { return Err(SpaceError::SpaceRoleSystem); } let txn = self.access.database.write().begin().await?; if let Some(true) = default_role { // 默认角色唯一:换默认之前先清掉旧的,否则新成员会被绑两个默认角色。 space_roles::Entity::update_many() .col_expr(space_roles::Column::DefaultRole, Expr::value(false)) .filter(space_roles::Column::OrganizationId.eq(organization_id)) .exec(&txn) .await?; } let mut active: space_roles::ActiveModel = role.clone().into(); if let Some(value) = name { active.name = Set(value); } if let Some(value) = description { active.description = Set(if value.trim().is_empty() { None } else { Some(value) }); } if let Some(value) = rank { active.rank = Set(value); } if let Some(value) = default_role { active.default_role = Set(value); } if let Some(value) = self_assignable { if role.system { return Err(SpaceError::SpaceRoleSystem); } active.self_assignable = Set(value); } active.update(&txn).await?; let version = bump_version(&txn, organization_id).await?; let refreshed = load_role(&txn, organization_id, role_id).await?; self.emit_role( &txn, organization_id, actor_user_id, serde_json::json!({ "id": role_id, "key": &role.key, "action": "update", "request_id": request_id, }), request_id, ) .await?; txn.commit().await?; self.invalidate_org(organization_id).await?; let mut summary = self.role_summary(&refreshed).await?; summary.version = version; Ok(summary) } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, role_id = %role_id, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn delete_role( &self, actor_user_id: i64, organization_id: Uuid, role_id: Uuid, request_id: Option<&str>, ) -> SpaceResult { self.require_permission(actor_user_id, organization_id, "role.delete", request_id) .await?; let role = load_role(self.access.database.read(), organization_id, role_id).await?; if role.system { return Err(SpaceError::SpaceRoleSystem); } let bound = space_member_roles::Entity::find() .filter(space_member_roles::Column::RoleId.eq(role_id)) .count(self.access.database.read()) .await?; if bound > 0 { return Err(SpaceError::SpaceRoleInUse); } let txn = self.access.database.write().begin().await?; space_role_permissions::Entity::delete_many() .filter(space_role_permissions::Column::RoleId.eq(role_id)) .exec(&txn) .await?; space_roles::Entity::delete_many() .filter(space_roles::Column::Id.eq(role_id)) .exec(&txn) .await?; let version = bump_version(&txn, organization_id).await?; self.emit_role( &txn, organization_id, actor_user_id, serde_json::json!({ "id": role_id, "key": &role.key, "action": "delete", "version": version, "request_id": request_id, }), request_id, ) .await?; txn.commit().await?; self.invalidate_org(organization_id).await?; Ok(true) } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, member_id = %member_id, role_key = %role_key, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn assign_role( &self, actor_user_id: i64, organization_id: Uuid, member_id: Uuid, role_key: &str, request_id: Option<&str>, ) -> SpaceResult> { let member = crate::services::member::load_by_id( self.access.database.read(), organization_id, member_id, ) .await?; if member.banned_at.is_some() || member.left_at.is_some() { return Err(SpaceError::SpaceMemberNotFound); } let role = find_by_key( organization_id, role_key.trim(), self.access.database.read(), ) .await? .ok_or(SpaceError::SpaceRoleNotFound)?; if role.key == SYSTEM_ROLE_OWNER { return Err(SpaceError::Forbidden); } if actor_user_id == member.user_id { if role.system || !role.self_assignable { return Err(SpaceError::Forbidden); } } else { self.require_permission(actor_user_id, organization_id, "role.assign", request_id) .await?; // 系统角色等同于整套权限包:代绑时必须具备其中的每一条权限。 if role.system { let grants = grants_of(self.access.database.read(), role.id).await?; self.require_grantable(actor_user_id, organization_id, &grants, request_id) .await?; } } let txn = self.access.database.write().begin().await?; bind_role(&txn, member_id, role.id, actor_user_id).await?; let version = bump_version(&txn, organization_id).await?; let keys = role_keys_of(&txn, member_id).await?; self.emit_role( &txn, organization_id, actor_user_id, serde_json::json!({ "member_id": member_id, "role_key": &role.key, "action": "assign", "version": version, "request_id": request_id, }), request_id, ) .await?; txn.commit().await?; self.invalidate_org(organization_id).await?; Ok(keys) } #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, member_id = %member_id, role_key = %role_key, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn unassign_role( &self, actor_user_id: i64, organization_id: Uuid, member_id: Uuid, role_key: &str, request_id: Option<&str>, ) -> SpaceResult> { let role = find_by_key( organization_id, role_key.trim(), self.access.database.read(), ) .await? .ok_or(SpaceError::SpaceRoleNotFound)?; if role.key == SYSTEM_ROLE_OWNER { return Err(SpaceError::SpaceRoleSystem); } let member = crate::services::member::load_by_id( self.access.database.read(), organization_id, member_id, ) .await?; if actor_user_id == member.user_id { if !role.self_assignable { return Err(SpaceError::Forbidden); } } else { // 解绑系统角色必须与绑定同权:否则 Admin 绑定后没人能收回。 self.require_permission(actor_user_id, organization_id, "role.assign", request_id) .await?; if role.system { let grants = grants_of(self.access.database.read(), role.id).await?; self.require_grantable(actor_user_id, organization_id, &grants, request_id) .await?; } } let txn = self.access.database.write().begin().await?; unbind_role(&txn, member_id, role.id).await?; let version = bump_version(&txn, organization_id).await?; let keys = role_keys_of(&txn, member_id).await?; self.emit_role( &txn, organization_id, actor_user_id, serde_json::json!({ "member_id": member.id, "role_key": &role.key, "action": "unassign", "version": version, "request_id": request_id, }), request_id, ) .await?; txn.commit().await?; self.invalidate_org(organization_id).await?; Ok(keys) } /// 成员直挂权限:整表替换语义。`permission` 必须在白名单内,否则直接拒绝。 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, member_id = %member_id, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn set_member_permissions( &self, actor_user_id: i64, organization_id: Uuid, member_id: Uuid, grants: Vec, request_id: Option<&str>, ) -> SpaceResult> { self.require_permission( actor_user_id, organization_id, "role.permission.update", request_id, ) .await?; let member = crate::services::member::load_by_id( self.access.database.read(), organization_id, member_id, ) .await?; if member.role == models::space::SpaceMemberRole::Owner { return Err(SpaceError::SpaceOwnerRequired); } let grants = Self::validate_grants(&grants)?; self.require_grantable(actor_user_id, organization_id, &grants, request_id) .await?; let txn = self.access.database.write().begin().await?; models::space::space_member_permissions::Entity::delete_many() .filter(models::space::space_member_permissions::Column::MemberId.eq(member_id)) .exec(&txn) .await?; for grant in &grants { models::space::space_member_permissions::ActiveModel { member_id: Set(member_id), permission: Set(grant.permission.clone()), effect: Set(grant.effect), granted_by: Set(Some(actor_user_id)), ..Default::default() } .insert(&txn) .await?; } let version = bump_version(&txn, organization_id).await?; self.emit_role( &txn, organization_id, actor_user_id, serde_json::json!({ "member_id": member_id, "granted": grants.len(), "version": version, "request_id": request_id, }), request_id, ) .await?; txn.commit().await?; self.invalidate_org(organization_id).await?; self.member_permissions(member_id).await } pub(crate) async fn member_permissions(&self, member_id: Uuid) -> SpaceResult> { let rows = models::space::space_member_permissions::Entity::find() .filter(models::space::space_member_permissions::Column::MemberId.eq(member_id)) .order_by_asc(models::space::space_member_permissions::Column::Permission) .all(self.access.database.read()) .await?; Ok(rows .into_iter() .filter(|row| is_known_permission(&row.permission)) .map(|row| row.permission) .collect()) } /// 角色事件与业务写入同事务入队:提交时一起可见,回滚时一起消失。 pub(crate) async fn emit_role( &self, txn: &sea_orm::DatabaseTransaction, organization_id: Uuid, actor_user_id: i64, payload: serde_json::Value, request_id: Option<&str>, ) -> SpaceResult<()> { let labels = events::Labels::new() .with(label::ORGANIZATION_ID, organization_id) .with(label::ACTOR_USER_ID, actor_user_id); self.emit_tx( txn, label::ORGANIZATION_ROLE_UPDATE, labels, payload, request_id, ) .await } /// 单个角色详情。 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, role_id = %role_id, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn get_role( &self, actor_user_id: i64, organization_id: Uuid, role_id: Uuid, request_id: Option<&str>, ) -> SpaceResult { self.require_permission(actor_user_id, organization_id, "role.view", request_id) .await?; let row = load_role(self.access.database.read(), organization_id, role_id).await?; self.role_summary(&row).await } /// 自助加入:只接受 `self_assignable = true` 的非内置角色。 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, role_id = %role_id, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn join_role( &self, actor_user_id: i64, organization_id: Uuid, role_id: Uuid, request_id: Option<&str>, ) -> SpaceResult { let role = load_role(self.access.database.read(), organization_id, role_id).await?; if role.system || !role.self_assignable { return Err(SpaceError::Forbidden); } let row = member::find_active(self.access.database.read(), organization_id, actor_user_id) .await? .ok_or(SpaceError::Forbidden)?; let txn = self.access.database.write().begin().await?; bind_role(&txn, row.id, role_id, actor_user_id).await?; let version = bump_version(&txn, organization_id).await?; let refreshed = member::load_by_id(&txn, organization_id, row.id).await?; self.emit_member( &txn, organization_id, actor_user_id, serde_json::json!({ "id": row.id, "user_id": actor_user_id, "action": "join_role", "role_id": role_id, "request_id": request_id, }), request_id, ) .await?; txn.commit().await?; self.invalidate_org(organization_id).await?; let mut summary = self.member_summary(&refreshed).await?; summary.version = version; Ok(summary) } /// 自助退出:同样只接受自助角色;重复退出视为成功。 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, role_id = %role_id, request_id = %envelope::context::resolve_request_id(request_id)))] pub async fn leave_role( &self, actor_user_id: i64, organization_id: Uuid, role_id: Uuid, request_id: Option<&str>, ) -> SpaceResult { let role = load_role(self.access.database.read(), organization_id, role_id).await?; if role.system || !role.self_assignable { return Err(SpaceError::Forbidden); } let row = member::find_active(self.access.database.read(), organization_id, actor_user_id) .await? .ok_or(SpaceError::Forbidden)?; let txn = self.access.database.write().begin().await?; unbind_role(&txn, row.id, role_id).await?; let version = bump_version(&txn, organization_id).await?; let refreshed = member::load_by_id(&txn, organization_id, row.id).await?; self.emit_member( &txn, organization_id, actor_user_id, serde_json::json!({ "id": row.id, "user_id": actor_user_id, "action": "leave_role", "role_id": role_id, "request_id": request_id, }), request_id, ) .await?; txn.commit().await?; self.invalidate_org(organization_id).await?; let mut summary = self.member_summary(&refreshed).await?; summary.version = version; Ok(summary) } } async fn load_role( db: &C, organization_id: Uuid, role_id: Uuid, ) -> SpaceResult { space_roles::Entity::find_by_id(role_id) .filter(space_roles::Column::OrganizationId.eq(organization_id)) .one(db) .await? .ok_or(SpaceError::SpaceRoleNotFound) }