# 盤點 ①：授權守門與能力點現況（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 或狀態切換。
