FR-048 Phase 2c 交接 prompt(B 類收編:7 份私有 check* 委派 canonical)

下面整段是給執行 session 的 prompt。前置:Phase 2b 與 2b-2 都已收完(2c 與它們動同一批 app/grc/service/ 檔案,必須序列執行;2b-2 見同目錄 2026-07-07-phase2b2-remainder-prompt.md)。未收完就停下回報。


【接手主題】FR-048 統一授權守門 Phase 2c — common/authz/project.py 泛化 assert_project_role,grc 7 份私有 _check_* 複製品改為委派(行為保真、零行為變更

必讀:

  1. docs/features/FR-048-2607-unified-auth-guard/endpoint-authz-matrix.md §4 B 類(本波 scope 定義)
  2. docs/analysis/2026-07-07-unified-auth-guard-design.md §2 軸模型
  3. common/authz/project.py 現況

【branch】fix/v1.8.0-bugs,禁止切 branch。不動 jedi-* 套件。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【本波鐵則】

  • 行為保真是唯一目的:這是重複消除,不是行為修正。任何 403/404 行為、error code、判定順序都必須跟收編前一模一樣。
  • 發現語意不一致(見下方差異表的 audit_service 案)→ 標註回報,不修

【7 份複製品與語意差異表(2026-07-07 實查,執行前先驗證仍準確——2b 可能又動過這些檔)】

# 檔:行 method 入口 判定 特殊點
1 app/grc/service/audit_service.py:27 _check_role(project_uid, user_id, required_role) uid→resolve+404 exact match,無 manager override error_map 按 required_role 選碼
2 app/grc/service/poam_app_service.py:91 _check_manager(project_id, user_id) id manager-only
3 app/grc/service/audit_round_app_service.py:99 _check_role(project_id, user_id, required_role) id required_role 含 manager override 錯誤碼按 required_role 選 NOT_MANAGER/NOT_AUDITOR
4 app/grc/service/job_import_service.py:81 _check_manager_role(project_uid, user_id) uid→resolve+404 manager-only
5 app/grc/service/task_execution_service.py:47 _check_manager(project_uid, user_id) uid→resolve+404 manager-only 回傳 project entity
6 app/grc/service/assessment_plan_app_service.py:230 _check_auditor(project_id, user_id) id auditor+manager override
7 app/grc/service/assessment_result_app_service.py:111 _check_auditor(project_id, user_id) id auditor+manager override

⚠️ 查法差異(不可混):這 7 份都是「project 層 participant 精確查詢」(participant_domain_service.get_one(ProjectParticipantQueryEntity(project_id, user_id)))。既有 canonical assert_project_manager 走的是 role_service.get_user_role(control→group→project 由下往上 fallback)——兩者語意不同,不能把 7 份改成走 assert_project_manager

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【要做的事】

1. common/authz/project.py 新增泛化 canonical

def assert_project_role(
    participant_domain_service,
    project_id: int,
    user_id: int,
    allowed_roles: tuple = ("manager",),
    error_code=None,
) -> None:
    """Project 層 participant 精確查詢守門(不含 group/control fallback——那是
    assert_project_manager 的軸)。participant 為 None 或 role 不在 allowed_roles → ForbiddenError。

    error_code 未給時按 allowed_roles 選預設:含 "auditor" → GRC_NOT_AUDITOR,否則 GRC_NOT_MANAGER。
    manager override 由 caller 用 allowed_roles=("auditor", "manager") 表達,本函式不隱含 override。
    caller 必須在 @transaction scope 內。
    """
  • docstring 必須寫明與 assert_project_manager(fallback 查法)的分工,兩支並列在 __init__.py 決策表。
  • uid→project resolve+404 不收進 authz(那是業務前置條件不是授權),caller 保留自己那兩行;task_execution 本來就要 project entity,維持原樣。

2. 7 份逐一改為薄殼委派(保留原方法名,call site 全不動)

def _check_manager(self, project_id: int, user_id: int) -> None:
    # FR-048 2c:委派 canonical,語意同前(project 層精確查詢、manager-only)
    assert_project_role(self._participant_domain_service, project_id, user_id)
  • 各檔對映:#2/#4/#5 → allowed_roles=("manager",);#6/#7 → ("auditor", "manager");#3 → required_role=="manager" 時 ("manager",)、=="auditor" 時 ("auditor", "manager")(保住 override 語意與錯誤碼選擇);#1 見下。
  • 改完每檔跑該檔相關測試,再全套。

3. audit_service(#1)特殊處理——標註不改行為

#1 的 exact match 沒有 manager override,與 #3/#6/#7 不同調(同是 required_role 語意)。這可能是 bug 也可能刻意:

  • 收編時用 allowed_roles=(required_role,) 保住無 override 的現行為
  • 在該處加註解標明「與 audit_round 的 override 語意不一致,待 user 定調是否對齊」,並在收尾回報清單裡列出(含受影響端點)。不要自作主張加 override

4. 驗證(做完必跑)

  1. pytest test/(用 python -m pytest)全綠比對:baseline worktree 集合 diff(P3 review 用過的方法)——在 2c 前 commit 開 worktree 跑同套件,comm 比對 FAILED/ERROR 清單,新增失敗必須為 0。
  2. grep 確認 7 份方法體都只剩委派一行+註解,無殘留複製邏輯。
  3. 矩陣 §4 B 類狀態改「已收編」+ commit hash。

【commit】一個 commit 即可(收編性質單一):refactor(FR-048): grc 7 份私有 _check_* 收編為 common.authz assert_project_role 委派(行為保真)。顯式 git add、禁 -am、不 push。

【收尾】commit 完停下,回報:① 一句話 status;② audit_service override 不一致案(等 user 定調);③ 若 2b 期間又長出新的私有 check helper,列出來。收尾動作等 user 下令。