feat(plugin): 实现插件权限注册,install 时写入 permissions 表、uninstall 时软删除

跨 crate 方案:erp-plugin 使用 raw SQL 操作 permissions 表,
避免直接依赖 erp-auth entity,保持模块间松耦合。

- erp-core: 新增 PermissionDescriptor 类型和 ErpModule::permissions() 方法
- erp-plugin service.rs install(): 解析 manifest.permissions,INSERT ON CONFLICT DO NOTHING
- erp-plugin service.rs uninstall(): 软删除 role_permissions 关联 + permissions 记录
This commit is contained in:
iven
2026-04-16 12:42:13 +08:00
parent 92789e6713
commit a6d3a0efcc
3 changed files with 186 additions and 4 deletions

View File

@@ -8,4 +8,4 @@ pub mod rbac;
pub mod types; pub mod types;
// 便捷导出 // 便捷导出
pub use module::{ModuleContext, ModuleType}; pub use module::{ModuleContext, ModuleType, PermissionDescriptor};

View File

@@ -7,6 +7,22 @@ use uuid::Uuid;
use crate::error::{AppError, AppResult}; use crate::error::{AppError, AppResult};
use crate::events::EventBus; 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleType { pub enum ModuleType {
@@ -90,6 +106,13 @@ pub trait ErpModule: Send + Sync {
Ok(()) Ok(())
} }
/// 返回此模块需要注册的权限列表。
///
/// 默认返回空列表,有权限需求的模块(如 plugin可覆写此方法。
fn permissions(&self) -> Vec<PermissionDescriptor> {
vec![]
}
/// Downcast support: return `self` as `&dyn Any` for concrete type access. /// Downcast support: return `self` as `&dyn Any` for concrete type access.
/// ///
/// This allows the server crate to retrieve module-specific methods /// This allows the server crate to retrieve module-specific methods

View File

@@ -1,5 +1,5 @@
use chrono::Utc; 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 uuid::Uuid;
use sha2::{Sha256, Digest}; use sha2::{Sha256, Digest};
@@ -76,7 +76,7 @@ impl PluginService {
Ok(plugin_model_to_resp(&model, &manifest, vec![])) Ok(plugin_model_to_resp(&model, &manifest, vec![]))
} }
/// 安装插件: 创建动态表 + 注册 entity 记录 + 注册事件订阅 + status=installed /// 安装插件: 创建动态表 + 注册 entity 记录 + 注册事件订阅 + 注册权限 + status=installed
pub async fn install( pub async fn install(
plugin_id: Uuid, plugin_id: Uuid,
tenant_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 engine
.load( .load(
@@ -236,7 +249,7 @@ impl PluginService {
Ok(plugin_model_to_resp(&model, &manifest, entity_resps)) Ok(plugin_model_to_resp(&model, &manifest, entity_resps))
} }
/// 卸载插件: unload + 有条件地 drop 动态表 + status=uninstalled /// 卸载插件: unload + 有条件地 drop 动态表 + 清理权限 + status=uninstalled
pub async fn uninstall( pub async fn uninstall(
plugin_id: Uuid, plugin_id: Uuid,
tenant_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(); let mut active: plugin::ActiveModel = model.into();
active.status = Set("uninstalled".to_string()); active.status = Set("uninstalled".to_string());
active.updated_at = Set(now); active.updated_at = Set(now);
@@ -553,3 +569,146 @@ fn plugin_model_to_resp(
record_version: model.version, 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<chrono::Utc>,
) -> AppResult<()> {
for perm in perms {
let full_code = format!("{}.{}", plugin_manifest_id, perm.code);
// resource 使用插件 manifest idaction 使用权限的 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(())
}