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 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::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
|
||||||
|
|||||||
@@ -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 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