建議 model:Sonnet 5,effort:medium——policy 寫法有現成樣板,但這三張表在登入/切租戶/權限計算的熱路徑上,寫錯的症狀是「登入後選單全空」,且
user_org_units沒有 tenant_id 欄位要另想 policy 形狀。本卡屬 FR-094(母卡見兄弟卡段),.2 開隔離的第 2 棒(整案第 4 棒)。無前置棒,可與 .2a 平行。吸收 CM-1445。
三張表記的是「誰在哪個租戶有什麼角色」「誰隸屬哪些租戶」「誰屬於哪個部門」——這是全公司的名冊與門禁清單,現在完全沒隔離,任何客戶帳號直查就能看到別家「誰是管理員、誰能切到哪個租戶」。更危險的是 user_tenants 本身就是「切換租戶」合法性的依據,一張控制門禁的表自己沒有門禁。
首腦核對(2026-09-13 DEV localhost 實查):
public.user_roles:RLS 關、0 policy、41 筆;欄位 user_id NOT NULL, tenant_id (nullable), org_unit_id (nullable)(02-schema.sql:15555)。tenant_id 實際值分佈:1/102/131/158/162。public.user_tenants:RLS 關、0 policy、41 筆;欄位 user_id NOT NULL, tenant_id NOT NULL(02-schema.sql:15664)。public.user_org_units:RLS 關、0 policy、37 筆;欄位 user_id NOT NULL, org_unit_id NOT NULL——沒有 tenant_id(02-schema.sql:15506),標準 4 條 policy 套不上,要走 JOIN(org_units.tenant_id)或 app_org_allowed_for_session(org_unit_id)。jedi_iam/middleware/context.py:60 default_membership_check——名冊來源是 user.tenants(user_tenants 關聯表載出的)。陷阱:切到子租戶時,本尊列的 users.tenant_id 在父租戶,CM-1661 已補「本人恆可見」;user_tenants 的 policy 必須同樣保證「本人的列恆可見」(user_id = current_setting('app.user_id')),否則切租戶後查不到自己隸屬 → 403。jedi_iam/infra/repository/user_role_repo_impl.py:81-87——Capability JOIN RoleCapability JOIN UserRole JOIN Role,註解寫明「查本人時用 context 的 user 主鍵直接過濾 user_roles,不 join users」(因 users 有 RLS)。同理 user_roles policy 也要有「本人恆可見」分支,且要與 UiRouteRepoImpl.get_viewable_by_user_uid 共用條件(同檔註解提到)。/1/ 前綴涵蓋所有子租戶(app_tenant_allowed_for_session 是前綴比對),加 is_super_admin OR 後兩條都通。~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/2026-09-XX-fr094-cm<本卡號>-rls-user-roles-tenants-org-units.sql (新)本棒 migration
~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/manifest.tsv 加一列
~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/init/02-schema.sql:15506,15555,15664 三張表定義
~/Projects/Jedicogy/module/jedi-python-package/jedi-iam/jedi_iam/middleware/context.py:60 default_membership_check(切租戶讀 user_tenants)
~/Projects/Jedicogy/module/jedi-python-package/jedi-iam/jedi_iam/infra/repository/user_role_repo_impl.py:81-87 授權查詢 JOIN user_roles
~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/2026-09-12-cm1664-users-rls-self-visible-textcmp.sql 「本人恆可見」分支的寫法(文字比對版,照抄)
user_roles/user_tenants:各 4 條,select/update/delete 用 is_super_admin='t' OR app_tenant_allowed_for_session(tenant_id) OR user_id::text = current_setting('app.user_id', true)(本人恆可見分支照 CM-1664 的文字比對寫法,避免型別轉換炸);insert WITH CHECK (is_super_admin='t' OR app_tenant_allowed_for_session(tenant_id))。user_roles.tenant_id 可為 NULL——先查 NULL 筆數(select count(*) from user_roles where tenant_id is null),有的話 app_tenant_allowed_for_session(NULL) 回什麼要實測(helper 定義 02-schema.sql:623),不能讓 NULL 列變成人人可見或人人不可見而不自知;回寫本卡。user_org_units(無 tenant_id):兩案擇一,先查 app_org_allowed_for_session 定義與 org_units 表有無 tenant_id 再定:(A) USING (is_super_admin='t' OR user_id::text = current_setting('app.user_id', true) OR EXISTS (select 1 from public.org_units o where o.id = org_unit_id and app_tenant_allowed_for_session(o.tenant_id)));(B) 加 tenant_id 欄位由 org_units 回填(要改 jedi-iam model,較重)。預設選 A(不動 schema、帶舊資料升級零風險);若 A 在 org_units 本身有 RLS 時會遞迴擋住,回寫本卡問首腦。blsadmin(/1/)開後仍全量。貼實際數字。user_role_repo_impl 的能力查詢,開 RLS 前後回同一組能力名;以「被指派到子租戶」的帳號切租戶後 default_membership_check 仍 True。