FR-048 Phase 2b-4 交接 prompt(裸奔稽核後的寫入面真漏網收口)

給執行 session 的 prompt。前置:FR-048 主體已完工(SUMMARY 2026-07-07-FR-048-SUMMARY.md)。 本批是收尾後的裸奔稽核(5 群 subagent + 主導逐條驗 code)挖出的真漏網—— 大多數 agent 報的裸奔是誤判(只看 route 沒追 service 層守門),以下是通過驗證的真缺口。


【接手主題】FR-048 Phase 2b-4 — 收掉裸奔稽核確認的 4 個寫入面真漏網

必讀:

  1. docs/features/FR-048-2607-unified-auth-guard/endpoint-authz-matrix.md(軸模型、既有守門 pattern)
  2. common/authz/(canonical:assert_project_manager / assert_project_participant / require_capability)

【branch】fix/v1.8.0-bugs,禁止切 branch。不動 jedi-* 套件。守門一律 common.authz canonical。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【鐵則:先驗 live 再改(每條都要)】

修 bug 前先驗真實狀態(DB/FE/呼叫路徑),不要憑本 prompt 直接刻。每條的驗證步驟已列在下方。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. task_survey 填答 7 條(最大真漏網,軸③)

現況(已驗):app/task_survey/service/question_answer_service.py(update_task_survey_answer:61 / checkpoint:168 / patch:314)、task_survey_service.py(update_task_survey:71 / update_task_survey_configure:529)、question_answer_history revert——service 完全無身分檢查,任何登入者可改任何人的填答。route 只有 @jwt_required。

定調(D10):填答 = 被指派者本人(assignee);configure/管理類 = manager。

先驗:

  • FE 怎麼呼叫這些端點(哪個角色在用填答頁)——compliance-manager-fe grep 對應 api。
  • task_survey → 如何拿到 assignee / project:service 已注入 task_assignee_domain_service(task_survey_service.py:37),grep 既有 TaskAssigneeQueryEntity 用法找反查(禁重複造輪)。task_survey 有 task_id → task_assignees 有 project_id + user_id。

:

  • 填答類(answer update/patch/checkpoint/revert):驗「current user 是該 task 的 assignee 本人」——查 task_assignees 有無 (task_id, current_user_id) 這列;無 → ForbiddenError。若 D10 允許 manager 代填,manager 也放行(比照 AR override)。
  • configure(update_task_survey_configure)、update_task_survey:manager 級 → resolve project 後 assert_project_manager
  • 新增對應 error code(task_survey 模組碼,或復用 GRC_NOT_PROJECT_PARTICIPANT)。

2. poam update_poam(silent 漏網,矩陣沒記,軸③ manager)

現況(已驗):api/grc/routes/poam_route.py:73 PUT 呼叫 app/grc/service/poam_service.py:96 update_poam——此 service 零守門(grep assert_project_manager = 0)。注意這跟已守poam_app_service.py(v2,有 _check_manager)是兩個不同 service,route 用的是沒守的這個。

先驗:

  • 確認 poam_route.py 這條 PUT 是 live(FE 有在用?還是 v1 殘留)——grep FE + 看 api/grc/__init__.py 有無註冊。若 dead 就標 dead 不改。
  • poam_service.update_poam 的 ap_uid → 怎麼 resolve project_id(看 poam_app_service 或 audit_round 既有反查)。

(若 live):update_poam 開頭 resolve project → assert_project_manager。若同 service 有其他寫入 method(create/delete poam)一併檢視補齊。

3. report file-list 對齊 preview(軸②)

現況(已驗):api/report/routes/scheduler_report_route.py — preview(:69)有 @require_capability("report.read"),但 file-list(:44)POST 只有 @jwt_required,不一致。路徑已消毒(_safe_report_path),風險低但該對齊。

:file-list 掛同款 @require_capability("report.read")(或與 preview 一致的守門)。

4. ⚠️ report.read 能力點是否存在(可能是 4a 的 latent bug)

疑點:report.read任何 migration seed 都找不到(grep -rn "report" scripts/sql/*.sql 無)。若它不在 DB 既有 121 顆 capability 裡,則 preview(及本批要加的 file-list)的 @require_capability("report.read")把 super_admin 以外所有人擋掉(break-glass 才進得去)= 現行 preview 可能已壞。

先驗(DEV DB):

SELECT name, is_platform FROM public.capabilities WHERE name LIKE 'report%';

(用 cmmgr,連線資訊查 .env DB_SECRET / CLAUDE.md DB 環境清單;跑 SQL 前正確 source .env)

處置:

  • report.read 存在 → 沒事,file-list 直接掛。
  • 不存在 → 這是 4a 換裝挑錯名字的後遺症。二擇一:①改用一顆確實存在的 capability(grep 4a 換裝紀錄 §6.3 看 report 當初對到什麼);②seed 一顆 report.read(幂等 migration,配 Administrator,只套 DEV,STG/POC 列交付)。先回報 user 選哪個,別自己拍板 seed

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【驗證 + 收尾】

  • 每條補單測(非授權 403 / 授權放行;task_survey 用 assignee mock)。
  • 回歸:baseline worktree 集合 diff(起點 commit 開 worktree 跑 python -m pytest test/ -q --continue-on-collection-errors,comm 比對,新增失敗 0)。
  • 矩陣加一段「§10 裸奔稽核後補洞」記這 4 條 + 銷帳;順手把稽核的誤判澄清(flow_engine 12/oscal 13/cloud 4/participant 那批其實已守)寫進矩陣備查,避免下次又被當缺口重查。
  • commit 按條分批(顯式 add、禁 -am、不 push)。收尾:一句話 status + 手測 checklist(含 report.read 定調項),收尾動作等 user 下令。

【手測 checklist 必含】非 assignee 帳號改別人填答 → 403;assignee 本人 → 正常;report 預覽/檔案清單 admin 能開、一般帳號行為一致。