# FR-048 Phase 2a — 全站端點授權矩陣

> **性質**：純盤點（零 code 變更）。全站 `api/` 寫入端點（POST/PUT/DELETE/PATCH）+ 敏感 GET（匯出/下載/報表/qrcode/token）逐支判軸，供 user 過目定調。
> **上游設計**：[`../../analysis/2026-07-07-unified-auth-guard-design.md`](../../analysis/2026-07-07-unified-auth-guard-design.md)（§2 軸模型、§3.2 決策表、§5.0 scope、§7 phase）。
> **交叉驗證**：[`../../analysis/2026-07-07-known-pits-remediation-tracker.md`](../../analysis/2026-07-07-known-pits-remediation-tracker.md) §SEC 六族 75 條。
> **建立**：2026-07-07，branch `fix/v1.8.0-bugs`。**逐模組事實底稿在 [`_partials/`](_partials/)**（route/method/呼叫 service/守門 file:line 皆已實查引用）。
> **盤點法**：8 個 subagent 按模組分工，追 route → app service body 找守門呼叫；主 session 判軸 + 交叉驗證。

---

## 圖例

**軸別**（設計 §2）：
- **①platform** — root tenant 平台管理員（純 context，`require_platform_admin`）。全域共用資源（框架、平台設定、LOG）。
- **②super-admin** — 帳號層 super_admin（純 context，`require_super_admin`）。租戶級系統設定 / 範本 / 管理頁；Phase 3 過渡、Phase 4 換 capability。
- **③project-role** — 專案/SSP/workflow 成員資格（resolve 資源，`assert_project_manager` / `SspPermissionChecker` / workflow guard）。
- **④capability** — RBAC 能力點（Phase 4，尚未實作）。
- **⑤signed-token** — 簽章短期憑證（Phase 4，瀏覽器原生 GET）。
- **owner** — 資源擁有者個案（D 族，逐案）。
- **F** — 刻意不擋（health / OAuth callback / 公開登入流程 / FR-039 控制面）。

**狀態**：
- `已守` — 已有正確守門，本案不動。
- `待補` — 軸別明確、目前裸奔，待掛守門（Phase 2b/3/4）。
- `待定調` — 軸別或角色層級未定，**集中在 §1 問 user**。
- `no-action` — 刻意公開/自我 scope/低風險，確認即可。
- `dead` — route 未註冊（註解停用）或 service 為 stub，無 live 風險。

**Phase**：`2b`=軸③專案域補守門｜`3`=軸①②主體域 decorator｜`4`=軸④capability+軸⑤signed-token｜`—`=已守/no-action/dead。

---

## 0. 進度總覽

| 分類 | 端點數（約） | 說明 |
|------|------|------|
| **已守（no action）** | ~110 | participant 12、SSP-scoped 34、audit-round 家族 30、framework 家族 18、cloud_integration 5、其他個別 |
| **待補 軸③（Phase 2b）** | ~35 | grc job/review/comment/export、flow_engine 證據&任務（含停用 guard 復活）、project_summary_report、subtask_status_history、task_survey 填答、evidence_classification state |
| **待補 軸①②（Phase 3）** | ~55 | auth role/user/tenant/org CRUD、system_config、system_menu、device、bulletin、information_system、notification test、remote_agent 管理面、log export(軸①) |
| **待補 軸⑤ signed-token（Phase 4）** | 2（+新發現若干） | otp-qrcode、framework OSCAL download（+ uploadfile download/pdf-preview、report preview、task-survey import-template 待定調） |
| **待定調（§1）** | ~15 群 | 見下 |
| **no-action（刻意）** | ~20 | 公開登入流程、OAuth/webhook callback、health、FR-039 控制面、自我 scope（ai/chatbot） |
| **dead（未註冊/stub）** | ~30 | FR-038 DEAD-V1 註解 route、2A dark stub |
| **母數** | 373 寫入 method + 敏感 GET | 8 partial 逐支列出 |

> 「已守」大宗＝Phase 1 之前已鋪守門的三塊（participant / SSP / audit-round）＋framework。**裸奔集中在系統管理頁（軸②）與部分專案域寫入（軸③）**，與設計 §5.0 判斷一致。

---

## 1. 待定調清單 → **已全數定調（2026-07-07 user 拍板）**

> 判定規則：spec 權限矩陣有寫→照 spec；spec 沒寫 / FE-BE 粒度不一致 / 權限主體不明→集中在此，不自己猜。

**定調結果（全數同意建議案）**：

| # | 定調 |
|---|------|
| D1 | 軸②（Phase 3 掛 super-admin，Phase 4 換 `module_frame.manage`）。範本無 participant，tracker 歸 C 族係誤判 |
| D2 | 軸①platform（clone 全域框架三件組，同 framework 家族） |
| D3 | 軸②；直接採用既有錯誤碼 `GRC_FLOW_TEMPLATE_NO_PERMISSION`(403041) |
| D4 | 軸①platform（建租戶是平台操作） |
| D5 | 維持開放（by design，建立者自動 manager）；收緊等 Phase 4 `project.create` |
| D6 | 軸③ manager，粒度照 spec 權限矩陣對齊 |
| D7 | manager-only（job 建改刪＝規劃行為；執行/完成走 flow_engine 給 assignee） |
| D8 | manager（若 reviewer 角色實際在用再放寬為 manager+reviewer） |
| D9 | 軸②過渡 → Phase 4 `survey.manage` |
| D10 | 填答＝被指派者本人（assignee）；configure/import＝manager |
| D11 | enroll-token/revoke/rebind → 軸② admin；register/heartbeat 維持 F |
| D12 | 改刪＝owner 本人；export → 軸② admin |
| D13 | 軸②（租戶資產，不引入 owner） |
| D14 | file download/pdf-preview → 軸⑤ signed token（**動之前必查 FE 呼叫方式**，有前科）；schedule-report/preview → 補 JWT＋admin＋path 消毒（**最危險，插隊優先**）；task-survey import-template → 補 JWT（查 FE）；靜態範本、issue/get_members → 補 JWT |
| D15 | 直接移除 route（debug 殘留，hardcoded email 已實查確認 `user_route.py:164`） |

| # | 端點群 | 問題 | 傾向（供參考） | 對應 pit |
|---|--------|------|--------------|---------|
| **D1** | **module_frame 全模組**（範本 CRUD + defaults + 元件/資產/party，~43 支，全裸奔） | 範本（module_frame）管理權限主體是誰？範本無 project participant，`SspPermissionChecker` 對 template 分支是用帳號層 `is_admin`（軸②）。但 tracker 把 module-frame-defaults#5/#6 歸 **C 族(軸③)** — 兩者矛盾。 | 軸②super-admin 或 軸④capability(`module_frame.manage`)；**非**軸③（範本非專案） | module-frame-defaults#5/#6 |
| **D2** | **oscal resource-libraries**（POST create / publish） | 資源庫（clone framework_version 三件組）是平台級共用資產還是租戶級？ | 軸①platform（clone 全域框架）或 軸②租戶 | — |
| **D3** | **flow_engine flow-templates**（create/update/delete/duplicate/publish/unpublish，6 支） | 流程範本管理權限主體？現只有 `_guard_not_builtin`（資源守門，非角色）。 | 軸①platform 或 軸②（見 flow-template-manage spec） | — |
| **D4** | **auth tenant CRUD**（tenant POST/PUT/DELETE、`add_tenant_with_defaults`） | 建/改/刪租戶是平台操作還是租戶管理員操作？ | 軸①platform-admin（建租戶＝平台級） | tenant-manage#1/#4 |
| **D5** | **/oscal-project/start**（建專案） | 任何登入者可建專案（現況 by design，建立者補 manager）；要不要限制？ | 維持開放 或 軸④`project.create` | project-create#2 |
| **D6** | **planning-status#1** | FE/BE 權限粒度不對齊（繞 FE 權限面更寬）——BE 該收斂到哪個粒度？ | 軸③，粒度待對齊 spec | planning-status#1 |
| **D7** | **grc job 編輯**（job PUT/DELETE/create、batch-complete） | 專案任務編輯是 manager-only 還是被指派 auditor 也可寫？ | 軸③，角色層級待定 | project-task-edit#1/#2、planning-batch-ops#4 |
| **D8** | **grc review mark/unmark**（control/AO ×4） | 標記審查是 auditor / reviewer / manager 哪一層？ | 軸③，角色層級待定 | project-planning#4 |
| **D9** | **survey 設計/管理**（survey/folder/question/page/discussion CRUD，~20 支） | 問卷設計器/管理是租戶級 B 族還是有 owner 概念？ | 軸②super-admin 或 軸④`survey.manage` | survey-v2-designer#1、survey-v2-manage#1/#2 |
| **D10** | **task_survey 填答端點**（answers update/patch/checkpoint、history revert、configure） | 填答/設定是「被指派者本人」還是「任一專案 participant」？ | 軸③，assignee vs participant 待定 | survey-v2-fill#1/#2 |
| **D11** | **remote_agent FR-039 FE 端點**（create/update/delete/revoke/unrevoke/rebind/enroll-token gen+revoke） | 程式碼註解稱「需 admin」但全無守門；FR-039 demo 要維持開放，還是這些涉及 enroll-token（安全憑證）的要補 admin？ | enroll-token/revoke/rebind → 軸②admin；demo 控制面（register/heartbeat）維持 F | remote-agent-manage#4 |
| **D12** | **feedback 編輯/刪除**（PUT/DELETE、file DELETE、export） | 回饋編輯刪除是 owner 還是 admin？export（匯出全站 feedback）是否限 admin？ | 編輯刪除→owner；export→軸②admin | feedback-management#2/#5 |
| **D13** | **information_system / device 管理** | 資訊系統/設備是租戶管理員（軸②）還是有建立者 owner 概念？（device 連 owner 都沒擋） | 傾向軸②super-admin（租戶資產） | information-system-manage#1、device-manage#1/#6 |
| **D14** | **【新發現】未認證端點（不在 75 pit）** | 下列 route **完全無 `@jwt_required`**，處置待定（加 JWT / 軸⑤ signed-token / owner）：<br>• `GET /file/download/<uid>`、`GET /file/pdf-preview/<uid>`（uploadfile，任意 uid 下載）<br>• `GET /schedule-report/preview`（report，query 拼路徑→任意檔讀取風險）<br>• `GET /task-survey/import-template/<task_uid>`（洩該 task 題庫）<br>• `GET /survey/import-template`、`/module-frame/download/template`+`/yaml-template`（靜態範例，低風險）<br>• `GET /issue/get_members`（純讀裸奔） | download/preview→軸⑤或JWT+owner；靜態範本→補 JWT；issue→補 JWT | 無（新增） |
| **D15** | **【新發現】`POST /send-user-test-mail`** | 無 `@jwt_required`、寄測試信到 **hardcoded email**（`raymond.jedicotech@gmail.com`）；疑似 debug 殘留。 | 建議移除或補 JWT+admin | 無（新增） |

> **D1 是最大結構性問題**：module_frame 43 支全裸奔且軸別本身有爭議（tracker 軸③ vs 程式碼軸②），必須先定調才能規劃 Phase 2b/3 分工。

---

## 2. 交叉驗證：75 SEC pit → 矩陣對映

> tracker §SEC 六族 + 漏網 8 條，每條對映到矩陣模組/狀態。對不上的回頭補。

### A 族（未認證，3 條）
| pit | 對映端點 | 軸 | 狀態 |
|-----|---------|----|------|
| user-log#1 | `GET /log/api-logs/export` | 軸①（角色）+ 認證 | 認證已修（`182bb2f7`）；角色守門(SEC-002)＝軸① Phase 3 |
| compliance-framework-manage#5 | `GET /oscal-framework-version/download/oscal/<uid>` | 軸⑤ | ✅ 已修待驗（`40a7d9c5`，signed-token） |
| user-profile#3 | `GET /otp-qrcode/<uid>` | 軸⑤ | ✅ 已修待驗（`40a7d9c5`，signed-token＋自我限定 bind_uid） |

### B 族（系統設定僅 JWT，→ 軸②/④ Phase 3/4）
| pit | 對映模組/端點 | 狀態 |
|-----|--------------|------|
| ldap-config#1 | user_auth_provider `ldap/connect-test`、system_config LDAP group | 待補 軸② P3 |
| notify-channels#2 | notify_config test（**已有 admin 守門**）、system_config notify | 部分已守；config 寫入待補 P3 |
| smtp-config#6 | system_config SMTP group + notification `mail/test` | 待補 軸② P3 |
| issue-integrate-config#2/#6/#7 | system_config issue-integration group | 待補 軸② P3；#2 另＝D 族 token 明文（見下） |
| system-menu-manage#3 | system_menu POST/PUT/DELETE | 待補 軸② P3 |
| role-manage#1 | auth role POST/PUT/DELETE/status | 待補 軸② P3 |
| user-form#2、user-manage#1、user-import#1 | auth user POST/PUT/DELETE/change-status/batch-upload/upload | 待補 軸② P3 |
| tenant-manage#1/#4 | auth tenant CRUD | **待定調 D4**（軸①？） |
| department-manage#8 | auth org-unit CRUD | 待補 軸② P3 |
| device-manage#1/#6 | device POST/PUT/DELETE | **待定調 D13**（傾向軸② P3）；#6＝E 族(IP 格式 FE) |
| information-system-manage#1 | information_system POST/PUT/DELETE | **待定調 D13** |
| storage-config#3 | （storage-config 設定頁）→ system_config storage group | 待補 軸② P3 |
| background-services#1 | 見 F 族（設計如此） | no-action 候選 |
| _overview#2/#5 | system-admin 首頁彙總（多為讀）| _overview#5＝F 族 |

### C 族（GRC 專案寫入無角色守門，→ 軸③ Phase 2b）
| pit | 對映端點 | 狀態 |
|-----|---------|------|
| planning-basic-info#1 | grc `project/<uid>` PUT（update_project） | 待補 軸③ P2b |
| project-planning#4 | grc review mark/unmark | **待定調 D8** |
| project-task-edit#1/#2 | grc job PUT/create/DELETE | **待定調 D7**；#2 另＝E 族(審核人 FE) |
| planning-batch-ops#4 | grc `jobs/batch-complete` | **待定調 D7** |
| project-create#2 | `/oscal-project/start` | **待定調 D5** |
| project-ap-list#1 | grc `project/<uid>/ap/<ap_uid>` PUT（**dark stub**）| dead（2A stub，不做事）；DELETE 已守 |
| project-ssp-basic#1 | oscal `/ssp/<uid>/system-characteristic` PUT | **已守**（require_manager）→ pit 應已解；#1 另＝E 族(name 必填 FE) |
| project-ssp-components#2 | oscal `/ssp/<uid>/components` | **已守**（require_manager）→ pit 應已解 |
| project-ssp-inventory#1 | oscal `/ssp/<uid>/inventory-items` | **已守** → pit 應已解 |
| project-ssp-leveraged#5 | oscal `/ssp/<uid>/leveraged` | **已守** → pit 應已解 |
| project-ssp-parties#3 | oscal `/ssp/<uid>/parties` | **已守** → pit 應已解 |
| module-frame-defaults#5/#6 | module_frame defaults PUT/DELETE | **待定調 D1**（軸別爭議） |

> **交叉驗證發現**：C 族的 5 條 `project-ssp-*` pit 在實查中**已有 `require_manager` 守門**（SSP-scoped 34 支全數已守）——這些 pit 可能是 spec §12 撰寫時的舊觀察、之後已補。建議 user 定調時將其標為「已解，複驗即可」。真正待補的 C 族集中在 grc（job/review/update_project/batch）與 flow_engine。

### D 族（資訊洩漏，逐案）
| pit | 對映 | 狀態 |
|-----|------|------|
| issue-integrate-config#2 | system_config GET **secret 明文回傳**（實查確認 `jedi_system_config` 無 mask） | 待修（遮罩），逐案 |
| user-log#3（response 未遮罩）/ #4（api_logs 無 tenant 隔離） | log 模組 | 逐案（併軸① P3 時檢視） |
| forget-password#1（帳號列舉）、user-profile#2（讀他人完整資料） | auth | 逐案 |

### E 族（驗證只在 FE）
forget-password#2、device-manage#6、survey-v2-fill#1、project-task-edit#2、project-ssp-basic#1 — **非授權缺口，屬 FE 驗證**，本案不處理，維持 tracker E 族後排。

### F 族（刻意不擋 / no-action）
cloud-integrations#4/#2（OAuth callback state token / webhook channel token，實查確認）、my-audits#8（SQL 已鎖自己）、remote-agent-manage#4（FR-039 控制面 register/heartbeat）、evidence/_overview#6/#7（FR-039 demo）、ai-dashboard#6/#2（health 公開）、background-services#1、_overview#5、login#7/#3（captcha 順序 / 設計）→ **全數 no-action**，已於各模組矩陣標明理由（防下次重查）。

### 漏網 8 條
compliance-framework-manage#5（A）、user-profile#3（A）、information-system-manage#1（B/D13）、planning-status#1（D6）、user-profile#6（LDAP 綁定不驗本人，低風險→逐案）、user-manage#2（排序唯讀，非缺口）、ai-dashboard#2（F）、login#7（F）——**8 條全部對映到矩陣列**（無漏）。

---

## 3. 逐模組矩陣

> 每支端點一列。事實欄（現況守門/呼叫 service）源自 [`_partials/`](_partials/)，此處補「軸別/角色/Phase/狀態」判斷欄。**dead（未註冊/stub）僅列關鍵者**，完整 dead 清單見各 partial。

### 3.1 api/oscal（framework 已守軸①、SSP 已守軸③；缺口 3）

| Route path | Method | 現況守門 | 軸別 | 角色 | Phase | 狀態 | 備註 |
|---|---|---|---|---|---|---|---|
| /oscal-framework(+/versions/parse-jobs 全家族 CRUD/import/confirm) | POST/PUT/DELETE | require_platform_admin | ① | platform | — | 已守 | 18 支，framework_app/version/edit/parse |
| /oscal-framework-version/download/oscal/<uid> | GET | @signed_token_required | ⑤ | — | 4 | 已修待驗(40a7d9c5) | 發 token 端點 .../oscal/<uid>/token(@jwt)；FE window.open 帶 ?st= |
| /oscal/resource-libraries | POST | 僅 JWT | ①/② | — | 待定 | **待定調 D2** | clone framework_version |
| /oscal/resource-library/<uid>/publish | POST | 僅 JWT | ①/② | — | 待定 | **待定調 D2** | 狀態轉換 |
| /ssp-excel-imports、/ssp-docx-imports（parse/discard/confirm，6 支） | POST/DELETE | 條件式：ssp→require_project_manager；framework_version/module_frame→僅 tenant | ③(+②) | manager | 2b/待定 | 部分已守 | ssp 分支已守；非 ssp 分支軸別隨 **D1/D2** |
| /ssp/<uid>/excel-import/upload | POST | require_project_manager | ③ | manager | — | 已守 | |
| /ssp/<uid>/（system-characteristic/components/inventory/leveraged/parties/ssp-resources/document-pool/control-implementation/…，30+ 支寫入） | POST/PUT/DELETE | SspPermissionChecker.require_manager | ③ | manager | — | 已守 | **SSP-scoped 全數已守**；解 project-ssp-* pit |
| /ssp/<uid>/control-implementations/export | GET | require_participant | ③ | 參與者 | — | 已守 | 敏感 GET 已守 |
| /ssp/<uid>/control-implementations/import(+validate) | POST | require_participant | ③ | 參與者 | — | 已守 | 讀預覽 participant，confirm 為 manager |
| /ssp/<uid>/export | GET | require_participant | ③ | 參與者 | 2b | 已修待驗(99945a10) | export_from_ssp_version 補 require_participant；export_from_module_frame（軸②）不動 |

### 3.2 api/module_frame（全裸奔，軸別待定調 D1）

| Route path | Method | 現況守門 | 軸別 | 角色 | Phase | 狀態 | 備註 |
|---|---|---|---|---|---|---|---|
| /module-frame（CRUD）、/clone、/item(+xml)、/import(+yaml/verify) | POST/PUT/DELETE | 僅 JWT | ②/④? | — | 待定 | **待定調 D1** | 範本 CRUD ~13 支 |
| /module-frame/<uid>/control-defaults(+objective-defaults) | PUT/DELETE | 僅 JWT | ②/④? | — | 待定 | **待定調 D1** | module-frame-defaults#5/#6 |
| /module-frame/<uid>/(reference-documents/components/inventory/leveraged/parties/ssp-resources/system-characteristic) | POST/PUT/DELETE | 僅 JWT | ②/④? | — | 待定 | **待定調 D1** | 範本子結構 ~25 支 |
| /module-frame/<uid>/control-defaults/import(+template/verify/validate) | POST | 僅 JWT | ②/④? | — | 待定 | **待定調 D1** | |
| /module-frame/<uid>/control-defaults/export、/template/download、/ssp-import-template、/ssp/<uid>/excel-template、/ssp-import-template、/ssp-export | GET | 僅 JWT | ②/④? | — | 待定 | **待定調 D1** | 敏感 GET（含 MF/SSP 實際資料） |
| /module-frame/download/template、/download/yaml-template | GET | **無 JWT** | — | — | 待定 | **待定調 D14** | 靜態範例檔，低風險 |
| （ModuleFramesRoute/StartRoute） | POST | 僅 JWT | — | — | — | dead | __init__ 註解未註冊 |

### 3.3 api/project（audit-round 家族已守軸③；缺口＝建專案定調）

| Route path | Method | 現況守門 | 軸別 | 角色 | Phase | 狀態 | 備註 |
|---|---|---|---|---|---|---|---|
| /oscal-project/start | POST | 僅 JWT（建立者補 manager） | ④? | — | 待定 | **待定調 D5** | project-create#2 |
| /projects/<uid>/audit-rounds、launch-audit、close-round、launch-reverify | POST | _check_role(manager) | ③ | manager | — | 已守 | |
| start-auditing、launch-reverify、generate-draft、finalize、AR judge/observation/risk、AP set-* | POST/PUT | _check_role/_check_auditor（manager override） | ③ | auditor | — | 已守 | |
| ap/docx-imports、ar-imports（parse/confirm/discard） | POST/GET/DELETE | resolve_ap/round_and_check_auditor/participant | ③ | auditor/參與者 | — | 已守 | |
| poam remediation/milestone（新增/改/刪） | POST/PUT/DELETE | _check_manager | ③ | manager | — | 已守 | |
| audit-rounds/list、round/ap/findings/risks/poam-items（GET/list POST） | GET/POST | 僅 JWT | ③ | 參與者(讀) | 2b? | 讀取 | 專案域讀未驗參與者；RLS 擋租戶，低優先，可併 2b 順補 |

### 3.4 api/project_summary_report + subtask_status_history（軸③，缺口）

| Route path | Method | 現況守門 | 軸別 | 角色 | Phase | 狀態 | 備註 |
|---|---|---|---|---|---|---|---|
| /project-summary-report（create/update/delete） | POST/PUT/DELETE | 僅 JWT（**未查專案歸屬**） | ③ | 待定 | 2b | **待補** | project-summary-report#1；角色層級待 spec |
| /project-summary-report/export/<uid> | GET | 僅 JWT | ③ | 待定 | 2b | **待補** | 敏感 GET（PDF），無 ownership |
| /project-summary-report/history-revert | POST | 僅 JWT | ③ | 待定 | 2b | **待補** | |
| /subtask-status-history（POST/PUT/DELETE） | POST/PUT/DELETE | 僅 JWT | ③ | 待定 | 2b | **待補** | 需確認可反查專案輪次 |

### 3.5 api/grc（缺口集中；多項待定調）

| Route path | Method | 現況守門 | 軸別 | 角色 | Phase | 狀態 | 備註 |
|---|---|---|---|---|---|---|---|
| /projects/batch-delete、/project/<uid> DELETE | POST/DELETE | is_admin 或 owner/manager | ③ | manager | — | 已守 | |
| /project/<uid> PUT（update_project） | PUT | assert_project_manager | ③ | manager | 2b | 已修待驗(80514dff) | D6 planning-basic-info#1 |
| /project/<uid>/ap/<ap_uid> PUT | PUT | 僅 JWT（**dark stub**） | ③ | — | — | dead | 2A stub，不做事 |
| /project/<uid>/ap/<ap_uid> DELETE | DELETE | manager | ③ | manager | — | 已守 | |
| /job/<uid> PUT、DELETE、/assessment-object/<ao>/jobs POST | PUT/DELETE/POST | assert_project_manager | ③ | manager | 2b | 已修待驗(80514dff) | D7；delete_job 新增 project_uid 參數 |
| /jobs/batch-complete | POST | 僅 JWT | ③ | manager | 2b | **待補（handoff）** | D7；每 job 完成走 flow_engine complete_job，隨 guard 復活受守；自身 project 反查待復活波 |
| control/AO review mark/unmark（×4） | POST/DELETE | assert_project_manager | ③ | manager | 2b | 已修待驗(80514dff) | D8 project-planning#4 |
| job-execution comments POST | POST | 僅 JWT | ③ | 參與者? | 2b | **待補（handoff）** | 角色未定調（參與者 vs manager）；需 assert_project_participant canonical |
| job/comment PUT/DELETE | PUT/DELETE | 作者本人（owner） | owner | — | — | 已守 | owner-of-record |
| /jobs/export | GET | 僅 JWT | ③ | 參與者 | 2b | **待補（handoff）** | 敏感 GET；參與者級讀，需 assert_project_participant canonical（見 §7） |
| /jobs/import/confirm | POST | manager | ③ | manager | — | 已守 | validate/import 為讀 |
| /start-task-execution | POST | manager | ③ | manager | — | 已守 | |
| projects/list、jobs/list、jobs/my/list、audits/my/list | POST | 僅 JWT（可見性過濾/自我 scope） | ③ | — | — | 讀取 | POST 實為查詢 |

### 3.6 api/flow_engine（證據&任務守門停用待復活；範本待定調）

| Route path | Method | 現況守門 | 軸別 | 角色 | Phase | 狀態 | 備註 |
|---|---|---|---|---|---|---|---|
| /task/complete、/task/revert | POST | **已有(停用)** assert_workflow_project_participant（body `return`） | ③ | 參與者 | 2b | **待補（復活）** | FR-038 2B 重建 v2 反查鏈 |
| /job-evidence PUT、DELETE | PUT/DELETE | **已有(停用)** 同上 | ③ | 參與者 | 2b | **待補（復活）** | 停用期間僅 RLS |
| /job-evidences POST、/job-evidence POST | POST | 僅 JWT | ③ | 參與者 | 2b | **待補** | 證據新增裸奔 |
| /process/comments PUT、/job-execution/task/link PUT | PUT | 僅 JWT | ③ | 參與者? | 2b | 待補 | |
| /flow-templates（create/update/delete/duplicate/publish/unpublish） | POST/PUT/DELETE | 僅 JWT（僅 _guard_not_builtin 資源守門） | ①/② | — | 待定 | **待定調 D3** | 範本管理權限主體 |
| /stage/advance | POST | role gate（force 需 MANAGER） | ③ | 角色/manager | — | 已守 | |
| /flow-templates/validate | POST | 僅 JWT | — | — | — | 讀取 | 純驗證 |

### 3.7 api/auth + user_auth_provider + captcha（B 族軸②，tenant 待定調 D4）

| Route path | Method | 現況守門 | 軸別 | 角色 | Phase | 狀態 | 備註 |
|---|---|---|---|---|---|---|---|
| /user（POST/PUT/DELETE）、/user/change-status、/user/batch-upload、/users/upload | POST/PUT/DELETE | 僅 JWT（delegate jedi-auth，主專案無守門） | ② | admin | 3 | **待補** | user-manage#1/user-form#2/user-import#1 |
| /user-profile/<uid> PUT | PUT | route self-check（uid==本人） | owner | — | — | 已守 | 本人改自己 |
| /role（POST/PUT/DELETE）、/role/status | POST/PUT/DELETE | 僅 JWT | ② | admin | 3 | **待補** | role-manage#1 |
| /tenant（POST/PUT/DELETE）、add_tenant_with_defaults | POST/PUT/DELETE | 僅 JWT | ①? | — | 待定 | **待定調 D4** | tenant-manage#1/#4 |
| /org-unit（POST/PUT/DELETE） | POST/PUT/DELETE | 僅 JWT | ② | admin | 3 | **待補** | department-manage#8 |
| /user/download/sample | GET | 僅 JWT | ② | admin | 3 | 待補 | 敏感 GET（scoped 內容） |
| /otp-qrcode/<uid> | GET | @signed_token_required(bind_uid) | ⑤ | — | 4 | 已修待驗(40a7d9c5) | /otp-qrcode-token(@jwt) 發放、token 綁本人 uid＝只能拿自己 QR |
| /totp-secret | POST | 僅 JWT（get_user_context 綁本人） | owner | — | — | no-action | 取本人 TOTP secret |
| /send-user-test-mail | POST | **無 JWT**（hardcoded email） | — | — | 待定 | **待定調 D15** | 疑 debug 殘留 |
| /login、/refresh、/logout、/forget-password、/change-pwd-*、/otp-verify、/otp-resend、/verify-captcha | POST/GET | 無/refresh JWT | F | — | — | no-action | 公開登入流程 |
| user_auth_provider /auth-provider POST/DELETE | POST/DELETE | 僅 JWT（可指定任意 user_uid） | ②/owner | 待定 | 3 | 待補 | 應限本人或 admin |
| user_auth_provider /ldap/connect-test | POST | 僅 JWT | ② | admin | 3 | 待補 | ldap-config#1 |

### 3.8 api/survey + task_survey（survey 設計待定調 D9；填答待定調 D10）

| Route path | Method | 現況守門 | 軸別 | 角色 | Phase | 狀態 | 備註 |
|---|---|---|---|---|---|---|---|
| survey/folder/question/page/discussion（CRUD/reorder/move/clone/release/import，~20 支） | POST/PUT/DELETE | 僅 JWT（jedi_survey，全裸） | ②/④? | 待定 | 待定 | **待定調 D9** | survey-v2-designer/manage |
| /survey/import-template | GET | @signed_token_required | ⑤ | — | 4 | 已修待驗(40a7d9c5) | /survey/import-template/token(@jwt) 發放；BatchImportDialog 改帶 ?st= |
| task-survey update/configure/import-answers | PUT/POST | 僅 JWT（有專案 context 未驗） | ③ | 待定 | 待定 | **待定調 D10** | |
| project-survey answers（update/patch/checkpoint）、history/revert | PUT/POST | 僅 JWT | ③ | assignee? | 待定 | **待定調 D10** | survey-v2-fill#1/#2 |
| /task-survey/import-template/<task_uid> | GET | @jwt_required | — | — | 4 | 已修待驗(40a7d9c5) | 唯一 consumer 在 _archived＝dead，直接補 JWT 即可（4b 判定） |

### 3.9 api/participant + remote_agent（participant 已守；agent 待定調 D11）

| Route path | Method | 現況守門 | 軸別 | 角色 | Phase | 狀態 | 備註 |
|---|---|---|---|---|---|---|---|
| task-assignee、project-participant、project-control-participant、control-group-participant（POST/PUT/DELETE，12 支 live） | POST/PUT/DELETE | assert_project_manager | ③ | manager | — | 已守 | scope 各異（project/group/control）|
| task-assignees/batch | POST | 僅 JWT（**stub `return []`**） | ③ | — | — | dead | 2A stub |
| process-participant / project-group-participant（全組） | POST/PUT/DELETE | 部分無守門 | ③ | — | — | dead | __init__ 註解未註冊 |
| remote-agents（create/update/delete） | POST/PUT/DELETE | 僅 JWT | ② | admin? | 3 | **待定調 D11** | 註解稱需 admin |
| agents/enroll-token（gen/revoke）、agents/<uid>/(revoke/unrevoke/rebind) | POST | 僅 JWT | ② | admin? | 3 | **待定調 D11** | 安全憑證，優先定調 |
| agents/register、agents/heartbeat | POST | **無 JWT** | F | — | — | no-action | FR-039 控制面（mTLS/token） |

### 3.10 api/misc（cloud_integration 多已守；系統設定 B 族待補；新發現未認證）

| Route path | Method | 現況守門 | 軸別 | 角色 | Phase | 狀態 | 備註 |
|---|---|---|---|---|---|---|---|
| cloud_integration google-drive（disconnect/auth-url/retry/rebuild-all） | DELETE/POST | is_admin / super-admin | ② | admin | — | 已守 | |
| cloud_integration verify-and-repair-folders | POST | project manager | ③ | manager | — | 已守 | |
| cloud_integration init-folders | POST | 僅 JWT（刻意不擋 is_admin） | ③? | — | 待定 | 待定 | 註解稱避免擋 PM，但也無 PM 檢查；併 D13 檢視 |
| cloud_integration webhook/register | POST | 僅 JWT（doc 稱 admin only） | ② | admin | 3 | 待補 | |
| cloud_integration OAuth callback、webhook push | GET/POST | 無 JWT（state/channel token） | F | — | — | no-action | cloud-integrations#2/#4 |
| system_config（config CRUD、by-group/key） | POST/PUT/DELETE | 僅 JWT | ② | admin | 3 | **待補** | smtp/ldap/notify/issue B 族 |
| system_config GET（by-group/uid/key） | GET | 僅 JWT（**secret 明文**） | ② + D | admin | 3 | 待補 | issue-integrate-config#2（遮罩 D 族） |
| system_menu（POST/PUT/DELETE） | POST/PUT/DELETE | 僅 JWT | ② | admin | 3 | **待補** | system-menu-manage#3 |
| information_system（POST/PUT/DELETE） | POST/PUT/DELETE | 僅 JWT | ② | 待定 | 3 | **待定調 D13** | information-system-manage#1 |
| device（POST/PUT/DELETE） | POST/PUT/DELETE | 僅 JWT（無 owner） | ② | 待定 | 3 | **待定調 D13** | device-manage#1 |
| bulletin（POST/PUT/DELETE） | POST/PUT/DELETE | 僅 JWT | ② | admin | 3 | **待補** | bulletin-manage#4 |
| feedback（POST/PUT/DELETE/file/export） | POST/PUT/DELETE | 僅 JWT | owner/② | 待定 | 待定 | **待定調 D12** | feedback-management#2/#5 |
| evidence_classification trigger-classify | POST | project manager | ③ | manager | — | 已守 | |
| evidence_classification run/state、archive、ground-truth | PUT/POST | 僅 JWT（僅 tenant 解析） | ③/② | 待定 | 2b | 待補 | classify 專案域；GT 匯入或屬 admin |
| notification、mail/test | POST | 僅 JWT | ② | admin | 3 | 待補 | 測試發送 |
| notify_config/<channel>/test | POST | is_admin | ② | admin | — | 已守 | notify-channels#2 部分 |
| ai-chatbot（POST/DELETE）、ai-dashboard/auto-generate | POST/DELETE | 僅 JWT（自我 scope user.id） | F/self | — | — | no-action | |
| ai-dashboard/health | GET | 無 JWT | F | — | — | no-action | 刻意公開 |
| log/api-logs/export | GET | 僅 JWT（認證已修） | ① | platform | 3 | **待補** | SEC-002；LOG 能力 platform-only（軸①）|
| log/api-logs（list） | POST | 僅 JWT | ① | — | 3? | 讀取 | 隨 export 一併檢視角色 |
| uploadfile /file/download/<uid>、/pdf-preview/<uid> | GET | @signed_token_or_jwt（雙軌） | ⑤ | — | 4 | 已修待驗(40a7d9c5) | /file/access-token/<uid>(@jwt) 發放；原生呼叫帶 ?st=、既有 axios bearer 軌不動（TOKEN_401002 refresh 保留） |
| uploadfile /file/upload(+uploads)、/upload/<uid> DELETE | POST/DELETE | 僅 JWT（delete 無 owner） | owner | — | 待定 | 待定 | delete 應限 owner |
| report /schedule-report/preview | GET | **無 JWT**（query 拼路徑） | ⑤/② | — | 待定 | **待定調 D14** | 任意檔讀取風險（新發現）|
| report /schedule-report/file-list | POST | 僅 JWT | ② | — | 3? | 讀取 | 檔案清單 |
| issue /get_members | GET | **無 JWT** | — | — | 待定 | **待定調 D14** | 純讀裸奔（新發現）|
| translate、label | POST/GET | 僅 JWT/查詢 | — | — | — | no-action | 非寫入/非敏感 |

---

## 4. 「已守」端點整併政策（2026-07-07 user 拍板：要整合，行為不動、住址收編，優先級在補洞之後）

「已守」~110 支不是同一套寫法，分三類處理：

| 類 | 內容 | 處置 | 時機 |
|----|------|------|------|
| **A. 已用 canonical** | participant 12 / SSP 34 / framework 18（用 `assert_project_manager` / `SspPermissionChecker` / `require_platform_admin`） | 只把 import 從舊 shim 換 `common.authz` 新路徑，零行為變更 | Phase 2b 順手 |
| **B. 私有 helper 複製 7 份** | audit-round 家族 ~30 支：`_check_role`/`_check_manager`/`_check_auditor` 散在 `app/grc/service/` 7 個 service（audit_service.py:27、poam_app_service.py:91、audit_round_app_service.py:99、job_import_service.py:81、task_execution_service.py:47、assessment_plan_app_service.py:230、assessment_result_app_service.py:111）——未登記的「第 5 套」，但行為正確 | ✅ **已收編**（Phase 2c，見 §7.5）：`common/authz/project.py` 泛化 `assert_project_role(allowed_roles=...)`，7 份 `_check_*` 改為薄殼委派（call site 全不動、行為保真）；複製邏輯已刪 | **Phase 2c 已完成** |
| **C. inline `is_admin` 散用 22 處** | cloud_integration / notify_config / flow_engine stage 等直接讀 `ctx.is_admin`——兩個 is_admin 地雷的散彈面 | 換 `require_super_admin()` / `viewer_is_super_admin()`（同為帳號層語意，零行為變更），消滅語意混用 | Phase 3 順手 |

## 5. 下一步

1. ~~user 逐項定調 §1~~ ✅ 已定調（見 §1 定調結果）。
2. **D14 schedule-report/preview（任意檔讀取）插隊優先處理**。
3. Phase 2b（軸③補守門＋A 類換路徑，含 flow_engine guard 復活）與 Phase 3（軸①② decorator＋C 類 inline is_admin 收編）並行推進，逐列銷帳。
4. Phase 2c（B 類收編：assert_project_role 泛化＋7 份 _check_* 委派）排 2b 收尾後。
5. §3 標「待補」者為 Phase 2b/3 工作面；「已守」複驗即可；「dead」不動。

> **本檔為 living tracker**：Phase 2b/3/4 落地時逐列更新狀態欄（待補→已守）。

---

## 6. Phase 3 落地紀錄（2026-07-07，branch `fix/v1.8.0-bugs`）

主體域 route decorator 建置 + 掛點完成。**decorator 本體**：`common/authz/decorators.py`
（`require_platform_admin_route` / `require_super_admin_route`，後者支援 `error_code=` 覆寫），
委派 `platform.py` / `admin.py` 純 context 判定，掛在 `@jwt_required()` 之內、`@inject` 之外。
單元測試 `test/test_authz_decorators.py`（7 案：raise/pass/fail-closed/custom-code 全綠）。
`pytest test/` 對比 baseline **零新增失敗**（既有 71 fail / 43 err 全為 stale/env，與本案無關）。

**掛點統計**：軸① platform 7 支、軸② super-admin 109 支（含 flow-template 6 支用 alias），
共 **116 支**，分佈 16 模組。

### 6.1 插隊件

| 端點 | 處置 | 備註 |
|------|------|------|
| `GET /schedule-report/preview`（D14） | 補 `@jwt_required` + `@require_super_admin_route` + `_safe_report_path()` path 消毒（擋 `..`/絕對路徑/分隔符 + realpath 容器檢查） | FE 唯一呼叫方在 `_archived`（2026-03-31 停用、路由已移除），無 live consumer；同時把 `file-list` 的 path 併入 `_safe_report_path` 硬化。新增 error code `REPORT_400001` |
| `POST /send-user-test-mail`（D15） | 直接移除 route（class + `__init__` 兩處註冊） | debug 殘留 hardcoded email |

### 6.2 軸① platform-admin（`@require_platform_admin_route`，7 支）

| 模組 | 端點 | 定調 |
|------|------|------|
| log | `POST /log/api-logs`、`GET /log/api-logs/export` | SEC-002 首發，LOG 能力 platform-only |
| auth | `POST/PUT/DELETE /tenant`（3 支，含 `add_tenant_with_defaults`） | D4 建租戶＝平台級 |
| oscal | `POST /oscal/resource-libraries`、`POST /oscal/resource-library/<uid>/publish` | D2 clone 全域框架三件組 |

### 6.3 軸② super-admin 過渡名單 → **✅ Phase 4a 已換能力點（軸④）**

> ~~掛 `@require_super_admin_route` 皆為過渡；Phase 4 能力點 seed 齊後逐支換~~
> **2026-07-07 Phase 4a 完成換裝**（見 §8）。策略（user 定調）＝**復用既有 granular cap、精準對
> HTTP verb**（POST→`<res>.create`／PUT→`<res>.update`／DELETE→`<res>.delete`／敏感 GET→`.read`），
> resource 用模組既有 DB `resource_type`；只 3 組缺口新 seed（system_config／feedback.export／
> notification.test）。下表「建議能力點」欄的 `.manage` 命名為原始草案，實作採 granular verb 映射。

| 模組 / 端點群 | 支數 | 建議能力點 |
|--------------|------|-----------|
| auth user CRUD + change-status + import（upload/validate/batch-upload/sample-download） | 8 | `user.manage`（read/create/update/delete） |
| auth role CRUD + status | 4 | `role.manage` |
| auth org-unit CRUD | 3 | `org_unit.manage` |
| system_config CRUD（by-uid + by-group/key，含 SMTP/LDAP/notify/issue group） | 5 | `system_config.manage` |
| system_menu CRUD | 3 | `system_menu.manage` |
| device CRUD | 3 | `device.manage`（D13 租戶資產） |
| information_system create/update/delete | 3 | `information_system.manage`（D13） |
| bulletin CRUD | 3 | `bulletin.manage` |
| notification `mail/test` + `notification` 測試發送 | 2 | `notification.test` |
| user_auth_provider `ldap/connect-test` | 1 | `auth_provider.manage`（ldap-config#1） |
| remote_agent 管理面（create/update/delete/health/reconcile/enroll-token gen+revoke/revoke/unrevoke/rebind） | 10 | `remote_agent.manage`（D11；register/heartbeat 維持 F） |
| feedback `export` | 1 | `feedback.export`（D12；編輯/刪除＝owner，另案） |
| module_frame 全模組寫入（CRUD/clone/import/defaults/元件/資產/party/reference/inventory/leveraged/ssp-resources/system-characteristic） | 44 | `module_frame.manage`（D1；Phase 4 換此點） |
| survey 設計/管理（survey/folder/question/page/discussion CRUD + reorder/move/clone/release/import） | 24 | `survey.manage`（D9） |
| flow_engine flow-templates（create/update/delete/duplicate/publish/unpublish） | 6 | `flow_template.manage`（D3；錯誤碼沿用 `GRC_FLOW_TEMPLATE_NO_PERMISSION`(403041)，非預設碼） |

### 6.4 C 類 inline is_admin 收編（帳號層語意，零行為變更）

**已收編（14 處，全為 ambient `get_user_context().is_admin` 請求路徑讀取 → `common.authz` canonical）**：
- `notify_config_test_service.test_channel`：inline gate → `require_super_admin(NOTIFY_CHANNEL_TEST_FORBIDDEN)`（測試 patch 路徑同步改指 `common.authz.admin.get_user_context`）
- cloud_integration route：`google_drive_sync_route`（3 param 讀 + webhook/register inline gate `require_super_admin()`）、`google_drive_integration_route`（disconnect/auth-url 2 param 讀）→ `viewer_is_super_admin()`
- grc route：`project_route`（4）、`job_route`（1）、`job_import_route`（1）、`user_route`（1，update user）→ `viewer_is_super_admin()`

**刻意不收（scope 邊界）**：
- **import service 的 `user_context.is_admin`（ap_docx/ar/ssp_excel/ssp_docx 共 8 處）**：`user_context` 是**顯式參數**（route 傳入），非 ambient；已是正確帳號層 DTO 讀取。改成 `viewer_is_super_admin()`（讀 ambient ContextVar）會**回退**成 ambient 依賴、破壞可測試性與 worker-thread 安全 → 維持顯式參數形式。
- **`common/decorator/common_decorator.py:superuser_required`**：讀 legacy Flask `session`（非 UserContextDTO），且全站無 caller（dead）→ 不動。

### 6.5 本波未處理（列出回報，非默默略過）

| 項目 | 原因 / 後續 |
|------|-----------|
| module_frame 敏感 GET（control-defaults/export、ssp-export、ssp-import-template 含實際資料）→ 軸② | D1 有涵蓋，但為 GET；若 FE 以 `window.open`/iframe 原生呼叫，盲加守門有 A 族回歸前科 → 需先查 FE 呼叫方式再掛，留待 GET 專項（同 D14 方法論） |
| module_frame/survey 靜態範本 GET（download/template、yaml-template、survey import-template，D14 無 JWT） | 靜態範例檔、低風險；補 JWT 需查 FE，留 GET 專項 |
| user_auth_provider `/auth-provider` POST/DELETE | 矩陣標 ②/owner **待定**（本人 or admin 未拍板）→ 跳過 |
| 軸③ / 2b / 2c 全部 | 非本波（Phase 3 只做主體域軸①②） |

---

## 7. Phase 2b 落地紀錄（2026-07-07，branch `fix/v1.8.0-bugs`）

軸③ 資源域守門，一律 app service 層、`common.authz` canonical import。

### 7.1 已完成（已修待驗）

| 端點群 | 守門 | 角色 | commit |
|--------|------|------|--------|
| §4 A 類：participant 5 + SSP 11 + framework 4 service + oscal DI + ui_route | import 換 `common.authz.*`（零行為變更） | — | `cb558280` |
| `/ssp/<uid>/export`（export_from_ssp_version） | `SspPermissionChecker.require_participant` | 參與者 | `99945a10` |
| grc `update_project`（PUT project） | `assert_project_manager` | manager | `80514dff` |
| grc `create_job` / `update_job` / `delete_job` | `assert_project_manager`（走 `resolve_project_id`；delete_job 新增 project_uid 參數） | manager | `80514dff` |
| grc review `mark/unmark control+ao` ×4 | `assert_project_manager`（走 `project_domain_service.get_by_uid`） | manager | `80514dff` |

單測：`test_fr048_ssp_export_guard`（2）、`test_fr048_grc_guards`（4）。`pytest test/` 對比 baseline **零新增失敗**（76 fail / 41 err 全為既有 stale/env；passed 662→675＝本波 6 + Phase 3 的 7）。DI 全 boot 驗證受限於本 sandbox 缺 GITLAB/GITHUB token（jedi_issue import 期實例化，Phase 3 已記同狀況）；wiring 以嚴格 inspection 確認（`participant_role_service` / `associations_container.project_domain_service` 皆既有 provider）。

### 7.2 留待下一波（列出回報，非默默略過）

| 端點群 | 原因 | 建議 |
|--------|------|------|
| **flow_engine 守門復活**（task/complete、revert、job-evidence PUT/DELETE/POST、process/comments、task/link；`assert_workflow_project_participant` 開頭 `return`） | 需以 v2 反查鏈（workflow_execution→輪次/SSP→project，走 `task_assignees.project_id`）重建；v1 AP-task mapping 已 dark。量大、屬 FR-038 2B 排程，prompt 明示可拆下 session。 | **下一波壓軸**；判定沿用任一參與者、fail-closed、admin 放行；不另立新軸 |
| **grc `jobs/batch-complete`** | 每 job 完成實走 flow_engine `complete_job`，隨上項 guard 復活即受守；自身 project 反查（job_uids 可跨專案）待復活波一併處理 | 併 flow_engine 波 |
| **grc `jobs/export`、`job-execution comments POST`** | 角色為「參與者」層級，但 `common.authz` 目前無 `assert_project_participant`（只有 manager / SSP）；comments 角色（參與者 vs manager）未定調 | 先於 `common/authz/project.py` 增 `assert_project_participant`（design §2.1「任一參與者」屬軸③），再逐支掛 |
| **project_summary_report（create/update/delete/export/history-revert）、subtask_status_history（POST/PUT/DELETE）** | spec 明載「**無角色細分**」＝參與者-成員資格層級（非 manager）；缺口是「未驗專案歸屬」而非角色。同樣需 `assert_project_participant` canonical；且 update/delete/export 依 uid，需先反查 report/subtask→project | 同上，補 `assert_project_participant` 後掛；update/delete 需 domain service 提供 report→project 反查 |
| **evidence_classification `run/state`、`archive`、`ground-truth import`** | 矩陣標 軸③/② **待定**；classify 已守 manager，但 run-scoped 端點目前僅 tenant 解析，ground-truth import 可能屬 admin（軸②）。非 D 定調行 | 待 user 定調 run-scoped 角色（專案參與者 vs tenant admin） |

> **關鍵待辦**：`common/authz/project.py` 增 `assert_project_participant`（任一參與者放行）＝解鎖 jobs/export、comments、project_summary_report、subtask 四組的前置。這是小 canonical 補強（design §2.1 已認可「任一參與者」為軸③一支），非新軸。

---

## 7.3 Phase 2b-2 落地紀錄（2026-07-07，branch `fix/v1.8.0-bugs`）

完成 2b 交棒單剩餘 + 跨 phase 縫隙。守門一律 `common.authz` canonical。

### 已完成（已修待驗）

| 端點群 | 守門 | 角色 | commit |
|--------|------|------|--------|
| `common/authz/project.py` 增 **`assert_project_participant`**（任一參與者，登決策表） | — | — | `6d5deac8` |
| evidence_classification `put_state` / `archive_run`（run_folder→專案）+ `ground-truth import`（租戶任一專案 manager） | manager（模組 EC_NOT_MANAGER）| manager | `a68c1c82` |
| grc `/jobs/export` | `assert_project_participant` | 參與者 | `ed5eb47d` |
| **flow_engine 守門復活**：task/complete、revert、job-evidence PUT/DELETE（既有 wrapper 自動復活）+ job-evidence(s) POST、task/link PUT（加掛）；v2 反查鏈 `workflow_execution→USER job→task_assignees.project_id`（common.authz.workflow 收斂為政策層，解析器在 WorkflowExecutionService） | 任一參與者 | `0f50bbd0` |
| D14 補 JWT：`/module-frame/download/template`+`/yaml-template`、`/issue/get_members`（FE 實查安全） | JWT | — | `88d1050f` |

單測新增 16 案（participant 5 / evidence 5 / workflow 政策 6）全綠。**回歸驗證**：對 baseline `f1ea97c3` 比對 FAILED/ERROR 集合，`76 failed / 41 errors` 逐一相同（**零新增失敗**），passed 675→691＝本波 16 新測；無既有通過測試回歸。

### 留待下一波（列出回報，非默默略過）

| 端點群 | 原因 | 建議 |
|--------|------|------|
| **grc `job-execution comments POST`** | 角色未定調；且需 workflow_execution→project 反查（JobCommentService 無 workflow_execution_service 依賴，或改由 job_execution_uid 走已復活的 flow 鏈） | 定調角色後，注入 workflow guard 或 assert_project_participant |
| **project_summary_report（create/update/delete/export/history-revert）、subtask_status_history（POST/PUT/DELETE）** | assert_project_participant 已就緒；但 `ProjectSummaryReportContainer` / subtask container **未接 `project_participant_container`**（需加 DI wiring）；update/delete/export 依 uid，PSR 走 entity.project_id 反查、subtask 需確認 subtask→project 反查 | 補 container wiring（participant_role_service）→ create 走 find_project_id_by_uid、update/delete/export 走 entity.project_id → assert_project_participant |
| **MF 敏感 GET（軸②）**：control-defaults export/download、ssp-import-template、control-defaults GET、ssp-export | FE 實查：api.js 標「GET (blob)」者為 axios（bearer-safe，可加 `@require_super_admin_route`）；ssp-export / 其餘需逐支確認非 window.location | 逐支 FE 確認 blob → 掛 `@require_super_admin_route`（併 `module_frame.manage` 過渡名單） |
| **軸⑤ signed-token（Phase 4）新增**：`/survey/import-template`（BatchImportDialog window.location LIVE）、`/task-survey/import-template/<task_uid>`（_archived window.location） | 原生下載帶不了 bearer，盲加 JWT 踩 ffa01a78 前科 | Phase 4 signed-token（與 otp-qrcode / framework download 同機制） |
| **process/comments PUT** | route 直呼 jedi_flow_engine `ElementVariableService`，無主專案 service 可掛守門 | Phase 5 下沉套件；低風險留言 |
| **grc `jobs/batch-complete` 自身入口** | 每 job 走 complete_job 已受守（transitive）；自身入口 project 反查（job_uids 可跨專案）未加 | 逐 job 反查驗（低優先，transitive 已覆蓋主風險） |

---

## 7.4 Phase 2b-3 落地紀錄（2026-07-07，branch `fix/v1.8.0-bugs`）

收掉 2b-2 §7.3 尾巴。守門一律 `common.authz` canonical。

### 已完成（已修待驗）

| 端點群 | 守門 | 角色 | commit |
|--------|------|------|--------|
| project_summary_report create/update/delete/export/history-revert（含 history service） | `assert_project_participant`（container 補接 project_participant_container；entity.project_id 反查） | 參與者 | `02e7674d` |
| grc job 留言 POST（add_comment） | 復用 `WorkflowExecutionService.assert_project_participant`（job_execution→workflow_execution_id） | 參與者 | `e2c448ff` |
| grc `jobs/batch-complete` 入口 | `assert_project_manager`（route project_uid thread 進；per-job transitive 守門保留） | manager | `e2c448ff` |
| MF 敏感 GET：control-defaults GET list/detail・export・template/download、ssp-import-template ×2・excel-template・ssp-export | `@require_super_admin_route`（FE 逐支實查 axios/blob，bearer-safe） | super-admin | `a83a8619` |

單測新增 7 案（PSR 4 / job 留言 3）全綠。batch-complete 守門＝既有 `assert_project_manager` 呼叫（`test_fr048_grc_guards` 已覆蓋；本 service import WorkflowExecutionService 有既有循環 import，不另單獨 import 測）。**回歸**：對 baseline `c05ed713` 比對，`76 failed / 41 errors` 相同（**零新增失敗**），passed 691→698＝本波 7 新測。

### FE 查證結論（檔案下載類，逐支）

| 端點 | FE 呼叫 | 結論 |
|------|---------|------|
| control-defaults export / template/download | `axios.get(url, {responseType:'blob'})`（`ModuleFrameTemplateService.js:208/219`） | ✅ blob → 掛 `@require_super_admin_route` |
| ssp-import-template ×2 / excel-template / ssp-export | api.js 全標「GET (blob)」 | ✅ blob → 掛 |
| control-defaults GET list/detail | `this.get(...)`（BaseService axios） | ✅ JSON 讀 → 掛 |

### 留待後續（非默默略過）

| 端點群 | 原因 / 後續 |
|--------|-----------|
| **subtask_status_history（POST/PUT/DELETE）** | 唯一 FE consumer 在 `_archived/projects/oscal/ProjectTaskManage.vue`（v1-dead，無 live 流量）；`task_id` 為 opaque 字串、無 documented project 反查、無既有反查方法 → 猜反查鏈有正確性風險（verify-first / 禁重複造輪）。**待確認 task_id 指涉**（或隨 v1 端點退役）再處理 |
| **process/comments PUT** | route 直呼 jedi_flow_engine `ElementVariableService`，無主專案 service 可掛守門 → Phase 5 下沉套件；低風險留言 |
| **survey / task-survey import-template** | 原生 `window.location` → **Phase 4 signed-token**（見 §7.3；盲加 JWT 踩 ffa01a78 前科） |

> **Phase 2b（含 2b/2b-2/2b-3）軸③ 資源域收口大致完成**；剩餘為 v1-dead（subtask）、套件下沉（process/comments）、Phase 4 signed-token（原生下載）三類非本軸工作。

---

## 7.5 Phase 2c 落地紀錄（2026-07-07，branch `fix/v1.8.0-bugs`）

**§4 B 類收編完成**：`app/grc/service/` 7 份私有 `_check_*` 複製品 → 委派 `common.authz` canonical，**行為保真、零行為變更、call site 全不動**。

**canonical**：`common/authz/project.py` 新增 `assert_project_role(participant_domain_service, project_id, user_id, allowed_roles=("manager",), error_code=None)`——project 層 participant 精確查詢守門，**不做** control→group→project fallback（那是 `assert_project_manager` 的軸，兩支並列在 `__init__.py` 決策表、語意不可互相冒充）。error_code 未給時按 `allowed_roles` 選預設（含 `auditor`→GRC_NOT_AUDITOR，否則 GRC_NOT_MANAGER）；manager override 由 caller 用 `allowed_roles=("auditor","manager")` 明示。

**7 份對映**：

| 檔 | method | 委派參數 | 特殊處理 |
|----|--------|---------|---------|
| audit_service | `_check_role` | manager→`("manager",)`／其他→`(required_role,"manager")` | uid→resolve+404 保留；override 於 2026-07-07 user 定調後對齊（見下） |
| poam_app_service | `_check_manager` | 預設 `("manager",)` | — |
| audit_round_app_service | `_check_role` | manager→`("manager",)`／auditor→`("auditor","manager")` | manager override 保住 |
| job_import_service | `_check_manager_role` | 預設 `("manager",)` | uid→resolve+404 保留 |
| task_execution_service | `_check_manager` | 預設 `("manager",)` | resolve+404 保留、**維持回傳 project entity** |
| assessment_plan_app_service | `_check_auditor` | `("auditor","manager")` | manager override |
| assessment_result_app_service | `_check_auditor` | `("auditor","manager")` | manager override |

**✅ 已定調並對齊（2026-07-07 user 拍板「讓 manager 可代行」）**：`audit_service._check_role` 原為 exact match 無 manager override（與 audit_round / assessment_* 不同調），收編時先保真並標註。user 定調後已對齊：auditor 端點改 `allowed_roles=(required_role, "manager")`，manager 可代行 AR 相關寫入（generate-draft / judge / observation / risk），全站 override 語意一致。此為 2c 收尾唯一的行為變更（放寬，非收緊）。

**驗證**：baseline worktree（2c 前 commit `8068d954`）集合 diff——`pytest test/ --continue-on-collection-errors` FAILED/ERROR 節點集比對，**新增失敗 0**；2 支 task_execution 測試在全套跑 FAILED→PASSED 為既有跨測試順序 flakiness（隔離跑兩樹皆 5 綠），與本案無關。7 份方法體皆只剩委派一行＋註解，殘留複製邏輯 0；now-unused import（ForbiddenError / ProjectParticipantQueryEntity）順手清除。

---

## 8. Phase 4a 落地紀錄（2026-07-07，branch `fix/v1.8.0-bugs`）

軸④ capability guard 建置 + §6.3 過渡名單換裝完成。

**① guard 本體**：`common/authz/capability.py` — `@require_capability(*names, mode="any"/"all", error_code=?)`。
委派 DI `CapabilityGuard`（`auth_container.capability_guard`，自帶 `@transaction`），走 web-menu
既有查詢鏈 `user→roles→role.capabilities→cap.name`（不另立查詢路徑）；`flask.g` per-request
memoize；帳號層 super_admin **break-glass** 直接放行（`viewer_is_super_admin()`）。錯誤碼
`GRC_CAPABILITY_REQUIRED`(GRC_403022)。單測 `test_authz_capability.py` 14 案全綠。commit `eb590ae4`。

**② 能力點策略（user 定調）**：**復用既有 121 顆 granular cap（`<resource>.<action>`）、精準對
HTTP verb**（POST→create／PUT→update／DELETE→delete／敏感 GET→read），**不新造 `.manage` 平行方案**
（避免與 FE 選單所讀的 granular RBAC 分裂＝第五套）。Administrator[root,id=2] 已全持有既有 write cap，
換裝零 role_capabilities 變更。只 3 組缺口新 seed（migration `2026-07-07-fr048-phase4a-capability-seed.sql`，
已套 DEV、驗冪等）：`system_config.{create,update,delete}`（is_platform，泛用設定寫入觸及平台級）、
`feedback.export`（feedback.create 語意為使用者提交、不可復用）、`notification.test`（無 notification
resource）。commit `7828653e`。

**③ 換裝（35 檔，commit `e8b28a4a`）**：`@require_super_admin_route` → `@require_capability("<res>.<verb-action>")`。
模組→resource：auth `user`/`role`/`department`、`bulletin`、`device`、`information-system`、`system-menu`、
`remote-agent-manage`、`module-frame`（15 檔）、`survey`（4 檔，survey_route 見下）、`ldap-config`、`report`、
`system_config`、`feedback.export`、`notification.test`。flow_template 用 3 支 verb-specific alias 保留自訂碼
`GRC_FLOW_TEMPLATE_NO_PERMISSION`。**軸① platform decorator 本波不換**（tenant/log/framework 維持
`@require_platform_admin_route`）。

**④ 語意變化（重要，須手測確認）**：判定從「帳號層 super_admin」→「持有能力點 **或** super_admin
break-glass」。**持 Administrator 角色的一般帳號現在也能過**（原本 403）——放寬到 RBAC 正軌，符合設計
§2.0；但權限面「誰能過」變了，須讓 user 知悉。

**⑤ 驗證**：clean worktree 集合 diff（baseline `c29acbd0` 無 4a／test `e8b28a4a` 4a-only，隔離 4b 未 commit
干擾）——FAILED/ERROR 節點集 128↔128 **新增失敗 0**，passed +14（capability 單測）；17 支換裝 route 模組
import smoke 全過。DEV live 403/pass 三態測列手測 checklist（BE user 自起）。

**⚠️ 與 Phase 4b（signed-token）同 working tree 交錯**：`api/survey/routes/survey_route.py` 的 survey CRUD
換裝已在工作區完成，但該檔 import 行同時被 4b 改（`issue_signed_token`/`signed_token_required`）、且依賴 4b
未 commit 的 `common/authz/signed_token.py` → **暫不納入 4a commit**，待與 4b 協調後一併 commit。其餘 35 檔
不受影響。

**未處理／延後**：tenant.* 標 is_platform（Phase 5 併軸鋪路，有存量副作用，刻意延後）；STG/POC migration
（待 user 指示）；FE 角色矩陣新 cap 顯示 / i18n（查 FE 慣例後回報）。

---

## 9. Phase 4b 落地紀錄（2026-07-07，branch `fix/v1.8.0-bugs`；本節由 review session 補記——4b 執行 session 未寫矩陣）

軸⑤ signed-token 機制＋五件收口。BE `40a7d9c5`、FE `79c0133`（compliance-manager-fe，17 檔含共用 `signedDownloadUtil.js`）。

**機制**：`common/authz/signed_token.py` — `itsdangerous.URLSafeTimedSerializer`，key 自 JWT_SECRET_KEY 派生、**scope 當 salt**（跨 scope 重放無效），payload `{scope, resource_uid, user_id}`，TTL 120s。`issue_signed_token` / `verify_signed_token` / `@signed_token_required`（純簽章軌，`bind_uid_arg` 可綁 path 參數）/ `@signed_token_or_jwt`（雙軌，bearer 軌刻意不吞例外→保留 `TOKEN_401002` 觸發 FE refresh）。錯誤碼 `GRC_401011`。單測 `test_authz_signed_token.py` 12 案。

**五件**：①otp-qrcode（發放端 `/otp-qrcode-token`＋token 綁發放者本人 uid＝自我限定，修掉「知 uid 拿他人 TOTP」本體洞）②framework OSCAL download（`.../oscal/<uid>/token`）③file download/pdf-preview（`/file/access-token/<uid>`＋雙軌，既有 axios 呼叫方零改動）④survey import-template（LIVE window.location 件）⑤task-survey import-template（實查 consumer 僅 `_archived`＝dead → 直接補 `@jwt_required`，不上 token）。

**Review 補核（2026-07-07）**：全套件 baseline 集合 diff（`c29acbd0`→HEAD，涵蓋 4a+4b）**新增失敗 0**、passed 692→724；signed-token/capability 單測 26 案獨立跑全綠。

**⚠️ 4b 未完項（待補）**：
1. **e2e 未寫**（4b prompt/設計 §6 明定前科件必附）：test repo 補「登入→個人資料頁 QR 有載出」「框架管理頁 OSCAL 下載成功」＋負向案（登出直打資源 URL→401）。
2. **token 過期行為未實測**（發 token→過 TTL→401）。
3. 手測 checklist 由 review session 代出（見交付訊息）。

---

## 10. 裸奔稽核後補洞（Phase 2b-4，2026-07-07，branch `fix/v1.8.0-bugs`）

主體收尾後的裸奔稽核（5 群 subagent + 主導逐條驗 code）挖出的**真漏網**——大多數 agent 報的裸奔是誤判
（只看 route 沒追 service 層守門）。逐條先驗 live 再改，通過驗證的真缺口 4 條處置如下。

### 10.1 已補 / 已處置

| # | 端點群 | 現況 | 軸 | 處置 | 狀態 |
|---|--------|------|----|------|------|
| 1 | **task_survey 填答 7 支**：answer update/patch/checkpoint（`question_answer_service`）、history revert（`question_answer_history_service`）、update_task_survey / configure / import-answers（`task_survey_service`） | service 零身分檢查，任何登入者可改**任何人**填答 | ③ | D10：填答＝assignee 本人（manager 可代填）；管理＝manager。新 guard `app/task_survey/common/task_survey_guard.py`（`assert_task_survey_writer` / `assert_task_survey_manager`），assignee 走 `task_assignees(task_id,user_id)` 自然鍵、manager 走 `assert_project_manager` canonical（project_id 由 `task_assignees.project_id` 反查）。新 error code `TASK_SURVEY_403001`。DI 補接 `project_participant_container` → `participant_role_service`。 | ✅ 已修待驗 |
| 2 | **poam `update_poam`**（`app/grc/service/poam_service.py:96`） | service 零守門 | ③ | **dead，不改**：route `PoamDetailResource` PUT 在 `api/grc/__init__.py:167-170` **已註解停用**（FR-038 DEAD-V1 §1B，FE `PoamView`→`RoundPoamView` 走 `/audit-round/*`）。`PoamService` 全站僅此 dead route 引用，無 live 風險。真 live 的 v2 是 `poam_app_service`（有 `_check_manager`）。 | dead（複驗即可） |
| 3 | **report `file-list`**（`api/report/routes/scheduler_report_route.py:44` POST） | 只有 `@jwt_required`，與 preview 不一致 | ② | 對齊 preview 掛 `@require_capability("report")`（見 #4 修正） | ✅ 已修待驗 |
| 4 | **report 能力點名稱錯**（4a 換裝後遺症） | preview 用 `@require_capability("report.read")`，但 DB **無 `report.read`**（DEV live 查證：只有既有 `report` id=49／is_platform=f／desc「report?v=1 read access」，掛 Administrator）→ 該守門 **fail-closed 擋掉 super_admin 以外所有人**＝preview 現行已壞 | ④ | user 定調①：`report.read`→既有 `report`。preview + file-list 皆改 `@require_capability("report")`，零 migration。 | ✅ 已修待驗 |

**驗證**：新增單測 `test/test_fr048_task_survey_guard.py`（6 案：writer assignee/manager-override/deny、manager pass/deny/unresolvable）全綠；`test_audit_event_instrumentation.py` 補守門 fixture（1 案回綠 + `_build_*` 注入 guard deps）。**回歸**：baseline worktree（起點 `108df8f8`）集合 diff——FAILED/ERROR `128↔126`（**新增失敗 0**；少 2 為既有 `test_task_execution_service` 跨測序 flaky，隔離跑綠），passed 724→730＝本波 6 新測。

### 10.2 稽核誤判澄清（備查，避免下次重查）

裸奔稽核 agent 報的其餘「缺口」經主導逐條追 service 層後**確認已守 / 非缺口**，記此免重查：

| 稽核指稱 | 澄清 |
|----------|------|
| flow_engine 12 支「裸奔」 | §7.2/§7.3 已復活 `assert_workflow_project_participant` 守門鏈（task/complete、revert、job-evidence PUT/DELETE/POST、task/link）；agent 只看 route 未追 wrapper 復活狀態 → 誤判 |
| oscal 13 支「裸奔」 | §3.1 已列：SSP-scoped 30+ 支全數 `SspPermissionChecker.require_manager`、framework 家族 `require_platform_admin`；agent 未追 route decorator/service resolve → 誤判 |
| cloud 4 支「裸奔」 | §3.10/§F 族已列：disconnect/auth-url/webhook `is_admin`→`require_super_admin`、verify-and-repair manager；OAuth callback/webhook push 刻意 F（state/channel token）→ 誤判 |
| participant「那批裸奔」 | §3.9 已列：task-assignee/project/control/group participant 12 支 live 全 `assert_project_manager`；`batch`/`process-participant` 為 2A stub / 未註冊 dead → 誤判 |

> **本 §10 收口後，Phase 2b 軸③ 資源域含裸奔稽核補洞大致封版**；剩 v1-dead（subtask）、套件下沉（process/comments）、Phase 4 signed-token（原生下載）三類非本軸工作（見 §7.4 末）。
