盤點 ①:授權守門與能力點現況(BE + jedi-iam)

2026-10-06 探脈產出,唯讀盤點,供討論稿引用。jedi-iam 路徑以 ~/Projects/Jedicogy/module/jedi-python-package/jedi-iam/jedi_iam/ 為根。

最需要注意的三點:

  • 判定層完全沒用到部門範圍。 user_roles 有 org_unit_id 和 scope 兩欄,但判定只比 tenant_id。
  • api/ 下 61 支掛 jwt_required 的 route 檔裡,33 支沒有任何能力點守門。 OSCAL SSP 那一整組 13 支都在其中。
  • 有兩個讀取端不過濾「有效指派」。 一是選單明細(user_service.py:522-529),二是 JWT 的 is_admin claim(jwt_mw.py:79)。

1. 能力點 schema 與讀取端

ORM model 全部在 jedi-iam 的 infra/models/,BE 這邊沒有。

表 檔:行 欄位
capabilities capability.py:15-27 id、name(100, UNIQUE)、resource_type(100)、action(50)、description、is_platform(bool)
role_capabilities role_capability.py:14-23 role_id + capability_id 複合主鍵,兩邊都是 FK、CASCADE;沒有 pattern/wildcard 欄
route_capabilities route_capability.py:18-33 route_id + capability_id 複合主鍵,另有 requirement(ANY/ALL enum)
user_roles user_role.py:20-84 id、user_id、role_id、tenant_id、org_unit_id、starts_at、ends_at(timestamptz)、scope(預設 'tenant')、is_default
user_roles 約束 user_role.py:26-44 CHECK tenant_id 或 org_unit_id 至少一個不為空;兩條部分唯一索引:(user,role,tenant,starts_at) WHERE org_unit_id IS NULL,(user,role,org_unit,starts_at) WHERE tenant_id IS NULL
roles role.py:23-84 uid、pid、name、enable、is_admin(int)、is_delete、tenant_id;role.capabilities 是 association_proxy(role.py:76-84)

role_capabilities 的讀取端:

用途 檔:行 有沒有套 active_user_role_conditions
判定 jedi-iam infra/repository/user_role_repo_impl.py:73-101(get_active_capability_names) 有
選單可見性 jedi-iam infra/repository/ui_route_repo_impl.py:70-141(子查詢在 :87-103) 有
選單明細(每個節點列出持有的 caps) jedi-iam app/service/user_service.py:522-529, 547 沒有:直接迭代 user.roles → role.capabilities,不過濾效期、停用、租戶
JWT claim is_admin jedi-iam middleware/jwt_mw.py:79 沒有:any(role.is_admin == 1 for role in user.roles),不看 role_capabilities,但同樣不過濾
角色 CRUD 寫入 jedi-iam domain/service/role_domain_service.py:88-90(新增)、:96-117(先刪光再重建) 不適用
角色 CRUD 讀出 jedi-iam infra/mapper/role_mapper.py:32、app/dto/role.py:55、app/service/role_service.py:76,91 不適用
BE 角色 CRUD 授權檢查 app/auth/service/role_app_service.py:41,49-51(讀既有角色的 existing.capabilities id)、:54-91(_assert_new_capabilities_licensed,用 get_capabilities_by_id_list 取 resource_type) 不適用
BE 其他 core/plugins/evidence_classification.py:140-142(viewer_has_capability);common/authz/capability.py 只是轉接到 jedi-iam 的 shim;DI 接線在 di_containers/auth/auth_containers.py:57,100,122 —

2. jedi-iam 判定層

  • active_user_role_conditions:infra/repository/active_role_conditions.py:19-39。條件依序是:
    • UserRole.starts_at <= now()
    • UserRole.ends_at IS NULL OR ends_at >= now():含等號,now() 取 DB 時間,欄位是 timestamptz
    • Role.enable == 1
    • Role.is_delete == 0
    • 有給 tenant_id 時,再加 UserRole.tenant_id == tenant_id
  • get_active_capability_names 的查詢形狀:user_role_repo_impl.py:81-101。
    • 從 Capability 依序 join RoleCapability、UserRole、Role,最後 DISTINCT name。
    • 查本人時直接用 UserRole.user_id,不 join users(為了避開 RLS);查他人時才 join User 比 uid。
    • 完全沒用 org_unit_id 或 scope,也沒有子部門繼承。 只要 context 有 tenant_id,tenant_id IS NULL 的部門指派一律被排除。
  • CapabilityGuard:authz/capability.py:51-66。
    • capability_names_for 從 get_user_context().tenant_id 取租戶。
    • 快取只有單一請求內有效:存在 flask.g,key 是 _authz_capability_names(:48, :99-106),沒有跨請求快取。
    • super_admin 一律放行:viewer_has_capability 在 :115、require_capability 在 :135。
    • guard 預設從 DI 取得(:69-77),可用 set_guard_resolver 換掉(:85)。

3. 守門覆蓋(api/ 下有 jwt_required 的檔,共 61 支)

分組方式是對整支檔 grep 守門字樣。G 組代表「檔內至少一處有守門」,不保證每個方法都有。jedi-iam 自己的 route(例如 role_route)不在 BE 的 api/ 下,沒算進來。

G 組:有 require_capability / platform_admin / super_admin / signed_token 等守門,28 支

  • ai_call_log/ai_call_log_route
  • cloud_integration/:drive_app_credential_verify、google_drive_integration、google_drive_sync
  • flow_control/project_route
  • flow_engine/flow_template_route
  • module_frame/ 共 15 支:mf_ssp_export、components、control_default、control_objective_default、import、inventory、item、leveraged、party、reference_document、module_frame_route、ssp_resources、system_characteristic、template_import、ssp_import_template
  • notify_config/notify_config_route
  • oscal/:framework/oscal_framework_version、module_frame_template_ssp、resource_library
  • project/project_route
  • support/recent_error_route
  • system_config/security_policy_route

J 組:只有 jwt_required,33 支

模組 數量 檔案
oscal(SSP) 13 ssp_components、ssp_control_impl_import、ssp_control_implementation、ssp_document_pool、ssp_docx_import、ssp_excel_import、ssp_export、ssp_inventory_items、ssp_leveraged、ssp_party、ssp_resources、ssp_scoped_excel_import、ssp_system_characteristic
oscal(framework) 3 framework_parse_job、framework_version_edit、oscal_framework_route
flow_engine 5 flow_engine_route、job_evidence、stage_advance、stage_object、stage_rollback
grc(flow_control) 3 assessment_plan、job_force_start、job_route
grc(project) 3 ap_docx_import、ar_import、audit_round
grc(project_summary_report) 2 project_summary_report_route、project_summary_report_history_route
system 2 system_config/system_config_route、setup/setup_route(設定精靈,可能是刻意不設守門)
其他 2 ai_quota/ai_quota_route、support/diagnostic_bundle_route

注意:J 組的 SSP/flow_engine/grc 多數在 service 層有專案成員角色守門(見盤點 ②),「沒掛能力點」不等於「誰登入都能用」,但確實缺了租戶 RBAC 這一層。

4. 能力點命名現況(DEV DB)

  • 共 123 個能力點,37 種 resource_type,其中 16 個 is_platform = true。
resource 命名 resource_type 種數 能力點筆數
底線 4 13
連字號 19 56
單字,無分隔 14 54
  • 用底線的 4 個 resource_type:cloud_integration、notify_config、system_config、flow_template。
  • 非 CRUD 的動詞:export ×2(feedback、project)、assign ×1(project)、approve ×1(project)。
  • 沒掛任何 route 的孤兒能力點有 9 個:cloud_integration.update、feedback.export、system_config.create/delete/update、workflow.create/delete/read/update。
  • system_config 只有 create/delete/update,沒有 read;而對應的 system_config_route 剛好落在第 3 節的 J 組。

5. 預設角色 seed

  • 出貨基線:scripts/init/04-seed-core.sql。只有 1 個角色:Administrator,id=2、tenant_id=1、is_admin = 1(:62)。從 :272 起是 123 列 role_capabilities,用名稱反查 id,等於全部能力點都配給它。
  • 產生器:scripts/init/gen_capability_seed.sql,由 gen_seed_sql.sh:86 呼叫,結尾在 :292-327 有筆數自檢。改用名稱反查的理由寫在 scripts/init/README.md:57-66(CM-1757)。
  • 新租戶:jedi-iam app/service/tenant_provisioning_service.py:49 會建一個 "System Manager" 角色,配「能力點全集扣掉排除清單」,並在 :149 設成 is_admin = 1。
  • 既有租戶回補:scripts/init/migrate-capability-grants.sql 是模板,build 時產出 scripts/sql/packages/_capability_grants.sql。
  • 「新增能力點後幫舊角色補配」的 migration 範例:scripts/sql/2026-09-25-fr114-module-frame-read-capability-backfill.sql。
  • DEV 現況:活著的角色有 14 個,is_admin = 1 的有 9 個。

6. 角色匯出/匯入

目前沒有。 角色端點只有 /roles/menu、/roles、/role、/role/<uid>、/role/status/<uid> 5 個,定義在 jedi-iam api/routing.py:107-111,全部是 CRUD 或狀態切換。