---
title: "FR-132 A.1 — D12 部門資料隔離：挑選表清單"
---

# D12 部門資料隔離：挑選表清單

- **卡**：T-1.6（CM-2516）
- **依據**：`design.md` 第 6.4 節（policy 形狀、session 變數）、D12
- **資料來源**：DEV `localhost:5432/guidant_ai_dev`（2026-10-06 實查 `pg_policy`、`pg_constraint`、`information_schema`）；子表 API 由 `api/` 與 jedi 套件 `api/routing.py` grep；證據、檢測、問卷三組的子表觀察引用 `a1-detection-evidence-survey.md` 第 384–394 行並逐條核實
- **用途**：A.2 寫 D12 policy migration（T-3.6）的輸入

## 先看這幾點

1. **六張採用、兩張排除。** 採用：`compliance.projects`、`public.devices`、`compliance.evidence_batches`、`compliance.detection_executions`、`survey.surveys`、`compliance.information_systems`。排除：`public.bulletins`（首腦已裁）、`compliance.module_frames`（**本棒建議改排除，待首腦裁**，理由見第 8 列）。
2. **專案底下十二張子表不用動。** 它們的 SELECT policy 全是 `EXISTS (SELECT 1 FROM compliance.projects p WHERE p.id = 子表.project_id)`，父表加條件就自動跟著收窄。問卷的子表同理，鏈到 `survey.surveys`。
3. **證據、檢測兩組的子表要一起改。** 子表各自只用 `tenant_id` 判，不透過父表；而且有 API 直接以子表 uid 定址。
4. **兩張表的 policy 是 `FOR ALL`**（`detection_executions`、`detection_execution_groups`），改成 D12 形狀時連 INSERT 也會被套上同一個條件。建議 migration 拆成 SELECT／UPDATE／DELETE 三條加原本的 INSERT 條件。
5. **「租戶指派」不能用 `org_unit_id IS NULL` 判。** DEV 的 `user_roles` 有 141 筆 `scope='tenant'`，其中 140 筆的 `org_unit_id` 有值。T-3.5／T-3.6 判「這筆是租戶指派」要看 `scope` 欄，不是看部門欄空不空，否則 140 筆租戶指派會被誤當成部門指派、範圍被收窄。
6. **前置條件在 DEV 成立**：`org_units` 10 列 `path` 全有值、全符合 `/id/…/` 格式；採用的六張表 `org_unit_id` 全數有值（`module_frames` 164 列中 52 列為空）。

## 逐表結論

既有 policy 形狀簡寫：**T** ＝ `app.is_super_admin` 為真 或 `app_tenant_allowed_for_session(tenant_id)`；**E(父)** ＝ `app.is_super_admin` 為真 或 `EXISTS (父表該列)`。

| 表 | 採用／排除 | org_unit_id | 父表 RLS 形狀 | 子表（鏈父表／獨立 tenant_id） | 直接定址子表的 API | 結論：要改哪幾張 policy |
|---|---|---|---|---|---|---|
| `compliance.projects` | 採用 | 有（1,218 列全有值） | select／update／delete＝T；insert＝`app_tenant_allowed_for_session` | **鏈父表 12 張**：`project_participants`、`project_control_participants`、`project_group_participants`、`control_group_participants`、`process_participants`、`project_assessment_plan_mapping`、`project_org_unit_mapping`、`project_system_characteristic_mapping`、`project_summary_reports`、`task_assignees`、`project_audit_rounds`、`review_marks`（後兩張無 FK，以 `project_id` 欄鏈） | 有：`/audit-round/<rid>/*` 約 20 支、`/project-summary-report/<uid>`、`/ssp/<ssp_uid>/*`、`/ap/<ap_uid>/*`、`/job-executions/<je>/comments`、`/flow-engine/task/complete/<id>` | `projects_select`、`projects_update`、`projects_delete` 三條。子表自動跟著收。SSP、AP 不在 RLS 內（oscal 表沒開 RLS），但 service 層反查專案時走 `projects`（`domain/oscal/service/ssp_project_resolver.py:93`），專案看不到就找不到；任務類見待裁第 2 條 |
| `public.devices` | 採用 | 有（9 列全有值） | select／update／delete＝T；insert＝租戶 且（`can_read_all_orgs` 或所屬部門） | 無直接子表。`job_execution_devices`（FK，未開 RLS，DEV 0 列）、`survey.task_surveys.device_id`（無 FK）只存 id | 無（`jedi_asset/api/routing.py` 全部以設備本身 uid 定址） | `devices_select`、`devices_update`、`devices_delete` 三條 |
| `compliance.evidence_batches` | 採用 | 有（20 列全有值） | select／update／delete＝T；insert＝T | **獨立 tenant_id 兩張**：`evidence_batch_files`、`evidence_classification_runs`（兩張都有 `org_unit_id`；DEV 與父表一致 193／25 列；`runs` 另有 8 列 `batch_id` 為空，部門欄仍有值） | 有：`/evidence-batches/<b>/files*`（寫）、`/classification-run/<r>/state`、`/classification-run/<r>/file/<f>/preview`、`/classification-run/<r>/report/*`（`jedi_evidence_classification/api/routing.py:40-52`） | 父表三條＋`evidence_batch_files_select／update／delete`＋`evidence_classification_runs_select／update／delete`，共 9 條。子表用**自己的** `org_unit_id` 判，不改成 `EXISTS (父表)`：`runs` 有 8 列沒有父批次，鏈父表會讓它們整批消失 |
| `compliance.detection_executions` | 採用 | 有（76 列全有值） | **單一 `FOR ALL` policy**＝T（`detection_executions_tenant_isolation`） | **獨立 tenant_id 一張**：`detection_execution_groups`（群組是上層，執行以 `group_uid` 掛在群組下；有 `org_unit_id`，DEV 與任務一致）。兩表無 FK | 有：`/detection-tools/execution-groups/<g>`、`.../<g>/cancel`、`.../<g>/assignments/<a>/rerun｜cancel｜start-now`、`/detection-tools/executions/<uid>`、`/detection-tools/jobs/<job_uid>/executions`（`jedi_detection/api/routing.py:124-162`） | `detection_executions_tenant_isolation`、`detection_execution_groups_tenant_isolation` 兩條 `FOR ALL`，建議各拆成 SELECT／UPDATE／DELETE 三條（套 D12）＋一條 INSERT（維持 T） |
| `survey.surveys` | 採用 | 有（52 列全有值，含 30 列 `is_snapshot` 快照） | select／update／delete＝T；insert＝租戶 且（`can_read_all_orgs` 或所屬部門） | **鏈父表**：`survey_pages`、`surveys_trans`、`survey_discussions`→`surveys`；`survey_questions`→`survey_pages`；`question_answers`→`survey_questions`→`survey_pages`。**另一條線**：`task_surveys`→`compliance.workflow_executions`；`question_answer_histories`→`task_surveys` | 有：`/survey-page/<uid>`、`/survey-question/<uid>`、`/survey-discussion/<uid>`、`/project-survey/answers/<uid>*`、`/task-survey/<uid>`（`jedi_survey`） | `surveys_select`、`surveys_update`、`surveys_delete` 三條。題目、頁面、討論、答案自動跟著收。**受訪者填答會受影響**，見待裁第 3 條 |
| `compliance.information_systems` | 採用 | 有（96 列全有值） | select／update／delete／insert 各一條＝T（本表 `FORCE ROW LEVEL SECURITY`） | 無 FK 子表。專案 SSP 的 `oscal.ssp_components` 以 `props.system_characteristic_id` 軟參照 id | 無（`jedi_asset/api/routing.py` 四條皆以資訊系統本身定址） | `information_systems_select`、`information_systems_update`、`information_systems_delete` 三條 |
| `public.bulletins` | 排除（首腦已裁） | 有（3 列） | select／update／delete＝T；insert＝租戶 且所屬部門 | `bulletin_org_units`（投遞對象，未開 RLS） | — | 不改。公告已有「投遞給哪些部門」的規則，再疊部門隔離，「發給全公司的公告」會被發文部門的隔離擋住，兩套規則互相打架 |
| `compliance.module_frames` | **首腦初裁採用；本棒建議改排除，待首腦裁** | 有（164 列中 112 列有值，52 列為空） | select＝`scope='SYSTEM'` 或 T 或（`scope='SHARED'` 且祖先租戶）；update／delete＝T（排除 SYSTEM） | `module_frames_trans`（FK，未開 RLS）；查核項目 BPMN 在 `compliance.workflow_templates`（有 `org_unit_id`、自己的 scope policy，不鏈 `module_frames`） | 有：`/module-frame/item/<uid>`、`/module-frame/item/xml/<uid>` 直接以 `workflow_templates` uid 讀（`app/module_frame/service/module_frame_item_service.py:31`）；`/flow-engine/process-definition/<uid>` 同 | 若採用：`module_frames_select／update／delete` 三條，而且 `workflow_templates` 也要一起改，否則查核項目內容仍可直接讀到。建議排除，理由見下 |

**`module_frames` 建議排除的理由**：

1. 決策者已在 `a1-oscal-flow.md` 定案第 10 條裁定「資源庫範圍層 tenant」，該份盤點全部資源庫端點的範圍層都標 tenant。資源庫是全租戶共用的範本，再加部門隔離與已裁結論衝突。
2. DEV 有 26 個專案引用了「部門不同」的資源庫（`projects.module_frame_id` 指向 `org_unit_id` 不同的 `module_frames` 列）。開隔離後，這些專案的規劃頁讀不到自己用的資源庫，且 RLS 擋掉不報錯。
3. 要隔離得完整，還要連 `workflow_templates` 一起改，維護面多一組 SYSTEM／SHARED 三種 scope 的交叉判斷。

**`bulletins` 與 `module_frames` 以外未列入的表**：解析任務（`oscal.*_parse_jobs` 四張，有 `org_unit_id`）、設定表、工作紀錄照 design 6.4 不挑。

## 隔離開關開啟時要動的 policy 清單

給 T-3.6 寫 migration。每條改成 design 6.4 的形狀：**既有條件 且（`app.org_unit_isolation` 不為真 或 `app.can_read_all_orgs` 為真 或 該列 `org_unit_id` 為空 或 該列部門 path 落在 `app.allowed_org_unit_paths` 子樹內）**；`app.is_super_admin` 那段 OR 保留在最外層。

| # | 表 | policy | 指令 | 備註 |
|---|---|---|---|---|
| 1 | `compliance.projects` | `projects_select` | SELECT | |
| 2 | 同上 | `projects_update` | UPDATE | USING 與 WITH CHECK 都要改 |
| 3 | 同上 | `projects_delete` | DELETE | |
| 4 | `public.devices` | `devices_select` | SELECT | |
| 5 | 同上 | `devices_update` | UPDATE | |
| 6 | 同上 | `devices_delete` | DELETE | |
| 7 | `compliance.evidence_batches` | `evidence_batches_select` | SELECT | |
| 8 | 同上 | `evidence_batches_update` | UPDATE | |
| 9 | 同上 | `evidence_batches_delete` | DELETE | |
| 10 | `compliance.evidence_batch_files` | `evidence_batch_files_select` | SELECT | 子表用自己的 `org_unit_id` |
| 11 | 同上 | `evidence_batch_files_update` | UPDATE | |
| 12 | 同上 | `evidence_batch_files_delete` | DELETE | |
| 13 | `compliance.evidence_classification_runs` | `evidence_classification_runs_select` | SELECT | 子表用自己的 `org_unit_id`；有 8 列無父批次 |
| 14 | 同上 | `evidence_classification_runs_update` | UPDATE | |
| 15 | 同上 | `evidence_classification_runs_delete` | DELETE | |
| 16 | `compliance.detection_executions` | `detection_executions_tenant_isolation` | ALL | 建議拆 SELECT／UPDATE／DELETE＋INSERT |
| 17 | `compliance.detection_execution_groups` | `detection_execution_groups_tenant_isolation` | ALL | 同上 |
| 18 | `survey.surveys` | `surveys_select` | SELECT | 子表鏈父表，自動跟著收 |
| 19 | 同上 | `surveys_update` | UPDATE | |
| 20 | 同上 | `surveys_delete` | DELETE | |
| 21 | `compliance.information_systems` | `information_systems_select` | SELECT | |
| 22 | 同上 | `information_systems_update` | UPDATE | |
| 23 | 同上 | `information_systems_delete` | DELETE | |

合計 9 張表、23 條 policy（其中第 16、17 兩條拆開後實際變成 8 條）。INSERT policy 一律不動：既有 INSERT 看的是使用者**所屬部門**（`app.allowed_org_paths`），與 D12 的**指派部門**不同，design 6.4 已定不變。

若首腦裁定保留 `module_frames`，追加：`module_frames_select`、`module_frames_update`、`module_frames_delete`，以及 `compliance.workflow_templates` 的 SELECT／UPDATE／DELETE（先查該表 policy 名稱）。

若首腦裁定任務也要隔離（待裁第 2 條），追加：`compliance.workflow_executions`、`compliance.job_executions` 各自的 select／update／delete（兩表皆有 `org_unit_id`，DEV 與所屬專案 100% 一致）。

## 裁示（2026-10-06）

**決策者：這一版部門資料隔離不擋任何資料。** 不改任何 RLS policy、不做隔離開關與預覽頁、不做跨部門成員放行。本檔與 design §6.4 留作下一版開 D12 時的輸入；下面五條在那時一併定。延後的卡見 `a1-capability-tree.md` 附錄 D 末段。

**首腦已裁（下一版直接沿用）**：`module_frames` 排除（資源庫已裁 tenant 層、DEV 有 26 個跨部門引用）；任務表 `workflow_executions`／`job_executions` 要一起隔離（否則看不到專案卻看得到它的任務）；「租戶指派」一律以 `user_roles.scope='tenant'` 判，不看 `org_unit_id` 空不空，design 6.3 措辭要改。

**下一版開 D12 前要定的**：

1. **`module_frames` 改排除？** 建議排除。理由見上表下方三點：資源庫範圍層已裁 tenant、DEV 有 26 個專案跨部門引用資源庫、要完整隔離還得連 `workflow_templates` 一起改。
2. **任務（`workflow_executions`／`job_executions`）要不要一起隔離？** 建議一起加。這兩張表與專案沒有 FK，policy 只看 `tenant_id`，「我的任務」這類直接查任務表的列表在專案被隔離後仍會列出別部門專案的任務；以任務 uid 定址的端點（提交、退回、留言）有 service 層專案成員檢查，但列表類沒有。兩表 `org_unit_id` 全有值且與專案一致，加進來成本是 6 條 policy。不加的代價是「看不到專案，卻看得到它的任務」。design 6.4 寫「不挑子表」，這兩張邏輯上是專案的子表、實際上不鏈父表，所以需要首腦明裁。
3. **跨部門的專案成員與受訪者會被擋。** 用 DEV 現有指派粗估：3,634 筆專案成員裡有 333 筆，本人既沒有租戶指派、也沒有涵蓋該專案部門的部門指派，開隔離後會看不到自己參與的專案。問卷同理：任務問卷的題目鏈到 `survey.surveys`，問卷快照的部門＝專案部門，別部門的受訪者會讀不到題目，且不報錯。建議：policy 另加一條放行路徑「本人是該專案的成員」（專案表與問卷快照各一），否則開隔離的客戶要替每個跨部門成員另開部門指派。此條影響 policy 形狀，需在 T-3.6 開工前定。
4. **「租戶指派」的判法**：DEV `user_roles.scope='tenant'` 的 141 筆中 140 筆 `org_unit_id` 有值。建議 T-3.5／T-3.6 一律以 `scope` 欄判，design 6.3「租戶指派為空」的描述要改寫成「`scope='tenant'`」。
5. **軟參照的名稱會靜默消失。** 專案 SSP 的設備、資訊系統以 id 軟參照（`oscal.ssp_components.props`），讀取時再查主表補名稱（`app/oscal/service/ssp_resources_context_service.py:126-158`，查不到吞例外回空）。專案引用了別部門的設備或資訊系統時，開隔離後 SSP 頁顯示的名稱會變空白。DEV 目前沒有這種跨部門引用可驗。是否接受，或改成查名稱時走系統放行旗標，請首腦裁。
