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:
@@ -8,4 +8,4 @@ pub mod rbac;
|
||||
pub mod types;
|
||||
|
||||
// 便捷导出
|
||||
pub use module::{ModuleContext, ModuleType};
|
||||
pub use module::{ModuleContext, ModuleType, PermissionDescriptor};
|
||||
|
||||
@@ -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<PermissionDescriptor> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Downcast support: return `self` as `&dyn Any` for concrete type access.
|
||||
///
|
||||
/// This allows the server crate to retrieve module-specific methods
|
||||
|
||||
@@ -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<chrono::Utc>,
|
||||
) -> 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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user