fix(auth): standardize permission codes to dot notation and add missing permissions

- Change all permission codes from colon (`:`) to dot (`.`) separator
  to match handler require_permission() calls consistently
- Add missing user.list, role.list, permission.list, organization.list,
  department.list, position.list permissions (handlers check for .list
  but seeds only had :read)
- Add missing message module permissions (message.list, message.send,
  message.template.list, message.template.create)
- Add missing setting.delete, numbering.delete permissions
- Fix workflow handlers: workflow: → workflow.
- Fix message handlers: message: → message.
- Update viewer role READ_PERM_INDICES for new permission list

This fixes a critical runtime bug where ALL permission checks in
erp-auth and erp-config handlers would return 403 Forbidden because
the seed data used colon separators but handlers checked for dots.
This commit is contained in:
iven
2026-04-11 14:30:47 +08:00
parent f29f6d76ee
commit 0d7d3af0a8
6 changed files with 99 additions and 139 deletions

View File

@@ -8,132 +8,94 @@ use super::password;
/// Permission definitions to seed for every new tenant. /// Permission definitions to seed for every new tenant.
/// Each tuple is: (code, name, resource, action, description) /// Each tuple is: (code, name, resource, action, description)
///
/// 编码使用点分隔 (`resource.action`),与 handler 中的 `require_permission` 调用保持一致。
const DEFAULT_PERMISSIONS: &[(&str, &str, &str, &str, &str)] = &[ const DEFAULT_PERMISSIONS: &[(&str, &str, &str, &str, &str)] = &[
("user:create", "创建用户", "user", "create", "创建新用户"), // === Auth module ===
("user:read", "查看用户", "user", "read", "查看用户信息"), ("user.list", "查看用户列表", "user", "list", "查看用户列表"),
("user:update", "编辑用户", "user", "update", "编辑用户信息"), ("user.create", "创建用户", "user", "create", "创建新用户"),
("user:delete", "删除用户", "user", "delete", "软删除用户"), ("user.read", "查看用户详情", "user", "read", "查看用户信息"),
("role:create", "创建角色", "role", "create", "创建新角色"), ("user.update", "编辑用户", "user", "update", "编辑用户信息"),
("role:read", "查看角色", "role", "read", "查看角色信息"), ("user.delete", "删除用户", "user", "delete", "软删除用户"),
("role:update", "编辑角色", "role", "update", "编辑角色"), ("role.list", "查看角色列表", "role", "list", "查看角色列表"),
("role:delete", "删除角色", "role", "delete", "删除角色"), ("role.create", "创建角色", "role", "create", "创建新角色"),
( ("role.read", "查看角色详情", "role", "read", "查看角色信息"),
"permission:read", ("role.update", "编辑角色", "role", "update", "编辑角色"),
"查看权限", ("role.delete", "删除角色", "role", "delete", "删除角色"),
"permission", ("permission.list", "查看权限", "permission", "list", "查看权限列表"),
"read", ("organization.list", "查看组织列表", "organization", "list", "查看组织列表"),
"查看权限列表", ("organization.create", "创建组织", "organization", "create", "创建组织"),
), ("organization.update", "编辑组织", "organization", "update", "编辑组织"),
( ("organization.delete", "删除组织", "organization", "delete", "删除组织"),
"organization:create", ("department.list", "查看部门列表", "department", "list", "查看部门列表"),
"创建组织", ("department.create", "创建部门", "department", "create", "创建部门"),
"organization", ("department.update", "编辑部门", "department", "update", "编辑部门"),
"create", ("department.delete", "删除部门", "department", "delete", "删除部门"),
"创建组织", ("position.list", "查看岗位列表", "position", "list", "查看岗位列表"),
), ("position.create", "创建岗位", "position", "create", "创建岗位"),
( ("position.update", "编辑岗位", "position", "update", "编辑岗位"),
"organization:read", ("position.delete", "删除岗位", "position", "delete", "删除岗位"),
"查看组织", // === Config module ===
"organization", ("dictionary.list", "查看字典", "dictionary", "list", "查看数据字典"),
"read", ("dictionary.create", "创建字典", "dictionary", "create", "创建数据字典"),
"查看组织架构", ("dictionary.update", "编辑字典", "dictionary", "update", "编辑数据字典"),
), ("dictionary.delete", "删除字典", "dictionary", "delete", "删除数据字典"),
( ("menu.list", "查看菜单", "menu", "list", "查看菜单配置"),
"organization:update", ("menu.update", "编辑菜单", "menu", "update", "编辑菜单配置"),
"编辑组织", ("setting.read", "查看配置", "setting", "read", "查看系统参数"),
"organization", ("setting.update", "编辑配置", "setting", "update", "编辑系统参数"),
"update", ("setting.delete", "删除配置", "setting", "delete", "删除系统参数"),
"编辑组织", ("numbering.list", "查看编号规则", "numbering", "list", "查看编号规则"),
), ("numbering.create", "创建编号规则", "numbering", "create", "创建编号规则"),
( ("numbering.update", "编辑编号规则", "numbering", "update", "编辑编号规则"),
"organization:delete", ("numbering.delete", "删除编号规则", "numbering", "delete", "删除编号规则"),
"删除组织", ("numbering.generate", "生成编号", "numbering", "generate", "生成文档编号"),
"organization", ("theme.read", "查看主题", "theme", "read", "查看主题设置"),
"delete", ("theme.update", "编辑主题", "theme", "update", "编辑主题设置"),
"删除组织", ("language.list", "查看语言", "language", "list", "查看语言配置"),
), ("language.update", "编辑语言", "language", "update", "编辑语言设置"),
( // === Workflow module ===
"department:create", ("workflow.create", "创建流程", "workflow", "create", "创建流程定义"),
"创建部门", ("workflow.list", "查看流程", "workflow", "list", "查看流程列表"),
"department", ("workflow.read", "查看流程详情", "workflow", "read", "查看流程定义详情"),
"create", ("workflow.update", "编辑流程", "workflow", "update", "编辑流程定义"),
"创建部门", ("workflow.publish", "发布流程", "workflow", "publish", "发布流程定义"),
), ("workflow.start", "发起流程", "workflow", "start", "发起流程实例"),
( ("workflow.approve", "审批任务", "workflow", "approve", "审批流程任务"),
"department:read", ("workflow.delegate", "委派任务", "workflow", "delegate", "委派流程任务"),
"查看部门", // === Message module ===
"department", ("message.list", "查看消息", "message", "list", "查看消息列表"),
"read", ("message.send", "发送消息", "message", "send", "发送新消息"),
"查看部门", ("message.template.list", "查看消息模板", "message.template", "list", "查看消息模板列表"),
), ("message.template.create", "创建消息模板", "message.template", "create", "创建消息模板"),
(
"department:update",
"编辑部门",
"department",
"update",
"编辑部门",
),
(
"department:delete",
"删除部门",
"department",
"delete",
"删除部门",
),
("position:create", "创建岗位", "position", "create", "创建岗位"),
("position:read", "查看岗位", "position", "read", "查看岗位"),
("position:update", "编辑岗位", "position", "update", "编辑岗位"),
("position:delete", "删除岗位", "position", "delete", "删除岗位"),
// Config module permissions
("dictionary:create", "创建字典", "dictionary", "create", "创建数据字典"),
("dictionary:list", "查看字典", "dictionary", "list", "查看数据字典"),
("dictionary:update", "编辑字典", "dictionary", "update", "编辑数据字典"),
("dictionary:delete", "删除字典", "dictionary", "delete", "删除数据字典"),
("menu:list", "查看菜单", "menu", "list", "查看菜单配置"),
("menu:update", "编辑菜单", "menu", "update", "编辑菜单配置"),
("setting:read", "查看配置", "setting", "read", "查看系统参数"),
("setting:update", "编辑配置", "setting", "update", "编辑系统参数"),
("numbering:create", "创建编号规则", "numbering", "create", "创建编号规则"),
("numbering:list", "查看编号规则", "numbering", "list", "查看编号规则"),
("numbering:update", "编辑编号规则", "numbering", "update", "编辑编号规则"),
("numbering:generate", "生成编号", "numbering", "generate", "生成文档编号"),
("theme:read", "查看主题", "theme", "read", "查看主题设置"),
("theme:update", "编辑主题", "theme", "update", "编辑主题设置"),
("language:list", "查看语言", "language", "list", "查看语言配置"),
("language:update", "编辑语言", "language", "update", "编辑语言配置"),
// Workflow module permissions
("workflow:create", "创建流程", "workflow", "create", "创建流程定义"),
("workflow:list", "查看流程", "workflow", "list", "查看流程列表"),
("workflow:read", "查看流程详情", "workflow", "read", "查看流程定义详情"),
("workflow:update", "编辑流程", "workflow", "update", "编辑流程定义"),
("workflow:publish", "发布流程", "workflow", "publish", "发布流程定义"),
("workflow:start", "发起流程", "workflow", "start", "发起流程实例"),
("workflow:approve", "审批任务", "workflow", "approve", "审批流程任务"),
("workflow:delegate", "委派任务", "workflow", "delegate", "委派流程任务"),
]; ];
/// Indices of read-only permissions within DEFAULT_PERMISSIONS. /// Indices of read-only (list/read) permissions within DEFAULT_PERMISSIONS.
const READ_PERM_INDICES: &[usize] = &[ const READ_PERM_INDICES: &[usize] = &[
1, // user:read 0, // user.list
5, // role:read 2, // user.read
8, // permission:read 5, // role.list
10, // organization:read 7, // role.read
14, // department:read 10, // permission.list
18, // position:read 11, // organization.list
22, // dictionary:list 15, // department.list
25, // menu:list 19, // position.list
27, // setting:read 23, // dictionary.list
30, // numbering:list 28, // menu.list
33, // theme:read 30, // setting.read
35, // language:list 32, // numbering.list
38, // workflow:list 37, // theme.read
39, // workflow:read 39, // language.list
43, // workflow.list
44, // workflow.read
49, // message.list
51, // message.template.list
]; ];
/// Seed default auth data for a new tenant. /// Seed default auth data for a new tenant.
/// ///
/// Creates: /// Creates:
/// - 21 permissions covering user/role/permission/organization/department/position CRUD /// - 53 permissions covering auth/config/workflow/message modules
/// - An "admin" system role with all permissions /// - An "admin" system role with all permissions
/// - A "viewer" system role with read-only permissions /// - A "viewer" system role with read-only permissions
/// - A super-admin user with the admin role and a password credential /// - A super-admin user with the admin role and a password credential
@@ -188,7 +150,6 @@ pub async fn seed_tenant_auth(
admin_role.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; admin_role.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?;
// Assign all permissions to admin role // Assign all permissions to admin role
// role_permission uses composite PK (role_id, permission_id) -- no separate id column
for perm_id in &perm_ids { for perm_id in &perm_ids {
let rp = role_permission::ActiveModel { let rp = role_permission::ActiveModel {
role_id: Set(admin_role_id), role_id: Set(admin_role_id),
@@ -281,7 +242,6 @@ pub async fn seed_tenant_auth(
cred.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?; cred.insert(db).await.map_err(|e| AuthError::Validation(e.to_string()))?;
// 5. Assign admin role to admin user // 5. Assign admin role to admin user
// user_role uses composite PK (user_id, role_id) -- no separate id column
let user_role_assignment = user_role::ActiveModel { let user_role_assignment = user_role::ActiveModel {
user_id: Set(admin_user_id), user_id: Set(admin_user_id),
role_id: Set(admin_role_id), role_id: Set(admin_role_id),

View File

@@ -22,7 +22,7 @@ where
MessageState: FromRef<S>, MessageState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "message:list")?; require_permission(&ctx, "message.list")?;
let db = &_state.db; let db = &_state.db;
let page = query.page.unwrap_or(1); let page = query.page.unwrap_or(1);
@@ -49,7 +49,7 @@ where
MessageState: FromRef<S>, MessageState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "message:list")?; require_permission(&ctx, "message.list")?;
let result = MessageService::unread_count(ctx.tenant_id, ctx.user_id, &_state.db).await?; let result = MessageService::unread_count(ctx.tenant_id, ctx.user_id, &_state.db).await?;
Ok(Json(ApiResponse::ok(result))) Ok(Json(ApiResponse::ok(result)))
@@ -65,7 +65,7 @@ where
MessageState: FromRef<S>, MessageState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "message:send")?; require_permission(&ctx, "message.send")?;
req.validate() req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?; .map_err(|e| AppError::Validation(e.to_string()))?;

View File

@@ -28,7 +28,7 @@ where
MessageState: FromRef<S>, MessageState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "message:template:list")?; require_permission(&ctx, "message.template:list")?;
let page = query.page.unwrap_or(1); let page = query.page.unwrap_or(1);
let page_size = query.page_size.unwrap_or(20); let page_size = query.page_size.unwrap_or(20);
@@ -56,7 +56,7 @@ where
MessageState: FromRef<S>, MessageState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "message:template:create")?; require_permission(&ctx, "message.template:create")?;
req.validate() req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?; .map_err(|e| AppError::Validation(e.to_string()))?;

View File

@@ -22,7 +22,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:list")?; require_permission(&ctx, "workflow.list")?;
let (defs, total) = DefinitionService::list(ctx.tenant_id, &pagination, &state.db).await?; let (defs, total) = DefinitionService::list(ctx.tenant_id, &pagination, &state.db).await?;
@@ -49,7 +49,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:create")?; require_permission(&ctx, "workflow.create")?;
req.validate() req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?; .map_err(|e| AppError::Validation(e.to_string()))?;
@@ -75,7 +75,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:read")?; require_permission(&ctx, "workflow.read")?;
let resp = DefinitionService::get_by_id(id, ctx.tenant_id, &state.db).await?; let resp = DefinitionService::get_by_id(id, ctx.tenant_id, &state.db).await?;
Ok(Json(ApiResponse::ok(resp))) Ok(Json(ApiResponse::ok(resp)))
@@ -92,7 +92,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:update")?; require_permission(&ctx, "workflow.update")?;
let resp = let resp =
DefinitionService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?; DefinitionService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?;
@@ -109,7 +109,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:publish")?; require_permission(&ctx, "workflow.publish")?;
let resp = DefinitionService::publish( let resp = DefinitionService::publish(
id, id,

View File

@@ -22,7 +22,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:start")?; require_permission(&ctx, "workflow.start")?;
req.validate().map_err(|e| AppError::Validation(e.to_string()))?; req.validate().map_err(|e| AppError::Validation(e.to_string()))?;
let resp = InstanceService::start( let resp = InstanceService::start(
@@ -47,7 +47,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:list")?; require_permission(&ctx, "workflow.list")?;
let (instances, total) = let (instances, total) =
InstanceService::list(ctx.tenant_id, &pagination, &state.db).await?; InstanceService::list(ctx.tenant_id, &pagination, &state.db).await?;
@@ -75,7 +75,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:read")?; require_permission(&ctx, "workflow.read")?;
let resp = InstanceService::get_by_id(id, ctx.tenant_id, &state.db).await?; let resp = InstanceService::get_by_id(id, ctx.tenant_id, &state.db).await?;
Ok(Json(ApiResponse::ok(resp))) Ok(Json(ApiResponse::ok(resp)))
@@ -91,7 +91,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:update")?; require_permission(&ctx, "workflow.update")?;
InstanceService::suspend(id, ctx.tenant_id, ctx.user_id, &state.db).await?; InstanceService::suspend(id, ctx.tenant_id, ctx.user_id, &state.db).await?;
Ok(Json(ApiResponse::ok(()))) Ok(Json(ApiResponse::ok(())))
@@ -107,7 +107,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:update")?; require_permission(&ctx, "workflow.update")?;
InstanceService::terminate(id, ctx.tenant_id, ctx.user_id, &state.db).await?; InstanceService::terminate(id, ctx.tenant_id, ctx.user_id, &state.db).await?;
Ok(Json(ApiResponse::ok(()))) Ok(Json(ApiResponse::ok(())))
@@ -123,7 +123,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:update")?; require_permission(&ctx, "workflow.update")?;
InstanceService::resume(id, ctx.tenant_id, ctx.user_id, &state.db).await?; InstanceService::resume(id, ctx.tenant_id, ctx.user_id, &state.db).await?;
Ok(Json(ApiResponse::ok(()))) Ok(Json(ApiResponse::ok(())))

View File

@@ -22,7 +22,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:approve")?; require_permission(&ctx, "workflow.approve")?;
let (tasks, total) = let (tasks, total) =
TaskService::list_pending(ctx.tenant_id, ctx.user_id, &pagination, &state.db).await?; TaskService::list_pending(ctx.tenant_id, ctx.user_id, &pagination, &state.db).await?;
@@ -50,7 +50,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:approve")?; require_permission(&ctx, "workflow.approve")?;
let (tasks, total) = let (tasks, total) =
TaskService::list_completed(ctx.tenant_id, ctx.user_id, &pagination, &state.db).await?; TaskService::list_completed(ctx.tenant_id, ctx.user_id, &pagination, &state.db).await?;
@@ -79,7 +79,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:approve")?; require_permission(&ctx, "workflow.approve")?;
req.validate().map_err(|e| AppError::Validation(e.to_string()))?; req.validate().map_err(|e| AppError::Validation(e.to_string()))?;
let resp = TaskService::complete( let resp = TaskService::complete(
@@ -106,7 +106,7 @@ where
WorkflowState: FromRef<S>, WorkflowState: FromRef<S>,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
require_permission(&ctx, "workflow:delegate")?; require_permission(&ctx, "workflow.delegate")?;
req.validate().map_err(|e| AppError::Validation(e.to_string()))?; req.validate().map_err(|e| AppError::Validation(e.to_string()))?;
let resp = let resp =