diff --git a/crates/erp-core/src/lib.rs b/crates/erp-core/src/lib.rs index b4c2a50..0900174 100644 --- a/crates/erp-core/src/lib.rs +++ b/crates/erp-core/src/lib.rs @@ -8,4 +8,4 @@ pub mod rbac; pub mod types; // 便捷导出 -pub use module::{ModuleContext, ModuleType}; +pub use module::{ModuleContext, ModuleType, PermissionDescriptor}; diff --git a/crates/erp-core/src/module.rs b/crates/erp-core/src/module.rs index 98239e7..15e9241 100644 --- a/crates/erp-core/src/module.rs +++ b/crates/erp-core/src/module.rs @@ -7,6 +7,22 @@ use uuid::Uuid; use crate::error::{AppError, AppResult}; use crate::events::EventBus; +/// 权限描述符,用于模块声明自己需要的权限。 +/// +/// 各业务模块通过 `ErpModule::permissions()` 返回此列表, +/// 由 erp-server 在启动时统一注册到权限表。 +#[derive(Clone, Debug)] +pub struct PermissionDescriptor { + /// 权限编码,全局唯一,格式建议 `{模块}.{动作}` 如 `plugin.admin` + pub code: String, + /// 权限显示名称 + pub name: String, + /// 权限描述 + pub description: String, + /// 所属模块名称 + pub module: String, +} + /// 模块类型 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ModuleType { @@ -90,6 +106,13 @@ pub trait ErpModule: Send + Sync { Ok(()) } + /// 返回此模块需要注册的权限列表。 + /// + /// 默认返回空列表,有权限需求的模块(如 plugin)可覆写此方法。 + fn permissions(&self) -> Vec { + vec![] + } + /// Downcast support: return `self` as `&dyn Any` for concrete type access. /// /// This allows the server crate to retrieve module-specific methods diff --git a/crates/erp-plugin/src/service.rs b/crates/erp-plugin/src/service.rs index eb4c8ee..ee00b34 100644 --- a/crates/erp-plugin/src/service.rs +++ b/crates/erp-plugin/src/service.rs @@ -1,5 +1,5 @@ use chrono::Utc; -use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; +use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, Set}; use uuid::Uuid; use sha2::{Sha256, Digest}; @@ -76,7 +76,7 @@ impl PluginService { Ok(plugin_model_to_resp(&model, &manifest, vec![])) } - /// 安装插件: 创建动态表 + 注册 entity 记录 + 注册事件订阅 + status=installed + /// 安装插件: 创建动态表 + 注册 entity 记录 + 注册事件订阅 + 注册权限 + status=installed pub async fn install( plugin_id: Uuid, tenant_id: Uuid, @@ -142,6 +142,19 @@ impl PluginService { } } + // 注册插件声明的权限到 permissions 表 + if let Some(perms) = &manifest.permissions { + register_plugin_permissions( + db, + tenant_id, + operator_id, + &manifest.metadata.id, + perms, + &now, + ) + .await?; + } + // 加载到内存 engine .load( @@ -236,7 +249,7 @@ impl PluginService { Ok(plugin_model_to_resp(&model, &manifest, entity_resps)) } - /// 卸载插件: unload + 有条件地 drop 动态表 + status=uninstalled + /// 卸载插件: unload + 有条件地 drop 动态表 + 清理权限 + status=uninstalled pub async fn uninstall( plugin_id: Uuid, tenant_id: Uuid, @@ -294,6 +307,9 @@ impl PluginService { } } + // 清理此插件注册的权限 + unregister_plugin_permissions(db, tenant_id, &manifest.metadata.id).await?; + let mut active: plugin::ActiveModel = model.into(); active.status = Set("uninstalled".to_string()); active.updated_at = Set(now); @@ -553,3 +569,146 @@ fn plugin_model_to_resp( record_version: model.version, } } + +/// 将插件声明的权限注册到 permissions 表。 +/// +/// 使用 raw SQL 避免依赖 erp-auth 的 entity 类型。 +/// 权限码格式:`{plugin_manifest_id}.{code}`(如 `erp-crm.customer.list`)。 +/// 使用 `ON CONFLICT DO NOTHING` 保证幂等。 +async fn register_plugin_permissions( + db: &sea_orm::DatabaseConnection, + tenant_id: Uuid, + operator_id: Uuid, + plugin_manifest_id: &str, + perms: &[crate::manifest::PluginPermission], + now: &chrono::DateTime, +) -> AppResult<()> { + for perm in perms { + let full_code = format!("{}.{}", plugin_manifest_id, perm.code); + // resource 使用插件 manifest id,action 使用权限的 code 字段 + let resource = plugin_manifest_id.to_string(); + let action = perm.code.clone(); + let description_sql = if perm.description.is_empty() { + "NULL".to_string() + } else { + format!("'{}'", perm.description.replace('\'', "''")) + }; + + let sql = format!( + r#" + INSERT INTO permissions (id, tenant_id, code, name, resource, action, description, created_at, updated_at, created_by, updated_by, deleted_at, version) + VALUES (gen_random_uuid(), '{tenant_id}', '{full_code}', '{name}', '{resource}', '{action}', {desc}, '{now}', '{now}', '{operator_id}', '{operator_id}', NULL, 1) + ON CONFLICT (tenant_id, code) WHERE deleted_at IS NULL DO NOTHING + "#, + tenant_id = tenant_id, + full_code = full_code.replace('\'', "''"), + name = perm.name.replace('\'', "''"), + resource = resource.replace('\'', "''"), + action = action.replace('\'', "''"), + desc = description_sql, + now = now.to_rfc3339(), + operator_id = operator_id, + ); + + db.execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + sql, + )) + .await + .map_err(|e| { + tracing::error!( + plugin = plugin_manifest_id, + permission = %full_code, + error = %e, + "注册插件权限失败" + ); + PluginError::DatabaseError(format!("注册插件权限 {} 失败: {}", full_code, e)) + })?; + } + + tracing::info!( + plugin = plugin_manifest_id, + count = perms.len(), + tenant_id = %tenant_id, + "插件权限注册完成" + ); + Ok(()) +} + +/// 清理插件注册的权限(软删除)。 +/// +/// 使用 raw SQL 按前缀匹配清理:`{plugin_manifest_id}.%`。 +/// 同时清理 role_permissions 中对这些权限的关联。 +async fn unregister_plugin_permissions( + db: &sea_orm::DatabaseConnection, + tenant_id: Uuid, + plugin_manifest_id: &str, +) -> AppResult<()> { + let prefix = format!("{}.%", plugin_manifest_id); + let now = chrono::Utc::now().to_rfc3339(); + + // 先软删除 role_permissions 中的关联 + let rp_sql = format!( + r#" + UPDATE role_permissions + SET deleted_at = '{now}', updated_at = '{now}' + WHERE permission_id IN ( + SELECT id FROM permissions + WHERE tenant_id = '{tenant_id}' + AND code LIKE '{prefix}' + AND deleted_at IS NULL + ) + AND deleted_at IS NULL + "#, + now = now, + tenant_id = tenant_id, + prefix = prefix.replace('\'', "''"), + ); + db.execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + rp_sql, + )) + .await + .map_err(|e| { + tracing::error!( + plugin = plugin_manifest_id, + error = %e, + "清理插件权限角色关联失败" + ); + PluginError::DatabaseError(format!("清理插件权限角色关联失败: {}", e)) + })?; + + // 再软删除 permissions + let perm_sql = format!( + r#" + UPDATE permissions + SET deleted_at = '{now}', updated_at = '{now}' + WHERE tenant_id = '{tenant_id}' + AND code LIKE '{prefix}' + AND deleted_at IS NULL + "#, + now = now, + tenant_id = tenant_id, + prefix = prefix.replace('\'', "''"), + ); + db.execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + perm_sql, + )) + .await + .map_err(|e| { + tracing::error!( + plugin = plugin_manifest_id, + error = %e, + "清理插件权限失败" + ); + PluginError::DatabaseError(format!("清理插件权限失败: {}", e)) + })?; + + tracing::info!( + plugin = plugin_manifest_id, + tenant_id = %tenant_id, + "插件权限清理完成" + ); + Ok(()) +}