FR-048 Phase 1 交接 prompt(建 common/authz/ guard library,純搬家)

下面整段是給執行 session 的 prompt,複製貼上即可。


【接手主題】FR-048 統一授權守門 Phase 1 — 建 common/authz/ package(純搬家+一支新 helper,零行為變更

你接手的是已拍板設計的第一波實作。設計、拍板紀錄、各 phase 注意事項全在 docs/analysis/2026-07-07-unified-auth-guard-design.md必讀,尤其 §0 拍板紀錄、§2 軸模型、§3.2 收斂形狀、§7 Phase 1 注意事項)。 追蹤表:docs/analysis/2026-07-07-known-pits-remediation-tracker.md

【branch】fix/v1.8.0-bugs,禁止切 branch;不對就停下問 user。

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

  • 零行為變更:不掛任何新守門到任何端點、不寫 decorator、不修任何既有判定邏輯。403 行為必須跟今天一模一樣。
  • 不動 jedi- 套件*(P2 拍板)。
  • 唯一新增物是 require_super_admin()只建不掛

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

1. 建 common/authz/ package(搬家)

新檔 來源 搬法
common/authz/platform.py common/util/permission.py 整檔內容搬入(require_platform_admin / viewer_is_platform_admin
common/authz/project.py common/util/participant_guard.py 整檔搬入(assert_project_manager
common/authz/ssp.py common/middleware/permission/ssp_permission.py 整檔搬入(SspPermissionChecker
common/authz/workflow.py common/util/workflow_project_guard.py 整檔搬入(停用中的守門,原樣搬,函式開頭的 return 與 FR-038 2B TODO 註解保留)
common/authz/admin.py 新寫 見 §2
common/authz/__init__.py 新寫 顯式 re-export 全部公開名 + 決策表 docstring(表內容抄設計文件 §3.2 那張「你的端點是… → 用」表)

舊檔一律改成 shim(不刪),顯式 re-import 原名,例如:

# common/util/permission.py
"""[DEPRECATED 2026-07 FR-048] 已遷移 common/authz/platform.py,新 code 請 import common.authz。"""
from common.authz.platform import require_platform_admin, viewer_is_platform_admin  # noqa: F401

(用顯式名字、不用 import *;四個舊檔都同樣處理。)

為什麼要 shim(實查過,別省略):22 個檔案 import 舊路徑——participant 5 支 service、 SSP 11 支 service、di_containers/oscal/oscal_containers.py:75api/auth/routes/ui_route_route.py:9app/oscal/service/framework_version_edit_service.py:60、多個測試檔。本波全部不改 call site,靠 shim 過。 測試不直接 patch guard 模組路徑(已實查確認,只透過 DI mock 注入),shim 風險低。

2. 新 helper common/authz/admin.py

def viewer_is_super_admin() -> bool:
    """帳號層 super admin(UserContextDTO.is_admin = user.is_super_admin)。"""

def require_super_admin(error_code=GrcErrorCode.GRC_NOT_ADMIN) -> None:
    """非帳號層 super admin 一律 ForbiddenError。"""
  • get_user_context(),context 為 None 或 is_admin 非 True → 拋 ForbiddenError(error_code)(fail-closed)。
  • docstring 必須寫明兩個 is_admin 的地雷:JWT claim 的 is_admin=角色層 any(role.is_admin==1)UserContextDTO.is_admin=帳號層 user.is_super_admin。本 helper 走後者;要角色層語意請走 capability 軸(Phase 4)。
  • 參考 common/authz/platform.py 的寫法風格(同為純 context 判定)。
  • 本波不掛任何端點

3. 標註(不修)既有誤用

common/authz/ssp.py_require_admin_for_template(原 ssp_permission.py:167-175)用了 getattr(user, "is_admin", False)——搬家時在該處加註解標明「此處為帳號層 super_admin 語意, dev-stage gate,Phase 3 檢討是否改 require_super_admin/capability」,行為不動

4. 文件三處

  1. docs/claude/domain-capabilities.md 補一列:授權守門 canonical=common/authz/ (五軸:platform / super-admin / project-role / capability(Phase 4) / signed-token(Phase 4)), 舊四路徑為 deprecated shim。這正是該索引「同功能多支、canonical 不明顯」要收的陷阱類。
  2. CLAUDE.md「權限檢查的正確做法」段落補兩點(P1 已拍板):
    • 守門一律用 common/authz/(決策表在其 __init__.py),禁止另立新 helper
    • 主體域守門(platform-admin / super-admin / capability)允許 route 層 decorator 形式 (實作委派 DI guard service,route 不碰 session);資源域守門(專案/SSP/workflow)維持 app service 層
  3. 設計文件 §7 表 Phase 1 標 ✅(含完成日期)。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【驗證(做完必跑)】

  1. pytest test/ 全綠(participant / SSP 守門既有測試=搬家回歸網)。
  2. grep -rn "common.util.permission\|common.util.participant_guard\|common.middleware.permission.ssp_permission\|common.util.workflow_project_guard" --include="*.py" api/ app/ di_containers/ test/ 列出的每個 call site 確認 shim 供得上原名(跑個 python -c "from common.util.permission import require_platform_admin" 級別的煙測即可)。
  3. 請 user 重啟 BE(只說「請重啟 BE」,不附命令),啟動無 DI 錯誤即可;有異常先看 log/app.log

【commit】可自行 commit(顯式 git add 檔名、禁 -am、不 push)。建議兩個 commit: ① refactor(FR-048): 建 common/authz/ 收斂守門 helper(舊路徑留 shim,零行為變更)docs(FR-048): domain-capabilities/CLAUDE.md 登記 authz canonical + decorator 定調

【收尾】commit 完停下,給一句話 status。spec/SUMMARY/memory 等收尾動作一律等 user 下令。 Phase 2(C 族補守門)/ Phase 3(B 族 decorator)是後續 session 的事,本波不要越界。