Files
KCGL/db_migrations/phase5_material_remark_rbac.sql
yueli 6b7b174e3d 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 可直接整段执行。
2026-09-17 11:07:50 +08:00

137 lines
7.4 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- =============================================================================
-- 基础信息 · 附件备注的读写权限修复
--
-- 现象
-- 主管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;