fix(db): 补注册附件备注的读写权限码(修「写通读断」的权限错位)

根因(比上一轮报告的更深一层)
----
field_permissions.py 的读过滤要求两个权限码:
    material_list:productImageRemark / material_list:manualLinkRemark
而这两个码**在 sys_element 里根本不存在**,也从未授予任何角色 —— 是「幽灵权限」。
除 SUPER_ADMIN(走 material_list:* 通配符绕过)外,没有任何人能通过读过滤。
配合写侧的「不在映射中 → 默认允许」兜底,形成:
    谁都能写、除超管没人能读 —— 填了存进去了,回显却被抹成 null,
    看起来就是「保存不了」。

本次改动
----
一、读侧:注册这两个元素,授予与 material_list:files **完全相同的 6 个角色**
    (INBOUND / OUTBOUND / SALES / SUPERVISOR / WAREHOUSE_MGR / SUPER_ADMIN)
    依据:备注依附于图片,「能看图的人就应该能看备注」。
二、写侧:新建 material_list:remark_edit,只授予 3 个核心管理角色
    (SUPER_ADMIN / SUPERVISOR / WAREHOUSE_MGR)。
    ★ 为何不复用 material_list:operation:它授予了 5 个角色(含 INBOUND /
      OUTBOUND),比业务要求的 3 个更宽,达不到「只有核心管理角色可改」。
    ⚠ 该码只做精确匹配,**切勿用 @permission_required 包裹** ——
      _expand_operation_perms 会前缀桥接,把 material_list:operation 的持有者
      一并放行,等于把刚收紧的口子又捅开。已在迁移与代码注释中双处警示。

幂等,可重复执行;脚本文本不含 psql 元命令,DataGrip 可直接整段执行。
This commit is contained in:
yueli
2026-09-17 11:07:50 +08:00
parent b26b124f8b
commit 6b7b174e3d

View File

@ -0,0 +1,136 @@
-- =============================================================================
-- 基础信息 · 附件备注的读写权限修复
--
-- 现象
-- 主管SUPERVISOR在基础信息里填「产品图备注 / 说明书备注」,点确定后
-- 刷新页面又是空的 —— 看起来像「保存不了」。
--
-- 根因:写通、读断,且读侧引用的是**两个从未注册过的权限码**
-- 写侧PUT /inbound/base/<id> 的 field_to_perm 里**没有**这两个字段,
-- 落入「不在映射中 → 默认允许」的兜底分支 → 直接落库。**保存是成功的。**
-- 读侧:列表/搜索接口用 apply_strict_rbac 按 field_permissions.py 过滤,
-- 该映射要求 material_list:productImageRemark / material_list:manualLinkRemark。
-- 而这两个码 **在 sys_element 里根本不存在**,也从未授予任何角色 ——
-- 除 SUPER_ADMIN走 material_list:* 通配符绕过)外,**没有任何人能读到**。
-- 于是:填了 → 存进去了 → 回显被抹成 null → 看起来没保存。
--
-- 本次修复(读写双向)
-- 一、读侧:注册这两个元素,并授予与 material_list:files **完全相同的 6 个角色**
-- —— 逻辑是「能看图的人,就应该能看备注」。
-- 二、写侧:新建 material_list:remark_edit只授予 3 个核心管理角色。
-- 后端同时把这两个字段显式纳入 field_to_perm 映射,废止那个兜底分支。
--
-- ---------------------------------------------------------------------------
-- ★ 为什么写侧要单独建码,而不复用 material_list:operation
-- material_list:operation 授予了 INBOUND / OUTBOUND / SUPERVISOR /
-- WAREHOUSE_MGR / SUPER_ADMIN 共 5 个角色,比业务要求的 3 个更宽。
-- 复用会让出库员、入库员也能改备注,达不到「只有核心管理角色可改」。
--
-- ★ 为什么码用冒号形式material_list:remark_edit
-- 本系统有两类权限码:动作类(无冒号,如 outbound_return与字段类
-- (冒号形式,如 material_list:files。这是**字段级**权限,与
-- material_list:* 家族保持一致,便于在权限管理界面归组。
-- ⚠ 它只用于 base.py 里的**精确匹配**perm_code in user_permissions
-- **切勿用 @permission_required('material_list:remark_edit') 包裹路由** ——
-- _expand_operation_perms() 会做前缀桥接,凡持有 material_list:operation
-- 的角色都会被放行,等于把刚收紧的口子又捅开。
--
-- 幂等:全部带存在性判断,可重复执行。
-- 执行docker exec -i inventory_db psql -U test -d inventory_system < 本文件
-- =============================================================================
BEGIN;
-- ---------------------------------------------------------------------------
-- 一、读侧:注册两个字段权限元素(此前从未注册,是「幽灵权限」)
-- ---------------------------------------------------------------------------
INSERT INTO sys_element (menu_code, name, code, element_type)
SELECT 'material_list', v.name, v.code, 'element'
FROM (VALUES
('产品图备注', 'material_list:productImageRemark'),
('说明书备注', 'material_list:manualLinkRemark')
) AS v(name, code)
WHERE NOT EXISTS (SELECT 1 FROM sys_element e WHERE e.code = v.code);
-- 授予与 material_list:files 完全相同的 6 个角色
INSERT INTO sys_role_permission (role_code, target_code, type, company_name)
SELECT r.role_code, e.code, 'element', NULL
FROM (VALUES ('material_list:productImageRemark'), ('material_list:manualLinkRemark')) AS e(code)
CROSS JOIN (VALUES
('INBOUND'), ('OUTBOUND'), ('SALES'),
('SUPERVISOR'), ('WAREHOUSE_MGR'), ('SUPER_ADMIN')
) AS r(role_code)
WHERE NOT EXISTS (
SELECT 1 FROM sys_role_permission x
WHERE x.role_code = r.role_code AND x.target_code = e.code AND x.type = 'element'
);
-- ---------------------------------------------------------------------------
-- 二、写侧:新建「附件备注编辑」权限码,只授予 3 个核心管理角色
-- ---------------------------------------------------------------------------
INSERT INTO sys_element (menu_code, name, code, element_type)
SELECT 'material_list', '附件备注编辑(产品图/说明书)', 'material_list:remark_edit', 'element'
WHERE NOT EXISTS (SELECT 1 FROM sys_element WHERE code = 'material_list:remark_edit');
INSERT INTO sys_role_permission (role_code, target_code, type, company_name)
SELECT r.role_code, 'material_list:remark_edit', 'element', NULL
FROM (VALUES ('SUPER_ADMIN'), ('SUPERVISOR'), ('WAREHOUSE_MGR')) AS r(role_code)
WHERE NOT EXISTS (
SELECT 1 FROM sys_role_permission x
WHERE x.role_code = r.role_code
AND x.target_code = 'material_list:remark_edit'
AND x.type = 'element'
);
COMMIT;
-- =============================================================================
-- 执行后核对
-- =============================================================================
SELECT '=== 1) 三个权限元素已注册(应 3 行)===' AS "核对项";
SELECT code, name, menu_code, element_type FROM sys_element
WHERE code IN ('material_list:productImageRemark','material_list:manualLinkRemark','material_list:remark_edit')
ORDER BY code;
SELECT '=== 2) 读权限:应各 6 个角色,与 material_list:files 一致 ===' AS "核对项";
SELECT target_code AS , count(*) AS ,
string_agg(role_code, ', ' ORDER BY role_code) AS
FROM sys_role_permission
WHERE target_code IN ('material_list:files','material_list:productImageRemark','material_list:manualLinkRemark')
AND type = 'element'
GROUP BY target_code ORDER BY target_code;
SELECT '=== 3) 写权限:应 3 个角色SUPER_ADMIN / SUPERVISOR / WAREHOUSE_MGR===' AS "核对项";
SELECT role_code FROM sys_role_permission
WHERE target_code = 'material_list:remark_edit' AND type = 'element' ORDER BY role_code;
SELECT '=== 4) 三组角色集合是否完全对齐(应全为 true===' AS "核对项";
SELECT
(SELECT bool_and(a = b) FROM (
SELECT
(SELECT string_agg(role_code,',' ORDER BY role_code) FROM sys_role_permission
WHERE target_code='material_list:files' AND type='element') AS a,
(SELECT string_agg(role_code,',' ORDER BY role_code) FROM sys_role_permission
WHERE target_code='material_list:productImageRemark' AND type='element') AS b
) t) AS files一致,
(SELECT bool_and(a = b) FROM (
SELECT
(SELECT string_agg(role_code,',' ORDER BY role_code) FROM sys_role_permission
WHERE target_code='material_list:files' AND type='element') AS a,
(SELECT string_agg(role_code,',' ORDER BY role_code) FROM sys_role_permission
WHERE target_code='material_list:manualLinkRemark' AND type='element') AS b
) t) AS files一致;
-- =============================================================================
-- 回滚段
-- =============================================================================
-- BEGIN;
-- DELETE FROM sys_role_permission WHERE target_code = 'material_list:remark_edit';
-- DELETE FROM sys_element WHERE code = 'material_list:remark_edit';
-- DELETE FROM sys_role_permission
-- WHERE target_code IN ('material_list:productImageRemark','material_list:manualLinkRemark');
-- DELETE FROM sys_element
-- WHERE code IN ('material_list:productImageRemark','material_list:manualLinkRemark');
-- COMMIT;