# A.1 盤點：system 系統設定＋iam 身分與存取 端點 × 三段式能力點

> 卡片 CM-2514（FR-132 T-1.4）。唯讀盤點，不改程式、不改 DB。

## 範圍與數量

- **範圍**：system 系統設定（smtp-config、ldap-config、notify_config、security-policy、log-forwarding、system_config、system-menu、issue-integrate-config、license，以及走同一組設定端點的 storage-config／AI 服務／Drive 應用程式設定、支援頁）＋ iam 身分與存取（user、role、department、tenant，含登入／MFA／改密碼／第三方登入綁定）。
- **主專案 route 檔 8 支、15 個 method**：`api/system_config/routes/` 2 支（8）、`api/notify_config/routes/` 1 支（1）、`api/setup/routes/` 1 支（2）、`api/support/routes/` 2 支（2）、`api/version/routes/` 2 支（2）。`api/user_auth_provider/` 只剩 `__pycache__`，route 已上移 jedi-iam。
- **外部套件 route 檔 19 支、81 個 method**：jedi-iam 12 支（51；卡上寫 13 支，實際 12 支，`routing.py` 凍結 URL 43 條）、jedi-system-core 2 支（11）、jedi-notification 1 支（1）、jedi-issue 1 支（1，`label_route`；同套件的 `feedback_route` 屬 T-1.5）、jedi-log（`jedi_api_log.forwarding`）1 支（3）、jedi-license-runtime 2 支（14）。讀的是 `.venv/lib/python3.11/site-packages/` 內實際執行的版本。
- **合計 27 支檔、96 個 method**。
- **DEV 查詢時間**：2026-10-06 17:28（本機 `localhost:5432/guidant_ai_dev`）。範圍內舊能力點 45 個（另 storage-config 4 個屬 T-1.3，本表只列被本範圍 route 用到的部分）。

## 先看這幾點

1. **設定類的「讀」幾乎都沒有能力點守門。** `GET /system/config/<group>/<key>`、`GET /system/configs/<group>`、`GET /system/config/<uid>` 三支只驗登入；service 層只多兩道（AI／Drive 鎖平台管理員、SMTP／LDAP／登入政策鎖總部），**沒有任何 `*.read`**。登入的一般使用者可以讀通知設定、議題整合設定、儲存設定（機密欄已遮罩）。寫入那側有按 GROUP 分流守門，讀這側沒有。
2. **`system_config.*` 不是純孤兒。** 它是 GROUP 分流的 fallback（`core/plugins/system_core.py:88-93`）：對照表沒登記的 group（DEV 有 `GOOGLE_DRIVE_APP_CONFIG`、`WEB_IDEL_CONFIG`；`AI_QUOTA` 另被專門擋掉）寫入時就要 `system_config.update`。Drive 應用程式設定頁（`/system/drive-app-config`）的儲存走的正是這條，選單卻綁 `storage-config.*`。只有 Administrator 角色持有 `system_config.*`，實務上靠平台管理員那道把關才沒出事。
3. **授權照管理端整組沒用能力點。** `/license/tenants*`、`/license/upload`、`/license/lc/plans` 9 支只疊平台管理員守門；`license.read` 只綁 `/license/manage` 選單。租戶自救 5 支只驗登入是刻意的防死鎖設計。
4. **幾個動作的守門動詞與語意不符**：停用／啟用使用者守 `user.create`、LDAP 連線測試守 `ldap-config.create`、改租戶只守平台管理員（`tenant.update` 沒有 route 用）。
5. **第三方登入綁定的查詢沒有本人檢查**：`GET /auth-providers/user/<user_uid>` 只驗登入，service（`jedi_iam/app/service/user_auth_provider_service.py:87`）不呼叫同檔的 `_assert_caller_can_manage`，任何登入者帶別人 uid 就能看到對方綁了哪個 LDAP／AD 帳號。新增與刪除兩支有本人或 `user.update` 檢查。
6. **安裝精靈是刻意豁免**，證據完整，結論見〈setup_route 結論〉。

## 新名總覽（定案）

system 模組（基礎設施，皆不受授權照管制或屬原廠）：

| 新資源 | read | create | update | delete | 獨立動詞 | 取代的舊名 |
|---|---|---|---|---|---|---|
| `system.smtp-config` | ✓ | ✓ | ✓ | ✓ | — | `smtp-config.*` |
| `system.ldap-config` | ✓ | ✓ | ✓ | ✓ | — | `ldap-config.*` |
| `system.notify-config` | ✓ | ✓（新建） | ✓ | ✓（新建） | — | `notify_config.read/update` |
| `system.issue-integrate-config` | ✓ | ✓ | ✓ | ✓ | — | `issue-integrate-config.*` |
| `system.security-policy` | ✓ | ✓（新建） | ✓ | ✓（新建） | — | `security-policy.read/update` |
| `system.log-forwarding` | ✓ | ✓（新建） | ✓ | ✓（新建） | — | `log-forwarding.read/update` |
| `system.system-config` | ✓（新建） | ✓ | ✓ | ✓ | — | `system_config.create/update/delete`（保留，見定案 1） |
| `system.system-menu` | ✓ | ✓ | ✓ | ✓ | — | `system-menu.*` |
| `system.storage-config` | ✓ | ✓ | ✓ | ✓ | — | `storage-config.*`（只管儲存設定頁，見定案 2） |
| 授權照 | — | — | — | — | — | 移至 `platform.license.*`（見定案 4） |

iam 模組：

| 新資源 | read | create | update | delete | 獨立動詞 | 取代的舊名 |
|---|---|---|---|---|---|---|
| `iam.user` | ✓ | ✓ | ✓ | ✓ | `import`（Excel 批次匯入） | `user.*` |
| `iam.role` | ✓ | ✓ | ✓ | ✓ | — | `role.*` |
| `iam.org-unit` | ✓ | ✓ | ✓ | ✓ | — | `department.*` |
| `iam.tenant` | ✓ | ✓ | ✓ | ✓ | — | `tenant.*` |

- 設定類的「測試送出」（測試信、通知測試、log 轉發測試、LDAP 連線測試）建議一律歸該資源的 `update`，不開 `send`：沒有客戶會要「能改設定但不能測」。見定案 3。
- storage-config 歸 system；AI 服務設定與 AI 證據分類設定改 `ai.ai-provider-config.*`（新開，歸 `ai` 模組）、Drive 應用程式憑證歸 platform 層、支援頁歸 `platform.support.*`（見定案 2）。

## 主表

欄位：route 路徑 ｜ HTTP ｜ 程式位置 ｜ 現有守門 ｜ 新三段名 ｜ 現有能力點 ｜ 對應 UI ｜ 可指派範圍層 ｜ 備註。

守門縮寫：**J**＝`jwt_required`／套件 `auth_required`；**C(x)**＝`require_capability("x")`／套件 `capability_required("x")`；**G(action)**＝按 GROUP 分流的 `assert_config_capability(group, action)`（對照表 `core/plugins/system_core.py:63-79`：STORAGE_CONFIG→storage-config、SMTP→smtp-config、THIRD_PARTY_LOGIN→ldap-config、NOTIFY_CONFIG→notify_config〔create／delete 改判 update〕、ISSUE_INTEGRATE_CONFIG→issue-integrate-config、RUNTIME_CONFIG→security-policy、AI_PROVIDER_CONFIG→storage-config；其餘 group fallback `system_config.*`）；**PA**＝平台管理員（`require_platform_admin`）；**HQ**＝總部層級（SMTP／LDAP／登入政策的讀寫、log 轉發的寫）。路徑前綴一律 `/api/1.0`。

### 泛用設定端點（system-config，依 group 落到各設定頁）

| route 路徑 | HTTP | 程式位置 | 現有守門 | 新三段名 | 現有能力點 | 對應 UI | 範圍層 | 備註 |
|---|---|---|---|---|---|---|---|---|
| `/system/config/<group>/<key>` | GET | `api/system_config/routes/system_config_route.py:149` | J；service：AI／Drive 鎖 PA、共用設定鎖 HQ、AI_QUOTA 拒絕 | `system.<group 對應資源>.read` | **無** | `SmtpConfigForm`、`LdapConfigForm`、`NotifyConfigForm`、`IssueIntegrateConfigForm`、`StorageConfigForm`、AI 服務設定（`ClassificationProfileService`）、Drive 設定（`CloudIntegrationService`） | tenant | **缺 read 守門**（先看這幾點第 1 條）；修法是 route 補 `G(read)` |
| 同上 | PUT | `system_config_route.py:160` | J＋G(update)；service 同上 | `system.<group 對應資源>.update` | 依 group：`smtp-config.update`、`ldap-config.update`、`notify_config.update`、`issue-integrate-config.update`、`storage-config.update`、`security-policy.update`；Drive／未登記 group＝`system_config.update` | 同上 | tenant | 四設定頁的儲存都打這條 |
| 同上 | DELETE | `system_config_route.py:173` | J＋G(delete) | `system.<group 對應資源>.delete` | 依 group 的 `.delete`；NOTIFY_CONFIG 改判 `notify_config.update` | 無 UI 直接呼叫 | tenant | |
| `/system/configs/<group>` | GET | 套件 `jedi_system_core/api/routes/system_config_route.py:39` | J；service 同上 | `system.<group 對應資源>.read` | **無** | `SmtpConfigManage` | tenant | **缺 read 守門** |
| `/system/config/<uid>` | GET | 套件 `system_config_route.py:54` | J；service 同上 | `system.<group 對應資源>.read` | **無** | `SmtpConfigManage` | tenant | **缺 read 守門**；group 由 uid 反查 |
| 同上 | PUT | 套件 `system_config_route.py:64` | J＋G(update)，group 由 uid 反查，查無退回 `system_config.update`（先 403 再 404，刻意） | `system.<group 對應資源>.update` | 依 group | `SmtpConfigManage` | tenant | |
| 同上 | DELETE | 套件 `system_config_route.py:79` | J＋G(delete) | `system.<group 對應資源>.delete` | 依 group | `SmtpConfigManage` | tenant | |
| `/system/config` | POST | 套件 `system_config_route.py:98` | J＋G(create)，group 取自 payload | `system.<group 對應資源>.create` | 依 group；NOTIFY_CONFIG 改判 `notify_config.update` | `SmtpConfigForm` | tenant | |

`system_config_route` 只驗登入是指 handler 外層；寫入三支在 handler 內呼叫 G(...)，讀取三支什麼都沒呼叫。套件與宿主兩支 `system_config_route` 的分工：套件管純 CRUD（新增、by-uid 讀改刪、by-group 列表），宿主管吃產品規則的（by-group/key 讀改刪、出廠快照還原、AI 模型下拉）；兩邊都注入同一支宿主包過的 `GuardedSystemConfigService`（`app/system_config/service/guarded_system_config_service.py`）。

### 設定頁專屬端點

| route 路徑 | HTTP | 程式位置 | 現有守門 | 新三段名 | 現有能力點 | 對應 UI | 範圍層 | 備註 |
|---|---|---|---|---|---|---|---|---|
| `/mail/test` | POST | 套件 `jedi_notification/api/routes/mail_route.py:20` | J＋C(smtp-config.update)（名稱取 `rt.config.test_mail_capability`，預設 `jedi_notification/plugin/contract.py:38`） | `system.smtp-config.update` | `smtp-config.update` | `SmtpConfigForm` | tenant | 測試信；改名時改套件預設值 |
| `/ldap/connect-test` | POST | 套件 `jedi_iam/api/routes/ldap_route.py:17` | J＋C(ldap-config.create) | `system.ldap-config.update` | `ldap-config.create` | `LdapConfigForm` | tenant | 動詞不符，定案 3 |
| `/notify-config/<channel>/test` | POST | `api/notify_config/routes/notify_config_route.py:23` | J＋C(notify_config.update) | `system.notify-config.update` | `notify_config.update` | `NotifyConfigForm` | tenant | 底線改連字號 |
| `/system/security-policy` | GET | `api/system_config/routes/security_policy_route.py:51` | J＋C(security-policy.read) | `system.security-policy.read` | `security-policy.read` | `SecurityPolicyForm` | tenant | 已守 |
| 同上 | PUT | `security_policy_route.py:59` | J＋C(security-policy.update) | `system.security-policy.update` | `security-policy.update` | 同上 | tenant | 已守；寫入另有 HQ |
| `/log-forwarding` | GET | 套件 `jedi_api_log/forwarding/api/routes/log_forwarding_route.py:49` | J＋C(log-forwarding.read)（宿主 `core/plugins/api_log.py:311` 注入） | `system.log-forwarding.read` | `log-forwarding.read` | `LogForwardingForm` | tenant | 套件 route 本身無 decorator，守門由 `ROUTE_TABLE` 逐 method 宣告、宿主注入 |
| 同上 | PUT | `log_forwarding_route.py:57` | J＋C(log-forwarding.update)＋HQ（`api_log.py:312`） | `system.log-forwarding.update` | `log-forwarding.update` | 同上 | tenant | |
| `/log-forwarding/test` | POST | `log_forwarding_route.py:73` | 同 PUT | `system.log-forwarding.update` | `log-forwarding.update` | 同上 | tenant | 套件刻意掛寫入欄（會對外送封包） |
| `/system/config/ai-provider/model-options` | GET | `system_config_route.py:100` | J＋G(read)＝`storage-config.read` | `ai.ai-provider-config.read` | `storage-config.read` | `ClassificationProfileService`、`AutoClassifyBatch` | tenant | 輔助端點；定案 2 |
| `/system/config/storage/restore-default` | GET | `system_config_route.py:119` | J＋G(read)＝`storage-config.read` | `system.storage-config.read` | `storage-config.read` | `StorageConfigForm` | tenant | 只回 flag |
| 同上 | POST | `system_config_route.py:127` | J＋G(update)＝`storage-config.update` | `system.storage-config.update` | `storage-config.update` | 同上 | tenant | 恢復出廠儲存 |
| `/labels/menu/<scope>` | GET | 套件 `jedi_issue/api/routes/label_route.py:14` | J | 跟 `platform.feedback.read`（T-1.5） | 無 | `SystemFeedbackForm`、`SystemFeedbackManage` | tenant | 輔助端點；回饋表單的下拉，**不是** issue-integrate-config |

### 系統選單字典（system-menu）

| route 路徑 | HTTP | 程式位置 | 現有守門 | 新三段名 | 現有能力點 | 對應 UI | 範圍層 | 備註 |
|---|---|---|---|---|---|---|---|---|
| `/system/menu/<group>` | GET | 套件 `jedi_system_core/api/routes/system_menu_route.py:36` | J | 豁免：全站字典 | 無 | `menuStore`、`surveyStore`、`UserManage`、`DeviceManage`、`BulletinForm` 等 10 處 | — | 套件註明刻意不守（全站下拉資料來源） |
| `/system/menus` | POST | `system_menu_route.py:53` | J＋PA | `system.system-menu.read` | 無（`system-menu.read` 沒有 route 用） | `SystemMenuManage`（`/system/system-menu-manage`） | tenant（僅 root） | 補 read 守門 |
| `/system/category-menu` | GET | `system_menu_route.py:75` | J＋PA | `system.system-menu.read` | 無 | 同上 | tenant（僅 root） | 輔助端點 |
| `/system-menu` | POST | `system_menu_route.py:94` | J＋C(system-menu.create)＋PA | `system.system-menu.create` | `system-menu.create` | 同上 | tenant（僅 root） | 名字取 `rt.config.menu_*_capability`，預設在 `jedi_system_core/plugin/contract.py:55-57` |
| `/system-menu/<id>` | PUT | `system_menu_route.py:114` | J＋C(system-menu.update)＋PA | `system.system-menu.update` | `system-menu.update` | 同上 | tenant（僅 root） | |
| 同上 | DELETE | `system_menu_route.py:126` | J＋C(system-menu.delete)＋PA | `system.system-menu.delete` | `system-menu.delete` | 同上 | tenant（僅 root） | |

### 授權照（license）

套件 `jedi_license_runtime/api/routing.py:43-57` 的 `ROUTE_TABLE` 宣告兩級：`admin`＝J＋PA（宿主 `core/plugins/license.py:325-327`），`login`＝只有 J（`license.py:332`，刻意）。

| route 路徑 | HTTP | 程式位置 | 現有守門 | 新三段名 | 現有能力點 | 對應 UI | 範圍層 | 備註 |
|---|---|---|---|---|---|---|---|---|
| `/license/status` | GET | `jedi_license_runtime/api/routes/license_route.py:64` | J（login 級） | 豁免：防死鎖 | 無 | `LicenseStatus`、`licenseStore` | — | 租戶自救；FR-076 L2 確認不可升級 |
| `/license/activation/machine-code` | GET | `license_route.py:87` | J（login 級） | 豁免：防死鎖 | 無 | `LicenseActivation` | — | 同上 |
| `/license/activation/upload` | POST | `license_route.py:104` | J（login 級） | 豁免：防死鎖 | 無 | `LicenseActivation` | — | 同上 |
| `/license/activation/online` | POST | `license_route.py:120` | J（login 級） | 豁免：防死鎖 | 無 | `LicenseActivation` | — | 同上 |
| `/license/upload` | POST | `license_route.py:48` | J＋PA | `platform.license.import` | 無 | `LicenseManage` | tenant（僅 root） | 定案 4 |
| `/license/tenants` | GET | `license_admin_route.py:42` | J＋PA | `platform.license.read`（定案 4） | 無（`license.read` 只綁選單） | `LicenseManage`（`/license/manage`） | tenant（僅 root） | |
| `/license/tenants/<uid>` | GET | `license_admin_route.py:50` | J＋PA | `platform.license.read`（定案 4） | 無 | 同上 | tenant（僅 root） | |
| `/license/tenants/<uid>/events` | GET | `license_admin_route.py:58` | J＋PA | `platform.license.read`（定案 4） | 無 | 同上 | tenant（僅 root） | |
| `/license/lc/plans` | GET | `license_admin_route.py:140` | J＋PA | `platform.license.read`（定案 4） | 無 | 同上 | tenant（僅 root） | 輔助端點（請照表單下拉） |
| `/license/tenants/<uid>/assign` | POST | `license_admin_route.py:66` | J＋PA | `platform.license.assign` | 無 | 同上 | tenant（僅 root） | 指派／換發 |
| `/license/tenants/<uid>/lc-issue` | POST | `license_admin_route.py:106` | J＋PA | `platform.license.assign` | 無 | 同上 | tenant（僅 root） | 向 License Center 請新照＝發給租戶 |
| `/license/tenants/<uid>/lc-extend` | POST | `license_admin_route.py:127` | J＋PA | `platform.license.update` | 無 | 同上 | tenant（僅 root） | 展延 |
| `/license/tenants/<uid>/lock` | POST | `license_admin_route.py:80` | J＋PA | `platform.license.update` | 無 | 同上 | tenant（僅 root） | 停權 |
| `/license/tenants/<uid>/unlock` | POST | `license_admin_route.py:93` | J＋PA | `platform.license.update` | 無 | 同上 | tenant（僅 root） | 解除停權 |

### 支援頁、版本、安裝精靈

| route 路徑 | HTTP | 程式位置 | 現有守門 | 新三段名 | 現有能力點 | 對應 UI | 範圍層 | 備註 |
|---|---|---|---|---|---|---|---|---|
| `/support/recent-errors` | GET | `api/support/routes/recent_error_route.py:35` | J；service PA（`app/support/service/recent_error_app_service.py:46`） | `platform.support.read`（定案 5） | 無 | `SupportPage`（`/system/support`） | tenant（僅 root） | 選單卻綁 `storage-config.*` |
| `/support/diagnostic-bundle` | POST | `api/support/routes/diagnostic_bundle_route.py:37` | J；service PA（`diag_bundle_export_service.py:75`） | `platform.support.export`（定案 5） | 無 | `SupportPage` | tenant（僅 root） | 匯出診斷包 |
| `/version` | GET | `api/version/routes/version_route.py:28` | 無 | 豁免：免登入 | 無 | 全站（關於頁） | — | |
| `/csp-report` | POST | `api/version/routes/browser_report_route.py:25` | 無 | 豁免：免登入 | 無 | 瀏覽器自動送 | — | 只記 log |
| `/setup/status` | GET | `api/setup/routes/setup_route.py:91` | 無；帶有效 setup token 才多回機器指紋 | 豁免：安裝精靈 | 無 | `SetupService`（`views/setup`） | — | 見下方結論 |
| `/setup/provision` | POST | `setup_route.py:113` | setup token（`_require_setup_token`）＋已開通即 409 | 豁免：安裝精靈 | 無 | 同上 | — | 見下方結論 |

### 使用者（iam.user）

| route 路徑 | HTTP | 程式位置 | 現有守門 | 新三段名 | 現有能力點 | 對應 UI | 範圍層 | 備註 |
|---|---|---|---|---|---|---|---|---|
| `/users/menu` | GET | 套件 `jedi_iam/api/routes/user_route.py:67` | J | 豁免：跨模組挑人下拉（定案 6） | 無 | `menuStore.fetchUserMenu`：`ProjectCreateView`、`ProjectPlanningView`、`ModuleFramePartiesPanel`、`SspBasicSection`、`InformationSystemManage`、`ProjectParticipantsPanel` 等 | — | 只回名字 |
| `/users` | POST | `user_route.py:80` | J＋C(user.read) | `iam.user.read` | `user.read` | `UserManage`（`/auth/user-manage`）、`PartyLinkDialog` | tenant | |
| `/user` | POST | `user_route.py:107` | J＋C(user.create) | `iam.user.create` | `user.create` | `UserMTRBACForm` | tenant | payload 帶 `user_roles`（指派），見 A.3 欄位位置 |
| `/user/<uid>` | GET | `user_route.py:129` | J；handler 內：本人放行、他人要 `user.read` | `iam.user.read`（本人豁免） | `user.read` | `UserMTRBACForm`、`UserProfileForm` | tenant | 刻意不能改 decorator（本人 profile 也用） |
| 同上 | PUT | `user_route.py:154` | J＋C(user.update) | `iam.user.update` | `user.update` | `UserMTRBACForm` | tenant | 同 POST 帶 `user_roles` |
| 同上 | DELETE | `user_route.py:172` | J＋C(user.delete) | `iam.user.delete` | `user.delete` | `UserManage` | tenant | |
| `/user/change-status/<uid>` | POST | `user_route.py:211` | J＋C(user.create) | `iam.user.update` | `user.create` | `UserManage` | tenant | 動詞不符：停用／啟用是改狀態不是新增 |
| `/user/web-menu` | GET | `user_route.py:186` | J | 豁免：本人 | 無 | `AppTopbar`、`Login`、`RoleForm` 等 | — | 回本人側邊選單＋`capabilities` |
| `/user-profile/<uid>` | PUT | `user_route.py:224` | J；非本人 401 | 豁免：本人 | 無 | `UserProfileForm` | — | 角色／租戶／部門不採信 payload；擋他人回 401 而非 403 |
| `/user-mfa-type` | PUT | `user_route.py:278` | J；只改本人 | 豁免：本人 | 無 | `UserProfileForm` | — | |
| `/users/upload` | POST | `user_import_route.py:27` | J＋C(user.create) | `iam.user.import` | `user.create` | `UserManageUpload` | tenant | 解析 Excel |
| `/user/validate` | POST | `user_import_route.py:41` | J＋C(user.create) | `iam.user.import` | `user.create` | 同上 | tenant | |
| `/user/batch-upload` | POST | `user_import_route.py:74` | J＋C(user.create) | `iam.user.import` | `user.create` | 同上 | tenant | |
| `/user/download/sample` | GET | `user_import_route.py:57` | J＋C(user.read) | `iam.user.import` | `user.read` | 同上 | tenant | 範本下載跟匯入走 |
| `/auth-providers/user/<user_uid>` | GET | `user_auth_provider_route.py:20` | J | `iam.user.read`（本人豁免） | 無 | `UserProfileForm` | tenant | **無本人檢查**，缺口 2 |
| `/auth-provider` | POST | `user_auth_provider_route.py:49` | J；service：本人或 `user.update`（`user_auth_provider_service.py:38-52`） | `iam.user.update`（本人豁免） | `user.update` | `UserProfileForm` | tenant | 綁 LDAP／AD |
| `/auth-provider/<uid>` | DELETE | `user_auth_provider_route.py:31` | 同上 | `iam.user.update`（本人豁免） | `user.update` | 同上 | tenant | 解綁 |

### 角色（iam.role）

| route 路徑 | HTTP | 程式位置 | 現有守門 | 新三段名 | 現有能力點 | 對應 UI | 範圍層 | 備註 |
|---|---|---|---|---|---|---|---|---|
| `/roles/menu` | GET | `jedi_iam/api/routes/role_route.py:26` | J＋C(role.read) | `iam.role.read` | `role.read` | `useTenantScopedMenus`（`UserMTRBACForm`、`UserManageUpload`） | tenant | 輔助端點；只有 `user.create` 沒 `role.read` 的人在使用者表單拿不到角色下拉 |
| `/roles` | POST | `role_route.py:38` | J＋C(role.read) | `iam.role.read` | `role.read` | `RoleManage`（`/auth/role-manage`） | tenant | |
| `/role` | POST | `role_route.py:61` | J＋C(role.create) | `iam.role.create` | `role.create` | `RoleForm` | tenant | payload `capabilities`：能力點 id 陣列 |
| `/role/<uid>` | GET | `role_route.py:78` | J＋C(role.read) | `iam.role.read` | `role.read` | `RoleForm` | tenant | |
| 同上 | PUT | `role_route.py:88` | J＋C(role.update) | `iam.role.update` | `role.update` | `RoleForm` | tenant | |
| 同上 | DELETE | `role_route.py:101` | J＋C(role.delete) | `iam.role.delete` | `role.delete` | `RoleManage` | tenant | |
| `/role/status/<uid>` | PUT | `role_route.py:113` | J＋C(role.update) | `iam.role.update` | `role.update` | `RoleManage` | tenant | |
| `/capabilities/menu` | GET | `capability_route.py:13` | J | `iam.role.read` | 無 | `menuStore.fetchCapabilityMenu`：`RoleForm`、`UserMTRBACForm` | tenant | 輔助端點（角色矩陣欄位）；定案 6 |
| `/ui-routes` | GET | `ui_route_route.py:13` | J | `iam.role.read` | 無 | `menuStore.fetchUiRoutes`：`RoleForm` | tenant | 輔助端點（角色矩陣列）；定案 6 |

### 部門（iam.org-unit）

| route 路徑 | HTTP | 程式位置 | 現有守門 | 新三段名 | 現有能力點 | 對應 UI | 範圍層 | 備註 |
|---|---|---|---|---|---|---|---|---|
| `/org-units/menu` | GET | `jedi_iam/api/routes/org_unit_route.py:23` | J | 豁免：跨模組挑部門下拉（定案 6） | 無 | `OrgUnitManage`、`ModuleFramePartiesPanel`、`ProjectPlanningView`、`BulletinForm`、`EntityPickerDialog` 等 | — | |
| `/org-units` | POST | `org_unit_route.py:36` | J＋C(department.read) | `iam.org-unit.read` | `department.read` | `OrgUnitManage`（`/department/department-manage`）、`PartyLinkDialog` | org_unit | |
| `/org-unit` | POST | `org_unit_route.py:61` | J＋C(department.create) | `iam.org-unit.create` | `department.create` | `OrgUnitManage` | org_unit | |
| `/org-unit/<uid>` | GET | `org_unit_route.py:77` | J＋C(department.read) | `iam.org-unit.read` | `department.read` | 同上 | org_unit | |
| 同上 | PUT | `org_unit_route.py:86` | J＋C(department.update) | `iam.org-unit.update` | `department.update` | 同上 | org_unit | |
| 同上 | DELETE | `org_unit_route.py:98` | J＋C(department.delete) | `iam.org-unit.delete` | `department.delete` | 同上 | org_unit | |

### 租戶（iam.tenant）

| route 路徑 | HTTP | 程式位置 | 現有守門 | 新三段名 | 現有能力點 | 對應 UI | 範圍層 | 備註 |
|---|---|---|---|---|---|---|---|---|
| `/tenants/menu` | GET | `jedi_iam/api/routes/tenant_route.py:26` | J | `iam.tenant.read` | 無 | `menuStore.fetchTenantMenu`：`TenantManage`、`UserManage`、`RoleManage`、`OrgUnitManage` | tenant | 輔助端點；呼叫者全是管理頁，可跟 read |
| `/tenants` | POST | `tenant_route.py:38` | J＋C(tenant.read) | `iam.tenant.read` | `tenant.read` | `TenantManage`（`/tenant/tenant-manage`）、`AiUsageSummary` | tenant | |
| `/tenant` | POST | `tenant_route.py:59` | J＋C(tenant.create)；建 root 租戶另加 PA | `iam.tenant.create` | `tenant.create` | `TenantManage` | tenant | |
| `/tenant/<uid>` | GET | `tenant_route.py:93` | J＋C(tenant.read) | `iam.tenant.read` | `tenant.read` | 同上 | tenant | |
| 同上 | PUT | `tenant_route.py:103` | J＋PA | `iam.tenant.update` | 無（`tenant.update` 沒有 route 用） | 同上 | tenant | 定案 7 |
| 同上 | DELETE | `tenant_route.py:115` | J＋C(tenant.delete) | `iam.tenant.delete` | `tenant.delete` | 同上 | tenant | 禁刪自己所在租戶 |

### 免登入與本人端點（豁免）

| route 路徑 | HTTP | 程式位置 | 現有守門 | 豁免理由 |
|---|---|---|---|---|
| `/login` | POST | `jedi_iam/api/routes/login_route.py:37` | 無 | 免登入：登入第一關 |
| `/refresh` | POST | `login_route.py:86` | refresh token | 免登入：換發 access token |
| `/logout` | POST | `login_route.py:97` | J | 本人 |
| `/otp-verify` | POST | `otp_route.py:112` | `mfa_token` | 免登入：登入第二關 |
| `/otp-resend` | POST | `otp_route.py:147` | `mfa_token`＋節流 | 免登入：登入第二關 |
| `/otp-qrcode/<uid>` | GET | `otp_route.py:76` | 簽章短期憑證，path uid 須等於發放者 | 免登入：`<img>` 帶不了 bearer |
| `/otp-qrcode-token` | GET | `otp_route.py:67` | J；只發給本人 | 本人 |
| `/totp-secret` | POST | `otp_route.py:45` | J；只取本人 | 本人 |
| `/totp-verify` | POST | `otp_route.py:99` | J；只驗本人 | 本人 |
| `/forget-password` | POST | `change_password_route.py:47` | 無 | 免登入：忘記密碼 |
| `/user/change-pwd-req/<uid>` | GET | `change_password_route.py:84` | 無；憑證即請求 uid，service 驗有效期 | 免登入：重設密碼信連結 |
| `/user/change-pwd-by-req` | POST | `change_password_route.py:20` | 無；憑證即請求 uid | 免登入：重設密碼 |
| `/version`、`/csp-report`、`/setup/status`、`/setup/provision` | — | 見上表 | — | 免登入／安裝精靈 |
| `/license/status`、`/license/activation/*`（3） | — | 見上表 | J | 防死鎖：只驗登入是刻意 |
| `/user/web-menu`、`/user-profile/<uid>`、`/user-mfa-type` | — | 見上表 | J | 本人 |
| `/system/menu/<group>` | GET | 見上表 | J | 全站字典 |

## setup_route 結論：刻意豁免

**登記為豁免，不是漏網。** 依據：

1. **檔頭明文宣告**未登入可達的理由（`api/setup/routes/setup_route.py:1-50`）：首次啟動時一個帳號都沒有，JWT 不可用；改以 install.sh 印在終端的一次性設定碼（`X-Setup-Token`）當身分證據；端點刻意只留兩支。
2. **有「已安裝就拒絕」的閘門，且 fail-closed**：`POST /setup/provision` 先過 `_require_setup_token()`（`setup_route.py:62-76`），service 第一步 `_assert_not_initialized()`（`app/setup/service/setup_wizard_service.py:167`、`:302-313`）——已開通回 409，DB 查不出狀態時也當已開通擋下。判據是 DB 內是否已有業務租戶使用者，不是記憶體旗標，重啟不失效。
3. **token 驗證 fail-closed**：`common/setup/setup_token.py:81-97`，磁碟上沒有 token 檔一律 False，timing-safe 比對。
4. **`GET /setup/status` 分級**：無 token 只回 `is_initialized` 與 `deployment_mode`（開站看畫面就知道的資訊），機器指紋要有效 token 才給（`setup_route.py:100-104`）。
5. **授權照豁免是有意的**：不掛 `@require_license`，並在 `common/middleware/license_readonly_mw.py` 的 `_EXEMPT_PREFIXES` 補了 `/setup/`（裝機當下必然無照）。

新名：不開能力點，在能力點登記表記為「豁免：安裝精靈（setup token＋已開通即封）」。

## 現有能力點反查（DEV 45 個）

| 舊名 | 主表有 route 守它？ | 新名 | 其他引用處 |
|---|---|---|---|
| `smtp-config.create/read/update/delete` | create／update／delete 經 G 分流；`.update` 另守 `/mail/test`；`.read` **無** | `system.smtp-config.*` | 宣告在 `jedi_notification/plugin/contract.py:28-33`；綁 `/system/smtp-config-manage`；基礎設施豁免 |
| `ldap-config.create/read/update/delete` | `.create` 守 `/ldap/connect-test`；create／update／delete 經 G（THIRD_PARTY_LOGIN）；`.read` **無** | `system.ldap-config.*` | 宣告在 `jedi_iam/plugin/contract.py:72-75`；綁 `/system/ldap-config`；基礎設施豁免 |
| `notify_config.read` | **無** | `system.notify-config.read` | 宣告在 `core/host_capabilities.py:54`；綁 `/system/notify-config` |
| `notify_config.update` | 有：通知測試、G 分流（create／delete 也改判它） | `system.notify-config.update` | `core/host_capabilities.py:55`；`_ACTION_ALIAS`（`system_core.py:84`）改名後要拿掉 |
| `security-policy.read/update` | 有：專屬端點；update 另經 G（RUNTIME_CONFIG） | `system.security-policy.*` | 宣告在 `jedi_iam/plugin/contract.py:76-82`；綁 `/system/security-policy`；基礎設施豁免 |
| `log-forwarding.read/update` | 有：三支轉發端點 | `system.log-forwarding.*` | 宣告在 `jedi_api_log/forwarding/plugin/contract.py:33-35`；宿主名在 `core/plugins/api_log.py:311-312`；基礎設施豁免 |
| `issue-integrate-config.create/read/update/delete` | create／update／delete 經 G；`.read` **無** | `system.issue-integrate-config.*` | 宣告在 `jedi_issue/plugin/contract.py:44-47`（is_platform）；綁 `/system/issue-integrate-config` |
| `system_config.create/update/delete` | 經 G 的 fallback：未登記 group 的寫入（Drive 設定頁實際走這條） | `system.system-config.*`（定案 1） | 宣告在 `scripts/init/04-seed-core.sql:198-200`、`scripts/sql/2026-07-07-fr048-phase4a-capability-seed.sql`；**不綁任何選單**；只有 Administrator 持有 |
| `system-menu.create/update/delete` | 有 | `system.system-menu.*` | 宣告在 `jedi_system_core/plugin/contract.py:33-50` |
| `system-menu.read` | **無**（管理端讀取只守 PA） | `system.system-menu.read` | 綁 `/system/system-menu-manage` |
| `license.read` | **無**（管理端只守 PA） | `platform.license.read`（定案 4） | 宣告在 `jedi_license_runtime/plugin/contract.py:25-27`；綁 `/license/manage` |
| `user.create` | 有：新增、改狀態（語意不符）、匯入三支 | `iam.user.create`／`.import`／`.update` | 宣告在 `jedi_iam/plugin/contract.py:54-57`；綁 `/auth/user-manage` |
| `user.read/update/delete` | 有 | `iam.user.*` | `user.update` 另在 service 層當綁定第三方身分的放行條件（`user_auth_provider_service.py`） |
| `role.create/read/update/delete` | 有 | `iam.role.*` | `jedi_iam/plugin/contract.py:60-63`；綁 `/auth/role-manage` |
| `department.create/read/update/delete` | 有 | `iam.org-unit.*` | `jedi_iam/plugin/contract.py:68-71`；綁 `/department/department-manage` |
| `tenant.create/read/delete` | 有 | `iam.tenant.*` | `jedi_iam/plugin/contract.py:64-67`；綁 `/tenant/tenant-manage` |
| `tenant.update` | **無**（PUT 只守 PA） | `iam.tenant.update` | 綁 `/tenant/tenant-manage` |
| `storage-config.*`（4，T-1.3 範圍） | 本範圍：AI 模型下拉、出廠還原、G 分流（STORAGE_CONFIG、AI_PROVIDER_CONFIG） | 見定案 2：儲存頁 `system.storage-config.*`、AI 頁 `ai.ai-provider-config.*` | 綁 `/system/storage-config`、`/system/ai-service-config`、`/system/drive-app-config`、`/system/support` 四頁 |

### 沒有任何 route 用到的能力點（孤兒）

`smtp-config.read`、`ldap-config.read`、`notify_config.read`、`issue-integrate-config.read`、`system-menu.read`、`license.read`、`tenant.update`，共 7 個。全部有綁選單、決定「選單看不看得到」，只是端點沒跟著守——前四個正是〈先看這幾點〉第 1 條的讀取缺口。卡上說的「`system_config.*` 三個都是孤兒」不成立：它們沒有綁選單，但寫入守門會 fallback 到它們。

### 不販售資源（`TENANT_ADMIN_EXCLUDED_RESOURCE_TYPES`）

`core/plugins/identity.py:89-95` 列的五個裡，DEV 只有 `workflow` 還有能力點（`workflow.create/read/update/delete`，T-1.1 已定刪除）；`cruise-project`、`dashboard`、`resource`、`report` 在 DEV 都沒有任何能力點。workflow 刪掉後這份清單可以清空。

## 給 A.3 的欄位位置（角色與指派 payload 裡帶能力點的地方）

- `POST /role`、`PUT /role/<uid>` 的 request：`capabilities: List[int]`——能力點 **id** 陣列（`jedi_iam/api/serializers/role.py:13`）。改成三段式名或 pattern 時這裡的型別要變。
- 角色 response：`capabilities: List[CapabilityResponse]`，每筆 `id／name／description／resource_type／action／is_platform`（`role.py:43`、`capability.py`）。
- `GET /capabilities/menu`：同 `CapabilityResponse` 陣列，角色矩陣的欄。
- `GET /ui-routes`：route 樹，每個節點帶 `route_capabilities`、`capabilities_count`（`serializers/ui_route.py:23-26`），角色矩陣的列。
- `GET /user/web-menu`：每個選單項帶 `operations: List[str]` 與 `capabilities: List[Dict]`（`serializers/user.py:136-142`）。
- `POST /user`、`PUT /user/<uid>` 的 request：`user_roles: List[{role_uid, tenant_id, org_unit_id, scope, is_default, start_at, end_at}]`（`serializers/user_role.py:4-11`）——這就是指派。`UserProfileRequest` 刻意不收它。

## 確認要補的守門缺口

第 1～4 條併入 T-4.6（CM-2542），在該卡編為第 6～8 條（第 3、4 兩條守錯動詞合成一條）；第 5 條屬改名時順手修，由 A.2／A.4 帶走，不併。

1. **設定讀取三支沒有 read 守門**：`GET /system/config/<group>/<key>`（`system_config_route.py:149`）、套件 `GET /system/configs/<group>`、`GET /system/config/<uid>`。修法對稱寫入側：route 呼叫 `assert_config_capability(group, "read")`（by-uid 用 `resolve_group_for_uid`）。注意 `/system/configs/<group>` 是套件 route，要動 jedi-system-core。上線前確認出貨角色範本都帶了各設定頁的 read。
2. **第三方登入綁定的查詢無本人檢查**：`get_user_auth_providers` 補呼叫 `_assert_caller_can_manage(user_uid)`（jedi-iam 套件內）。
3. **改狀態守錯動詞**：`/user/change-status/<uid>` 從 `user.create` 改為 `iam.user.update`。
4. **LDAP 連線測試守錯動詞**：`/ldap/connect-test` 從 `ldap-config.create` 改為 `system.ldap-config.update`。
5. **管理端讀取只守平台管理員、沒守 read**：`/system/menus`、`/system/category-menu` 補 `system.system-menu.read`；license 管理端照定案 4 補 `platform.license.*`（平台管理員那道保留）。

## 定案結論

決策者 2026-10-06 逐條裁決。

1. **`system.system-config` 保留並補 `read`**；`GOOGLE_DRIVE_APP_CONFIG` 正式登記進 GROUP 對照表，Drive 設定頁不再靠 fallback。fallback 只當「未登記 group 的兜底」。理由：fallback 不能拿掉（未登記 group 要有最嚴的門），但原本 Drive 頁「選單綁 storage-config、儲存要 system_config」是兩套門鎖，靠平台管理員那道才沒出事。
2. **storage-config 一顆開五門，全拆。** 儲存設定頁 → `system.storage-config.*`；AI 服務設定頁＋AI 證據分類設定 → `ai.ai-provider-config.*`（新開，歸 T-1.5 定案的 `ai` 模組）；Google Drive 應用程式憑證 → `platform` 層（原廠，具體資源名由 T-1.6 定）；支援頁 → `platform.support.*`（T-1.5 已裁）。storage-config 歸屬爭議結論：**system**。理由：五頁鎖的層級不同（AI、Drive 鎖平台管理員，儲存不鎖），共用一顆能力點無法分別授權。
3. **設定頁「測試送出」四支（測試信、通知測試、log 轉發測試、LDAP 連線測試）一律歸該資源 `update`，不開 `send`。** 理由：沒有客戶要「能改設定但不能測」；log 轉發套件也已刻意把測試掛在寫入欄。
4. **授權照原廠管理端 9 支移到 `platform.license.*`，不留 system。** 動詞：`read`、`assign`（指派、向 LC 請新照）、`update`（停權、解除、展延）、`import`（上傳照檔），`is_platform = true`；後端平台管理員判定保留再加能力點，兩道都過。租戶自救 5 支維持只驗登入（防死鎖，不變）。理由：這是原廠對租戶的操作，放 system 會讓客戶以為自己能管授權。
5. **支援頁已隨 T-1.5 第 5 條裁定 `platform.support.read`／`.export`**，本卡只引用不重裁。
6. **`/users/menu`、`/org-units/menu` 豁免**（跨模組挑人挑部門，只驗登入）；`/capabilities/menu`、`/ui-routes` → `iam.role.read`；`/tenants/menu` → `iam.tenant.read`。`/roles/menu` 守 `role.read` 的副作用（只有 `user.create` 的人在使用者表單拿不到角色下拉）留 A.3 處理。
7. **改租戶 PUT 改守 `iam.tenant.update`，平台管理員那道保留**（兩道都過）；A.3 實作時先查清 service 是否會動 `parent_id`，再決定能否拿掉。
8. **部門資源名用 `iam.org-unit`，不用 `department`。** 決策者原話：「跟程式統一才不會亂掉，以前已經改版過」。連帶：舊名 `department.*` → `iam.org-unit.*`；畫面顯示名由前端 i18n 處理，A.5 帶走。

沿革見 `FR-132-LOG.md` 與 git log。
