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

卡片 CM-2514(FR-132 T-1.4)。唯讀盤點,不改程式、不改 DB。

§1

範圍與數量

  • 範圍: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 用到的部分)。
§2

先看這幾點

  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 結論〉。
§3

新名總覽(定案)

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)。
§4

主表

欄位: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 全站字典
§5

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+已開通即封)」。

§6

現有能力點反查(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 刪掉後這份清單可以清空。

§7

給 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 刻意不收它。
§8

確認要補的守門缺口

第 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.*(平台管理員那道保留)。
§9

定案結論

決策者 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。