FR-094 · 需求索引 · 本頁由 build 掃資料夾生成

FR-094 租戶隔離修正(RLS 第三波)——13 張表+4 支 view 真的把「每個客戶只看自己資料」關上

✅ 全案 Done,隨 v1.20.0 出版(2026-09-15)。母卡 CM-1766,十張子卡(1767/1787/1788/1768~1773/1789/1790)+收尾卡(1794~1800)全收。RLS 第三波開啟 40+ 張表的租戶隔離、排程改用具名系統身分、指派候選名單只列本租戶、新增「分享給子租戶」開關。原先裁定排在 1.20.0 之後的五張(.1c/.2b/.2c/.2e/.3a)當輪一併完成,未留尾巴。

狀態:✅ 已完成 文件 1 份

FR-094 一頁看完

§1

🔴 先看哪一份

你想知道 看這份 這份的性質
租戶、階層、部門三條可見度原則,為什麼這樣定、業界怎麼做 設計 design.md 設計決策,定稿凍結
十棒各做了什麼、收尾卡有哪些 本頁「總進度」 現況
資料庫規則現況(session 變數、helper、各表 policy) docs/system-design/database/RLS_DESIGN.md 現況參考
13 張表+4 支 view 是怎麼查出來的 FR-087 盤點報告 一次性證據
決策者表態過什麼 本頁「需求討論紀錄」 歷史,只追加
§2

結論

  1. 產品靠資料庫的 RLS(Row Level Security,資料庫自己擋、讓每個客戶只查得到自己的資料)做多租戶隔離。FR-087 盤點(CM-1663)查出 13 張表、4 支 view 的隔離實際上是關的:有的規則寫好但開關沒開、有的連規則都沒寫、view 用「建立者身分」查底層而繞過隔離。決策者 2026-09-11 親測:切到子租戶後仍看到別家待辦 39 筆、專案 213 筆。本案把這些一張一張補齊、打開,並驗證登入、切租戶、我的任務、兩支凌晨排程都還活著。
  2. 三個定案:D1 公版機制沿用「policy 內加旗標 OR 分支」,不動 app_tenant_allowed_for_session helper(12+ 張表共用),既有 flow_templates.is_builtin/module_frames、detection_profiles 的 scope='SYSTEM' 不回改,新開的表一律用 scope;D2 問卷不加公版旗標(引用是複製進任務),要修的是 survey schema 另 12 張沒 RLS 沒 tenant_id 的裸表;D3 projects 的 RLS 是 2026-06-25 環境對齊順手關的(commit e7709295),非產品決策,補 is_super_admin 分支後重開、DEV 造跨租戶樣本驗。
  3. 順序硬相依:03:10 孤兒清理與 01:00 框架解析清理兩支排程靠「無身分=super admin」跑,那正是 CM-1559 要收緊的洞。必須 .1a 先給排程具名系統身分 → CM-1559 收緊 → .2c 才開 job_executions/framework_parse_jobs,反過來清理程式會安靜失效不報錯。
  4. 平台管理員不受影響(viewer_is_platform_admin() 走 root 路徑,不是靠隔離沒開)。不做:log_forwarding_settings(本質平台層,能力點守門已擋)、任務詳細 API 不驗 project_uid(歸 FR-086)、surveys/survey_folders 公版旗標(D2)。
  5. 可見度三原則(設計全文見 design.md):① 租戶不跨——一筆資料只屬一個租戶,不跨租戶指派、不跨租戶操作,別租戶要看只能靠公版、母租戶分享給子樹、或複製;② 階層只往下看——母租戶看得到子租戶資料但不在裡面操作,要操作就由該租戶加人給角色、切租戶進去做、紀錄留在該租戶;③ 部門不是牆——部門只記歸屬與寫入範圍,查詢規則一律不加部門條件,可見度靠角色與參與關係。
  6. 三原則對八張卡的影響:租戶部分的規則形狀不用改(前綴規則就是正解);.2d 與 .2e 開之前要先把 workflow_executions/workflow_templates 改、刪規則裡的部門條件換成租戶條件(DEV 任務指派 68% 跨部門,照現在規則開,跨部門推進任務會更新 0 筆不報錯)。長出兩件另開卡、不擋 1.20.0 的事:指派候選名單擋非本租戶帳號+SPEC 寫明不支援跨租戶指派;母租戶自訂資源(模組框架、檢測方案、範本)scope 加「分享給子樹」一檔。
  7. 每張開隔離的卡必做「開前 vs 開後」對照(模擬非 super_admin 租戶帳號查筆數);migration 只套 DEV(localhost)+ 188:25432 基線庫並重產 02-schema;帶舊資料升級不破。
§3

📊 總進度

棒 卡 做什麼 規模 順序 狀態
1 ✅ CM-1767 .1a 兩支凌晨排程具名系統身分(core/scheduler.py:202,386;jedi-common system_context()+is_system_context) BE 1 檔+jedi-common 1~2 檔 序列組 S 第一棒 ✅
2 ✅ CM-1787 .1b CM-1559 第二刀:db.py 拿掉 not tenant_id、signed_token.py 改走 build_user_context()(OTP QR 必修) jedi-common+jedi-iam,併 1.1.0 序列組 S:.1a 後 ✅
2b ✅ CM-1788 .1c CM-1559 第一刀:無身分 fail-closed+引導型/排程/機器端點三類接線;Drive 路徑要 STG 驗 jedi-common+BE+jedi-iam/remote-agent .1a+1787 後、等首腦令 ✅
3 ✅ CM-1768 .2a 甲組:tenant_drive_integrations 開;projects 補 super_admin 分支+開;DEV 跨租戶樣本;重產 02(吸收 CM-1453) 1 migration 可立即派 ✅
4 ✅ CM-1769 .2b 乙組高敏感:user_roles/user_tenants/user_org_units 補 policy+開(吸收 CM-1445) 1 migration 可立即派 ✅
5 ✅ CM-1770 .2c 乙組其餘:poams/evidence_classification_*/job_executions/framework_parse_jobs+information_systems 基線對齊;驗兩支排程 1 migration 序列組 S:.1a+1559 收完 ✅
6 ✅ CM-1771 .2d 丙丁組:workflow_executions 補 select+開;2 支 view security_invoker;v_user_routes/v_role_routes DROP(決策者裁);開前先把改、刪規則的部門條件換成租戶條件 1 migration+view SQL 可平行;與 .2c 同輪驗我的任務 ✅
7 ✅ CM-1772 .2e 戊組:workflow_templates 加 scope、回填無主凍結副本、policy 加公版分支+開;凍結根因修 1 migration+jedi-flow-engine 1~2 檔 可平行;無連結無主列處置等首腦裁 ✅
8 ✅ CM-1773 .3a 問卷 12 張裸表:實查引用鏈選「加 tenant_id 回填」或「JOIN-based policy」,定案後補 policy+開 jedi-survey model+1 migration 可平行;方案定案前不動 schema ✅
9 ✅ CM-1789 .4a 指派人/參與者候選名單只列本租戶帳號(jedi-iam get_users_menu 加租戶條件、FE 三處確認)+SPEC 寫明不支援跨租戶指派 jedi-iam+FE+SPEC 無前置 ✅
10 ✅ CM-1790 .4b 母租戶自訂資源分享給子樹:新 helper app_tenant_is_ancestor_of_session、四張表 scope 加 SHARED+select 分支+UI 開關 1 migration+讀取端+FE .2e 後 ✅

收尾卡(驗收與補強,非原始十棒)

卡 做什麼 狀態
CM-1794 DEV 19 張表+12 支 sequence 擁有者由 cm_app 改回 cmmgr(擁有者錯會讓 RLS 對該表失效) ✅
CM-1796 question_answers.survey_id 回填+task_survey_id 補 FK CASCADE ✅
CM-1797 CM-1769 的 12 條 policy 改 DROP POLICY IF EXISTS+CREATE(裸 CREATE 手動重套會撞 already exists);CM-1772 的 update WITH CHECK 去部門條件 ✅
CM-1800 專案底下 13 張表補租戶隔離(調查結果、成員、稽核輪次等,靠專案繞一層判可見度) ✅
§4

需求討論紀錄

  • 2026-09-13 決策者裁定 D1–D3 與順序硬相依(見結論 2、3)。同日開卡 runner 在 DEV(localhost)實查校正盤點數字:projects 221 筆全屬 tenant 102;workflow_templates 18,135 筆、本機 tenant_id IS NULL 為 0 筆(FR-087 於 188 快照查到 191 筆)——.2e 卡要求對 188 快照與基線庫再各查一次,回填 SQL 不論本機是否為 0 都寫進 migration(冪等);information_systems DEV 已由 jedi-asset 002 開 RLS(基線未開),.2c 只做基線對齊;tenant_drive_integrations DEV 在 public schema(盤點寫 compliance),.2a 動手前確認。
  • 2026-09-14 決策者裁可見度三原則:討論起點是「母租戶開任務給子租戶帳號,那個帳號會拿不到母租戶的框架、問卷」;分析後決策者判「根本不應該跨租戶指派,頂多上面能看下面,要操作就讓該租戶派權限、用切換進去」,與業界階層式多租戶做法一致。部門則裁「資料共享、部門只管誰能改、特定資料日後逐筆收緊」。連帶查到既有 workflow_executions/workflow_templates 改、刪規則綁部門且無逃生口,.2d/.2e 開前要先修。全文與被排除的選項見 design.md。
  • 2026-09-14 決策者裁 B 案:1.20.0 做 .1a(CM-1767)、.2a(CM-1768)、.2d(CM-1771,開前先把 workflow_executions 改、刪規則的部門條件換成租戶條件)、CM-1559 只做第二刀(tenant_id 空值不再算 super admin,含 signed-token 改填真實身分);CM-1559 第一刀(無身分直接拒絕)與 .2b/.2c/.2e/.3a 五張排 1.20.0 之後、SaaS 上線前必做。判斷依據:落地版一台機一個客戶,剩下 11 張表洩漏的是同公司內部子租戶資料,不擋出貨;現在做會撞 FR-093 出貨鏈。決策者原話「那先 B 吧」。
  • 2026-09-14 決策者點名核對安裝包 seed:出貨公版只有合規框架(CMMC 2.0 L1/L2)、流程範本 2 支、掃描設定檔 10 套三類(scripts/init/04-seed-core.sql/05-seed-catalog.sql 實查)。workflow_templates 基線刻意清空(04 尾端有非空即報錯守衛)、module_frames 與 surveys 零筆。DEV 上掛 tenant 1 的那些都是測試資料,前一輪分析被 DEV 資料誤導兩次(問卷公版、模組框架公版),已改正。判斷公版與否一律看 scripts/init/ 不看 DEV。
  • 2026-09-14 決策者確認兩件:① 租戶隔離的理解定案——租戶整個隔開;要在某租戶操作,必須先被該租戶授權(加進租戶、給角色)再切換過去;預設母對子只能瀏覽(資料庫層目前只擋讀的範圍、寫入靠程式層角色檢查,「上層對下層只讀不寫」要在 DB 層擋另開卡)。② 問卷沒有公版、不需要公版:出貨基線零筆問卷,DEV 掛 tenant 1 的 8 份是手建測試資料;D2 維持,任務引用時複製。新租戶初始化不複製參考資料(框架、範本、檢測方案走公版共用),只 seed 預設部門、管理員角色、儲存設定。
  • 2026-09-14 四項裁決(決策者「都照你建議,也都開」):① CM-1559 拆成 CM-1787(第二刀,1.20.0)與 CM-1788(第一刀,SaaS 前),原卡 Done;② 開 CM-1789(指派候選名單擋非本租戶+SPEC)與 CM-1790(母租戶資源分享給子樹),都排 1.20.0 之後;③ .2d 的 v_user_routes/v_role_routes 零讀者、pg_depend 無依賴,一律 DROP;④ 派工順序:CM-1768 與 CM-1771 現在派、平行(只動主專案 migration);CM-1767 等 FR-093 .4a~.4c 收完再派(動 jedi-common 走 path dependency,避免撞 pyproject);CM-1787 在 1767 後。
  • 2026-09-14 範圍討論(B 案的分析過程):首腦建議只做 .1a+CM-1559;範圍分析員指出 CM-1559 第一刀(無身分直接拒絕)回歸面最大、要關的洞落地版目前打不到(X-Tenant-ID:0 已被 FR-069.16 擋、users.tenant_id 無 NULL)、且 Drive 路徑在 DEV 驗不出(表擁有者是 cm_app);而決策者 9/11 親測的兩個症狀對應 .2a 與 .2d,是全案最便宜好驗的兩張。建議 B 案:.1a+.2a+.2d+1559 第二刀,其餘排 1.20.0 之後。等決策者裁。
  • 2026-09-13 吸收兩張舊安全債卡:CM-1445(三張授權關聯表無 RLS,FR-069 D9 附帶發現)→ 併 CM-1769;CM-1453(projects RLS 停用是否刻意)→ 查證為環境對齊順手關、非刻意,併 CM-1768。兩張卡已 append 併入說明並改 Done。
  • 待裁記帳(三項仍未裁):租戶層設定是否全部繼承最上層(現只複製儲存設定,SMTP/閒置逾時/第三方登入新租戶空白,讀取端各自 fallback);DB 層是否擋「上層對下層寫入」;.4b UI 開關要不要拆 FE 卡。(已裁:.2d 兩支 view DROP;.2e 無主列一律刪。實作期定案兩項:.3a 問卷裸表選 EXISTS 繞 surveys——不加 tenant_id 欄位,因回填會把還在用的作答靜默藏起來;.4b 選 scope 加第三態 SHARED——布林裝不下三態,is_builtin 不回改。)
  • 前作:FR-087 盤點報告 ../FR-087-2609-tenant-isolation-audit/scan-T1-isolation-gap-audit.md(§3.1 十三張表、§3.2 四支 view、§5.2 無主副本、§6.1 兩支排程、§8 分組);CM-1559 第一步盤點(31 張 RLS 表、8 支排程、引導悖論三類分流)。
§5

座標

  • RLS 設計:docs/system-design/database/RLS_DESIGN.md;helper 定義 scripts/init/02-schema.sql:623;標準 policy 樣板 scripts/sql/2026-04-23-google-drive-oauth-integration.sql:48-79
  • 兩支排程:core/scheduler.py:202(01:00 framework_parse_jobs)、:386(03:10 job_executions)
  • fail-open 判定:jedi-common jedi_common/session/database/db.py:100-119
  • 對照帳號:blsit(tenant 102,非 super_admin)/blsadmin(super_admin,路徑 /1/);DEV 庫 psql -h localhost -p 5432 -U cmmgr -d guidant_ai_dev;基線庫 188:25432 guidant_ai
  • branch:BE feature/FR-075、jedi feature/FR-075(2026-09-13 查)
  • skill:sql-migration、jedi-package-dev
§6

文件

以下全部由 build 掃資料夾產生,新增檔案重 build 即自動出現。標題連結指向渲染後的 HTML,md 連向源檔。

設計

文件 類型 標題 最後更新
design / md 設計 租戶是牆、階層只能往下看、部門不是牆 2026-09-14
§7

Notion 卡

卡片內容(決策紀錄、驗收條件)以 Notion 為準,本頁只記座標。

關係 卡號 標題 狀態
母案 CM-1766 FR-094 租戶隔離修正(RLS 第三波)——13 張表+4 支 view 把「每個客戶只看自己資料」真的關上(7 棒,實作;含既有 CM-1559) —
子卡 CM-1767 .1a 兩支凌晨排程具名系統身分 Done
子卡 CM-1787 .1b CM-1559 第二刀:tenant_id 空值不算 super admin+signed-token 填真實身分 Done
子卡 CM-1788 .1c CM-1559 第一刀:無身分 fail-closed+三類接線(SaaS 前) Done
子卡 CM-1768 .2a 甲組純開關:tenant_drive_integrations/projects(吸收 CM-1453) Done
子卡 CM-1769 .2b 乙組高敏感:user_roles/user_tenants/user_org_units(吸收 CM-1445) Done
子卡 CM-1770 .2c 乙組其餘+排程相依表 Done
子卡 CM-1771 .2d 丙丁組:workflow_executions+4 支 view security_invoker Done
子卡 CM-1772 .2e 戊組:workflow_templates scope+回填+開 Done
子卡 CM-1773 .3a 問卷 12 張裸表隔離 Done
子卡 CM-1789 .4a 指派候選名單只列本租戶+SPEC 不支援跨租戶指派 Done
子卡 CM-1790 .4b 母租戶自訂資源分享給子樹(scope SHARED) Done