LDAP 設定(LDAP Configuration)¶
功能群:系統管理|
system_configskey-value 慣例先讀 功能群總覽 §4事實基準:2026-07-05 從 FE
src/views/ldap-config/LdapConfigForm.vue+ BEapi/system_config//api/user_auth_provider/+ jedi-system-config / jedi-login 套件 + routes.json / db_schema.json 掃出。
變更紀錄¶
| 日期 | FR | 變更 |
|---|---|---|
| 2026-08-29 | CM-1418 | 共用設定寫入路徑四顆連環雷修正(存檔全壞):① 共用設定 ROOT 寫入路徑期待 {"value": {...}} 外殼,但 FE 與舊路徑的契約一律是「payload 即設定內容本體」→ 存檔回 400「缺少 value」,改由 BE 配合既有契約(不動 FE);② 新路徑漏了 changePwd 密碼保留邏輯 → 不改密碼存檔會弄丟既有密碼,補上並改讀共用設定的權威來源列;③ changePwd=false 時抄既有 secret 的寫法假設該欄位必定存在 → 「還沒填過密鑰」的設定列第一次存檔炸 500 KeyError: 'secret'(修在 jedi-system-config 0.0.15);④ system_configs 的 INSERT RLS policy 漏了 super_admin 逃生口(SELECT/UPDATE/DELETE 都有)→ 新裝機器第一次存 LDAP 必炸(出貨基線刻意不帶 LDAP 列,故每台新機必踩;DEV 因歷史列恆走 UPDATE 分支而測不出來) |
1. 功能描述¶
LDAP 設定頁是第三方登入(LDAP / Active Directory)的連線參數維護頁:管理員在此設定企業 LDAP 或 AD 伺服器資訊,設定值同時是登入流程(app/auth/service/login_service.py::init_config())在使用者選擇 LDAP 登入時實際套用的參數來源——不是獨立於登入之外的靜態設定。頁面只有單一表單,無列表、無分頁。
主要使用者:系統管理員(頁面本身無角色守門,見 §3)。
角色速覽:
| 角色 | 一句話 |
|---|---|
| 系統管理員(可見本選單者) | 維護 LDAP/AD 連線設定、測試連線 |
| 一般使用者 | 無此選單(ui_routes 可見性未命中);但可直接打 API(見 §3 ⚠️) |
1.1 功能總覽(本頁全部功能)¶
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 載入既有設定 | 進頁自動 GET 目前的 LDAP 設定回填表單 | 整頁(onMounted) |
§6.1 |
| 2 | 伺服器欄位 | 必填,LDAP/AD 伺服器 IP 或 FQDN | 表單 | §6.1 |
| 3 | 埠號欄位 | 必填,數字輸入,預設 389 |
表單 | §6.1 |
| 4 | Base DN 欄位 | 必填,LDAP 查詢起點 DN | 表單 | §6.1 |
| 5 | LDAP 類型下拉 | 二選一:Microsoft AD / LDAP(即 OpenLDAP),對應 is_ad 布林 |
表單 | §9.3 |
| 6 | 加密方式下拉 | 三選一:無 / SSL / STARTTLS |
表單 | §9.3 |
| 7 | 認證帳號欄位 | 選填,LDAP bind 用的服務帳號 | 表單 | §6.1 |
| 8 | 密碼欄位 | 選填,input type="password";留白送出=沿用舊密碼(見 §6.1 changePwd) |
表單 | §6.1 |
| 9 | 連線測試 | 用表單目前值即時測試 LDAP/AD bind,不落地儲存 | 表單底部按鈕 | UC-LDAP-02 |
| 10 | 儲存設定 | 寫回 system_configs(THIRD_PARTY_LOGIN/LDAP) |
表單底部按鈕 | UC-LDAP-01 |
| 11 | 離開頁面確認 | 表單有異動時離開觸發確認 dialog(onBeforeRouteLeave) |
全域 | — |
UC(§2)展開 1(載入)已併入儲存流程圖;核心路徑為 UC-LDAP-01(儲存)與 UC-LDAP-02(連線測試),兩者共用同一張流程圖。
2. Use Case¶
角色速覽:
| 角色 | 一句話 |
|---|---|
| 系統管理員(可見本選單者) | 維護 LDAP/AD 連線設定、測試連線 |
| 一般使用者 | 無此選單(ui_routes 可見性未命中);但可直接打 API(見 §3 ⚠️) |
視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點。
UC-LDAP-01 儲存 LDAP 設定¶
| 項目 | 內容 |
|---|---|
| 角色 | 系統管理員(BE 僅檢查登入,見 §3) |
| 前置條件 | 已登入;server / port / base_dn 已填 |
| 產出 / 後置條件 | public.system_configs(group=THIRD_PARTY_LOGIN, key=LDAP)新增或更新一列;密碼欄未勾異動則保留舊值 |
UC-LDAP-02 連線測試¶
| 項目 | 內容 |
|---|---|
| 角色 | 系統管理員(BE 僅檢查登入) |
| 前置條件 | 已登入;表單必填欄位已填(不需先儲存) |
| 產出 / 後置條件 | 無資料異動,純測試 bind;成功回 200、失敗回 401(LOGIN_401014) |
3. 權限矩陣¶
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 進入本頁 | ui_routes web-menu 可見性 |
無端點層強制(僅 @jwt_required) |
api/system_config/routes/system_config_route.py:42,109 |
| 讀取設定 | 有選單即可 | ⚠️ 僅 @jwt_required,無角色 / 能力守門 |
SystemConfigGroupRoute.get(system_config_route.py:96-104) |
| 儲存設定 | 有選單即可 | ⚠️ 僅 @jwt_required,無角色 / 能力守門 |
SystemConfigGroupRoute.put(system_config_route.py:106-119) |
| 連線測試 | 有選單即可 | ⚠️ 僅 @jwt_required,無角色 / 能力守門 |
LdapConnectTestRoute.post(api/user_auth_provider/routes/ldap_route.py:12-24) |
⚠️ 本頁三個端點全部僅
@jwt_required(),無任何角色 / 平台管理員守門——任何已登入使用者(不限系統管理員)直接打 API 都能讀取 / 覆寫全系統 LDAP 連線設定(含觸發連線測試)。與 _overview §5 一致(本群多頁通病)。本 spec 只記錄不修,收進 §12。
4. 狀態機與前置條件¶
本頁無狀態機,僅表單前端驗證與後端密碼保留邏輯:
| 條件 | 效果 | 出處 |
|---|---|---|
server / port / base_dn 任一為空 |
前端阻擋送出(連線測試與儲存皆同一驗證),focus 錯誤欄位 | LdapConfigForm.vue:30-42, 95-107, 128-139 |
儲存時 changePwd=false(密碼欄未手動改過) |
BE 用資料庫既有 secret 覆蓋 payload,不寫入前端傳來的空值 |
jedi-system-config system_config_domain_service.py:118-132(update_config_by_group_key) |
連線測試時 payload 無 secret 但有 user |
BE 沿用資料庫既有 secret 做測試(避免每次測試都要重打密碼) |
app/user_auth_provider/service/ldap_service.py:32-45(init_config) |
| 設定不存在(首次進頁) | BE get_system_config_by_key 找不到會 raise NotFound;FE getConfig() 未特別 catch(見 §12 坑 1) |
system_config_service.py:31-39 |
5. UI 設計¶
📸 實機截圖待補(亮色模式,另有專責 session)。目前以版面骨架圖示意。待補清單:初次進頁空表單狀態、已有設定回填狀態、連線測試成功 / 失敗 toast、必填欄位驗證錯誤紅框狀態。
版面骨架(單卡片表單,無 tab、無列表):
狀態呈現:loading 期間顯示 LoadingState(page size)取代表單;密碼欄為原生 <input type="password">(非 PrimeVue Password 元件),change 時同時標記 hasChange 與 passwordHasChange;儲存 / 測試按鈕在送出期間顯示 loading spinner(submitted state)。無 socket、無多人協作提示。
6. API 規格¶
Envelope:成功 {"code": 1, "data": {...}}、失敗 {"code": 0, "msg": "...", "data": {...}}(見 CLAUDE.md Response Format)。
6.1 Endpoint 總清單(本頁呼叫的全部 endpoint)¶
| 分類 | Method + Path | 說明 | 詳述 |
|---|---|---|---|
| 讀取 | GET /api/1.0/system/config/<group>/<key> |
讀取 LDAP 設定(group=THIRD_PARTY_LOGIN, key=LDAP),回應剔除 secret |
§6.2 |
| 儲存 | PUT /api/1.0/system/config/<group>/<key> |
更新 LDAP 設定(不存在則新增) | §6.2 |
| 連線測試 | POST /api/1.0/ldap/connect-test |
用送入的參數即時測試 LDAP/AD bind,不落地 | §6.3 |
endpoint 存在性對
routes.json核實(system-config.systemconfiggrouproute/user_auth_provider.ldapconnecttestroute)。group/key常數值取自 FE 呼叫點LdapConfigForm.vue:86, 115(API.SYSTEM_CONFIG + '/THIRD_PARTY_LOGIN/LDAP'),非泛用/system/config全量端點。
6.2 讀取 / 儲存(system_config/<group>/<key>)¶
GET Response data(SystemConfigResponse,api/system_config/serializers/system_config.py:9-15):
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | string | 該設定列的 UUID |
| group | string | 固定 THIRD_PARTY_LOGIN |
| key | string | 固定 LDAP |
| value | dict | LDAP 參數本體(見下表),回應前若含 secret 一律剔除(system_config_route.py:18,47-49 HIDDEN_SECRET 清單含 THIRD_PARTY_LOGIN) |
| created_at / updated_at | datetime | %Y-%m-%d %H:%M:%S |
value 內部欄位(對照 LdapConfigRequest,api/user_auth_provider/serializers/ldap_config.py:4-11,及 FE currData 初值 LdapConfigForm.vue:23-29):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| server | string | 是 | LDAP/AD 伺服器 |
| port | int | 是 | 埠號,FE 預設 389 |
| base_dn | string | 是 | Base DN |
| is_ad | bool | 否 | true=Microsoft AD(NTLM 認證)、false=OpenLDAP |
| encryption | string | 否 | '' / SSL / STARTTLS |
| user | string | 否 | bind 用認證帳號 |
| secret | string | 否 | bind 密碼(GET 回應永遠不含此欄;只在 PUT/POST 送入) |
PUT Request:同上 value 欄位 + 頂層 changePwd(bool,FE 送 passwordHasChange 的值,LdapConfigForm.vue:114)——changePwd=false 時 BE 用既有 secret 覆蓋,changePwd=true 才採用送入的新密碼。出處:jedi-system-config system_config_domain_service.py:118-132(update_config_by_group_key)。
BE 鏈路:SystemConfigGroupRoute.get/put(api/system_config/routes/system_config_route.py:92-119)→ SystemConfigService.get_system_config_by_key / update_config_by_group_key(jedi-system-config app/service/system_config_service.py:32-39,142-149)→ SystemConfigDomainService.update_config_by_group_key(同套件 domain/service/system_config_domain_service.py:118-132,密碼保留邏輯 + 找不到自動新增)→ repo 寫 public.system_configs。
6.3 連線測試(POST /ldap/connect-test)¶
Request(LdapConfigRequest,同 §6.2 value 欄位形狀,無 changePwd):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| server | string | 是 | LDAP/AD 伺服器 |
| port | int | 是 | 埠號 |
| base_dn | string | 是 | Base DN |
| is_ad | bool | 否 | true=Microsoft AD(NTLM 認證)、false=OpenLDAP |
| encryption | string | 否 | '' / SSL / STARTTLS |
| user | string | 否 | bind 用認證帳號 |
| secret | string | 否 | bind 密碼 |
Response:BooleanResponse(成功恆 true;失敗直接拋例外變成非 2xx,見下)。
BE 鏈路:LdapConnectTestRoute.post(api/user_auth_provider/routes/ldap_route.py:12-24)→ LdapService.test_connect(app/user_auth_provider/service/ldap_service.py:21-30):先 init_config() 組出 LoginConfigDTO(若送入 payload 無 secret 但有 user,沿用資料庫既有 secret,ldap_service.py:38-45)→ jedi-login get_authenticate_adapter(LDAP, config) → LdapAdapter.test_connect()(~/Projects/Jedicogy/module/jedi-python-package/jedi-login/jedi_login/infra/adapter/authenticate_adapter/ldap/ldap_adapter.py:81-91):建立 ldap3.Server(依 encryption 決定 ldaps:// 前綴 / use_ssl)→ Connection.bind()(AD 用 NTLM、STARTTLS 額外呼叫 start_tls())→ bind 成功回 True,失敗(含逾時、憑證錯、伺服器不可達)一律包成 UnauthorizedError(LOGIN_401014) → HTTP 401。
已知例外碼(jedi-login common/enum/error_code.py):LOGIN_404006(LDAP_SERVER_NOT_CONFIGURED,server 空字串)、LOGIN_405003(LDAP_ACCOUNT_FORMAT_ERROR,AD 帳號非 DOMAIN\username 格式)、LOGIN_401014(LDAP_CONNECT_FAILED,test_connect() 統一捕捉的失敗碼)。error code 前綴為 LOGIN_(jedi-login 套件自有前綴,非主專案 GRC_/AUTH_,查證方式:grep ~/Projects/Jedicogy/module/jedi-python-package/jedi-login/jedi_login/common/enum/error_code.py,非 common/code/*_error_code.py——本設定的錯誤碼定義不在主專案倉庫內)。
7. 前端檔案地圖(compliance-manager-fe/)¶
| 檔案 | 角色 |
|---|---|
src/views/ldap-config/LdapConfigForm.vue |
唯一檢視:表單載入 / 驗證 / 儲存 / 連線測試 / 離開確認 |
src/config/router/index.js:317-328 |
route ldap-config(/system/ldap-config) |
src/config/api/api.js:328,339 |
SYSTEM_CONFIG / LDAP_CONNECT_TEST 端點常數 |
src/service/BaseService.js |
HTTP 包裝(get/put/post) |
src/config/locales/i18n/zh-tw/pages.json:379-406 |
ldap_config 命名空間(欄位標籤 / placeholder / 驗證訊息) |
8. 後端檔案地圖(本頁核心鏈路)¶
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| 讀取 / 儲存設定 | api/system_config/routes/system_config_route.py::SystemConfigGroupRoute |
jedi-system-config SystemConfigService.get_system_config_by_key / update_config_by_group_key |
jedi-system-config SystemConfigDomainService → SystemConfigRepoImpl → public.system_configs |
| 連線測試 | api/user_auth_provider/routes/ldap_route.py::LdapConnectTestRoute |
app/user_auth_provider/service/ldap_service.py::LdapService.test_connect |
jedi-login get_authenticate_adapter → LdapAdapter(ldap3 套件) |
| 登入時實際套用 | (非本頁 route,登入頁觸發) | app/auth/service/login_service.py::LoginService.init_config(讀同一筆 THIRD_PARTY_LOGIN/LDAP 設定) |
jedi-login LoginService.process_login → LdapAdapter.authenticate |
DDD 提醒:路由層(SystemConfigGroupRoute / LdapConnectTestRoute)皆不碰 DB,委派 app service / jedi-system-config service,符合分層規範。但三端點皆無角色守門(僅 @jwt_required)——新增本頁 API 或提升守門強度時,比照 §3 現況先跟 user 確認是否要補角色檢查,勿在不知情下複製「僅 JWT」寫法到有敏感度更高的設定頁。
9. DB¶
9.1 資料表總清單(本頁讀寫的全部表)¶
| 表 | 讀/寫 | 說明 | 詳述 |
|---|---|---|---|
public.system_configs |
讀 / 寫 | LDAP 設定實際儲存位置:group='THIRD_PARTY_LOGIN', key='LDAP', value 為 jsonb(欄位見 §6.2) |
§9.3 |
public.user_auth_providers |
讀(間接,登入時) | 使用者 ↔ LDAP 第三方登入綁定關係(provider='LDAP');本頁不直接讀寫此表,但本頁設定的伺服器參數是登入時解析此表 provider_user_id 的前提 |
§9.3 |
表結構 / 欄位 / comment 取自
scripts/deliverables/out/db_schema.json(2026-07-05 dump)。
9.2 ER 圖¶
本頁只涉及單表寫入(system_configs 為通用 key-value 表,無專屬 FK),關聯示意見 _overview §6 核心資料模型圖(system_configs 節點)。user_auth_providers 與 LDAP 設定為邏輯關聯(同一套登入流程共用參數),非資料庫層 FK 關聯,故不另畫 ER。
9.3 核心表欄位卡¶
public.system_configs(comment:「系統配置表:儲存系統設定參數,以群組和鍵值對方式組織」):
| 欄位 | 型別 | 說明 |
|---|---|---|
| id | integer | 主鍵 |
| uid | varchar(50) | 配置唯一識別碼 UUID |
| group | varchar(255) | 配置群組(本頁固定 THIRD_PARTY_LOGIN) |
| key | varchar(255) | 配置鍵名(本頁固定 LDAP) |
| value | jsonb | 配置值(本頁形狀見 §6.2,含 server/port/base_dn/is_ad/encryption/user/secret) |
| tenant_id | integer|null | 設定歸屬租戶(本頁未見 FE 傳入,實務為全域設定) |
| org_unit_id | integer|null | 設定歸屬部門(同上,本頁未使用) |
| created_at / updated_at | timestamp | 審計時間戳(表本身無 created_user/updated_user 欄位) |
public.user_auth_providers(comment:「使用者第三方登入驗證表:記錄使用者與第三方身份提供者的綁定關係」,本頁不寫,僅背景關聯說明):uid / user_id(FK→users.id CASCADE)/ provider(如 LDAP)/ provider_user_id(LDAP entryUUID 或 AD objectGUID)/ created_at / updated_at / created_user / updated_user。
10. 頁面邏輯與資料對應¶
載入時序:onMounted → getConfig() → GET /system/config/THIRD_PARTY_LOGIN/LDAP → 成功則 currData.value = res.value(secret 因 BE 剔除而為 undefined,表單密碼欄顯示空白)→ loading=false。
欄位對應(畫面 ↔ FE state ↔ API 欄位):
| 畫面元素 | FE state | API 欄位 |
|---|---|---|
| 伺服器 | currData.server |
value.server |
| 埠號 | currData.port |
value.port |
| Base DN | currData.base_dn |
value.base_dn |
| LDAP 類型下拉 | currData.is_ad |
value.is_ad |
| 加密方式下拉 | currData.encryption |
value.encryption |
| 認證帳號 | currData.user |
value.user |
| 密碼 | currData.secret + passwordHasChange |
value.secret + 頂層 changePwd |
儲存流程:onSaveClick() → v$.value.$touch() 驗證必填三欄 → 通過則 update():組 payload = {...currData.value, changePwd: passwordHasChange.value} → PUT /system/config/THIRD_PARTY_LOGIN/LDAP → 成功後重新 getConfig() 回填(不是直接用 response 蓋,而是整個重抓)、showSuccess、重置 hasChange / passwordHasChange。
連線測試流程:onTestConnectClick() → 同樣先驗證必填三欄 → POST /ldap/connect-test,body 為當前表單值(非已儲存值,允許先測後存)→ 成功 showSuccess('連線成功');失敗(.finally 只重置 submitted,無專屬 .catch,錯誤走全域攔截器 toast,見 §12 坑 2)。
錯誤對應:全域 axios 攔截器(BaseService)catch 非 2xx 轉 toast;本頁未見自訂錯誤碼對應文案,因此 LOGIN_401014 等碼會顯示 BE 給的通用 msg(中文「LDAP連線失敗」),非 i18n key 對照。
11. 背景行為與外部依賴¶
| 類型 | 內容 |
|---|---|
| 通知 | 無 |
| Job / 排程 | 無 |
| Socket | 無:純表單 CRUD,資料更新靠操作後重新 GET |
| jedi-* 套件 | jedi-system-config(system_configs 表 CRUD + 密碼保留邏輯)、jedi-login(LdapAdapter 實際 bind 測試 / 登入時驗證,ldap3 第三方套件)、jedi-auth(JWT) |
| 系統參數 | 本頁本身即系統參數維護頁;設定值被 app/auth/service/login_service.py::LoginService.init_config() 在使用者選擇 LDAP 登入方式時讀取套用(非本頁 route,跨頁依賴) |
12. 邊界情況與已知坑¶
- 三端點皆無角色守門:讀取 / 儲存 / 連線測試僅
@jwt_required(),任何登入使用者都能改動全系統 LDAP 設定或觸發連線測試(可能被用作內部網路探測工具)。與 _overview §5 一致,本 spec 只記錄不修(§3)。 - 連線測試失敗無專屬錯誤處理:FE
onTestConnectClick()只有.finally(重置 loading),沒有.catch顯示失敗訊息或聚焦欄位,錯誤 toast 完全依賴 axios 全域攔截器的通用文案;LOGIN_401014等具體原因不會被特別標示在表單上。 - 密碼「留白=沿用舊值」的判斷靠 FE flag,不是靠是否為空字串:儲存走
changePwd(passwordHasChange)欄位判斷;連線測試走「payload 無secret但有user」判斷(ldap_service.py:43-45)——兩條路徑保留舊密碼的條件不同,若日後改動任一邏輯需同步檢視另一條。 is_ad決定認證機制而非單純顯示文字:AD 用 NTLM 認證且要求帳號格式DOMAIN\username(LOGIN_405003),OpenLDAP 走匿名或 simple bind 搜尋cn=;切換下拉不會清空既有帳號欄位,格式不符會在測試 / 登入時才報錯,非選擇當下即時驗證。encryption大小寫正規化:LdapAdapter內部一律.upper()比對,FE 下拉值固定送'SSL'/'STARTTLS'(大寫),與 BE 假設一致;但若日後有其他來源寫入小寫值(如手動改 DB),連線行為(是否啟用 STARTTLS)會因正規化前的舊版本差異而有雷(見套件內註解ldap_adapter.py:41-42,73,已修過一次大小寫不一致的坑)。- 設定不存在時的行為不對稱:
GET走get_system_config_by_key找不到會NotFound(SYSTEM_CONFIG_404001);PUT走update_config_by_group_key找不到則自動新增(非報錯)。首次使用本頁時,若 GET 400/404,FEgetConfig()沒有特別 catch,會直接留預設值(server:''/port:389/ ...),使用者未必會注意到「這是全新設定而非讀取失敗」。 system_configs表本身無created_user/updated_user欄位:不同於本專案「審計欄位回傳規範」(CLAUDE.md),本表無操作者留痕,異動追蹤需另查api_logs/system_logs(見操作日誌頁)。tenant_id/org_unit_id欄位存在但本頁未使用:LDAP 設定實務上是全域(root)層級參數,FE 從未傳入這兩欄,隱含「只有一份全租戶共用的 LDAP 設定」的假設;若未來要做多租戶各自 LDAP,需連同 §6 / §9 一起重新設計(非本頁現況涵蓋範圍)。
13. 開發與驗證¶
- 跑起來:BE
python main_socketio.py(port 8000,log 在log/app.log);FE 在compliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload)。 - 測試帳號:見 memory
reference_dev_login/.env(憑證不寫入本文件);本頁無角色門檻,任一登入帳號皆可進頁測試。 - 導航路徑:登入 → 側選單「系統管理」→「LDAP 設定」(
/system/ldap-config)。 - 前置資料:無強制前置資料;首次進頁
system_configs若無THIRD_PARTY_LOGIN/LDAP列,GET 會 404(見 §12 坑 6),儲存後才會建立該列。 - 實機驗證連線測試:需要一台可連通的 LDAP/AD 測試伺服器;DEV 環境是否已備測試伺服器需另行確認(存疑項,見報告)。
- E2E:
compliance-manager-test/repo(Cucumber + Playwright);本頁相關 feature 檔以ldap搜尋(若尚無則屬待補)。 - 相關文件:
docs/claude/frontend-overview.md(BE 視角速查);GAI-SD-02/03 涵蓋常規 CRUD 端點細節。