安全政策設定(Security Policy)¶
功能群:系統管理|三層 RBAC 模型與角色權限基調先讀 功能群總覽 §2 / §5
事實基準:2026-08-29 從 FE
src/views/security-policy/SecurityPolicyForm.vue+ BEapi/system_config/routes/security_policy_route.py/app/system_config/service/security_policy_app_service.py/infra/system_config/runtime_config.py+ migrationscripts/sql/2026-08-29-cm1423-security-policy-page.sql掃出(CM-1423 為新頁,routes.jsondump 早於本功能,端點與選單位置改以源碼與 migration 核實)
變更紀錄¶
| 日期 | FR | 變更 |
|---|---|---|
| 2026-08-29 | CM-1423 | 首版:登入安全政策設定頁(強制 MFA/登入鎖定/密碼週期/憑證效期共七參數),含開啟 MFA 前的防鎖死守門(OTP 通道未就緒需明示確認)與 JWT 效期需重啟的如實提示 |
1. 功能描述¶
安全政策設定頁是全站登入規則的單一設定頁:強制兩階段驗證(MFA)開關、登入失敗鎖定、閒置停用天數、密碼有效期、登入憑證效期,共七個參數。
這些設定值早就存在(system_configs 的 ROOT 租戶列、group='RUNTIME_CONFIG',自 FR-063.1g 起),登入流程每次都即時讀取——本頁補的是管理介面,不是新的機制。在此頁之前,客戶要改任何一項都只能請原廠下 SQL。無新增資料表。
系統層全域一份設定,故無列表、無 uid、只有一張表單。
🔴 本頁最重要的性質:強制 MFA 是唯一一個「開錯就沒有人進得來」的設定。 驗證碼靠寄信送達,SMTP 沒設好時開啟 MFA,會讓包含操作者自己在內的所有帳號卡在登入第二關,且無法從任何 UI 救回(只能下 SQL)。頁面為此設計了三層防呆,見 §4。
主要使用者:具 security-policy.* 能力的系統管理員(能力點授予對象比照 SMTP 伺服器設定,兩者同屬「系統設定」選單群)。
角色速覽
| 角色 | 一句話 |
|---|---|
具 security-policy.read 者 |
看得到本選單並讀得到政策與 OTP 通道就緒度 |
具 security-policy.update 者 |
可儲存政策(開啟 MFA 另有防鎖死守門,見 §4) |
| 其他登入者 | 無此選單;直打 API 亦被 require_capability 擋下 403 |
1.1 功能總覽(本頁全部功能)¶
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 載入既有政策 | 進頁 GET /system/security-policy 回填;非 404 的讀取失敗會鎖住儲存 |
整頁(onMounted) |
§6.2 |
| 2 | 強制兩階段驗證開關 | MFA_REQUIRED。即時生效,不需重啟 |
「兩階段驗證」區 | §4 |
| 3 | OTP 通道就緒度顯示 | 進頁即顯示:就緒=綠色確認訊息;未就緒=紅色常駐警告(列出缺哪些欄位)+「前往郵件伺服器設定」按鈕 | 開關上方 | §4 |
| 4 | 開啟 MFA 的風險確認框 | 「從關到開」且通道未就緒時,儲存前再過一次紅色確認框,確認後才帶 confirm_risk=true |
儲存時 | UC-SP-02 |
| 5 | 登入失敗鎖定次數 | LOGIN_MAX_LOCK_COUNT,1–100 |
「登入與鎖定」區 | §9.3 |
| 6 | 帳號鎖定時間 | LOGIN_USER_LOCK_TIME,30–86400 秒。UI 用分鐘、BE 存秒 |
「登入與鎖定」區 | §12 坑 3 |
| 7 | 閒置停用天數 | LOGIN_INACTIVITY_THRESHOLD_DAYS,1–3650 |
「登入與鎖定」區 | §9.3 |
| 8 | 密碼有效天數 | CHANGE_PASSWORD_THRESHOLD_DAYS,1–3650 |
「密碼政策」區 | §9.3 |
| 9 | 登入憑證效期 | JWT_ACCESS_TOKEN_EXPIRES / JWT_REFRESH_TOKEN_EXPIRES,60–31536000 秒。改了要重啟服務才生效 |
「登入憑證有效時間」區 | §12 坑 1 |
| 10 | 需重啟提示 | 憑證效期區常駐提示;存檔後若真的改到再 toast 一次(由 BE restart_required_keys 驅動,不在 FE 寫死) |
該區塊 + toast | §6.2 |
| 11 | 儲存政策 | PUT,部分更新;無實際變更時不寫入也不留稽核 |
表單底部 | UC-SP-01 |
| 12 | 離開頁面確認 | 快照比對(改回原值不算髒) | 全域 | §10 |
| 13 | 授權唯讀降級 | useLicenseReadonly 命中時儲存鈕反灰並提示原因(不隱藏) |
表單底部 | §3 |
展開成 UC:UC-SP-01(儲存政策)、UC-SP-02(開啟 MFA 的防鎖死路徑)。載入、離開確認等為常規表單行為,不另展開。
2. Use Case¶
角色速覽
| 角色 | 一句話 |
|---|---|
具 security-policy.read 者 |
看得到本選單並讀得到政策 |
具 security-policy.update 者 |
可儲存政策(開 MFA 另有守門) |
| 其他登入者 | 無此選單;直打 API 403 |
UC-SP-01 儲存登入政策¶
| 項目 | 內容 |
|---|---|
| 觸發 | 管理員改完欄位按「儲存」 |
| 前置 | 具 security-policy.update;進頁 GET 未失敗(否則儲存被鎖,見 §4) |
| 主流程 | ① FE 驗數值格式 → ② PUT /system/security-policy 送整包(unknown=EXCLUDE 容忍唯讀欄位回送)→ ③ app service 逐鍵比對現值、只挑真的變了的鍵 → ④ 逐鍵寫入 ROOT 租戶列 → ⑤ 寫稽核事件 6110 → ⑥ 回傳 changed_keys 與 restart_required |
| 例外 | 數值非整數 → 400 SECURITY_POLICY_400001;超出範圍 → 400 SECURITY_POLICY_400002(不靜默夾成邊界值);寫入失敗 → 400 SYSTEM_CONFIG_SHARED_WRITE_FAILED |
| 結果 | 除 JWT 效期外即時生效(登入流程每次重讀);改到 JWT 效期則 restart_required=true,FE 額外 toast 提示需重啟 |
無實際變更時不寫入:七個欄位原封不動按儲存,回
changed_keys=[]、不寫 DB、不留稽核——避免「按了儲存但什麼都沒改」污染稽核軌跡。
UC-SP-02 開啟強制 MFA(防鎖死路徑)¶
| 項目 | 內容 |
|---|---|
| 觸發 | 管理員把「強制兩階段驗證」由關切到開並按儲存 |
| 前置 | 同 UC-SP-01 |
| 主流程 | ① 進頁時 BE 已回 otp_readiness,未就緒則畫面常駐紅色警告 → ② 按儲存時 FE 判定「從關到開 + 通道未就緒」→ 跳確認框 → ③ 使用者按「我了解風險,仍要開啟」→ 帶 confirm_risk=true 送出 → ④ app service 同樣判定,confirm_risk 為真才放行 |
| 例外 | 未帶 confirm_risk 且通道未就緒 → 412 SECURITY_POLICY_412001,資料不寫入 |
| 結果 | 開啟後下次登入即要求 OTP(即時生效,無需重啟) |
只擋「開啟」方向:關閉 MFA 是解除鎖死的動作,任何情況下都不被擋住——這是刻意的,且已實測驗證。
不是硬性禁止,是要求明示確認:客戶可能正在用驗證器 App(TOTP,不需收信),或明知風險而刻意為之,一律禁止會變成擋路。
視覺慣例:判斷框(黃菱形)=分支條件;橙框=擋下;紅框=錯誤終點(標 error code);綠框=成功終點;虛線=可選路徑。
3. 權限矩陣¶
系統管理群整體基調見 _overview §5。本頁與 Log 轉發設定 同屬「兩個端點都有能力點守門」的頁(對照 SMTP 伺服器設定/LDAP 設定 的寫入端點僅 @jwt_required)。
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 進入本頁 | ui_routes 可見性(security-policy.read,requirement=ALL) |
— | public.route_capabilities |
| 讀取政策 | 有選單即可 | ✅ require_capability("security-policy.read") |
api/system_config/routes/security_policy_route.py(SecurityPolicyRoute.get) |
| 儲存政策 | hasCap('security-policy.update') → 否則全欄位 disabled |
✅ require_capability("security-policy.update") |
同上(SecurityPolicyRoute.put) |
| 開啟 MFA(通道未就緒) | 確認框攔一次 | ✅ app service 層擋(412),非只在 route | app/system_config/service/security_policy_app_service.py::update_policy |
| 授權唯讀期間 | useLicenseReadonly().writeDisabled() 把儲存鈕反灰並顯示原因 |
由授權層另行守門 | src/composables/useLicenseReadonly.js |
守門走 FR-048 統一授權守門 的軸④ capability,route decorator 形式——主體域守門只看「你是誰」,不必先 resolve 資源,故允許放在 route 層(decorator 內部委派 DI 注入的 guard service,route 本身不碰 session)。
兩個能力點皆
is_platform = false:這是租戶級系統設定頁,落地部署版的客戶系統管理員必須自己改得動;設成平台層會被trg_role_capabilities_platform_guard擋住而下放不了(同 CM-1283 SMTP/LDAP 下放的理由)。授予對象在 migration 中以「誰已持有smtp-config.update」動態推導,不硬編 role id。授權豁免:
security-policy列在common/authz/license.py的INFRASTRUCTURE_MODULES(CM-1409 模式)。判準是「這一項會出現在報價單上嗎」——不會,沒有人單買「兩階段驗證模組」。不豁免的話落地版會把整頁標成未授權而從選單消失。
4. 狀態機與前置條件¶
本頁無業務狀態機(單一設定組覆蓋式更新),前置條件集中在防鎖死守門與數值範圍:
| 條件 | 效果 | 出處 |
|---|---|---|
「MFA 從關到開」且 otp_readiness.ready=false 且未帶 confirm_risk |
412 SECURITY_POLICY_412001,不寫入 |
security_policy_app_service.py::update_policy |
同上但帶 confirm_risk=true |
放行(明知風險而為之,如已改用 TOTP) | 同上 |
| 關閉 MFA | 任何情況都不擋——關閉是解除鎖死的動作 | 同上(turning_mfa_on 只判 True 方向) |
| 不碰 MFA、只改其他欄位 | 不受防鎖死守門影響 | 同上(守門條件含 "MFA_REQUIRED" in payload) |
| 整數欄位非整數 | 400 SECURITY_POLICY_400001 |
同上(_coerce_for_write) |
| 整數欄位超出範圍 | 400 SECURITY_POLICY_400002——拋錯而非靜默夾成邊界值 |
同上(_INT_RANGES) |
payload 帶 DEFAULTS 以外的鍵 |
忽略(schema unknown=EXCLUDE 擋一層,app service 再擋一層) |
同上 |
| 無任何實際變更 | 不寫入、不留稽核,回 changed_keys=[] |
同上 |
| 進頁 GET 失敗且非 404 | FE configLoadFailed=true,之後按儲存直接 toast 擋下——此刻表單顯示的是預設值而非實值,存下去等於用預設覆蓋正確設定 |
SecurityPolicyForm.vue(比照 LogForwardingForm / StorageConfigForm) |
改到 JWT_*_EXPIRES |
restart_required=true;設定值已寫入 DB,但要重啟服務才生效 |
RESTART_REQUIRED_KEYS |
OTP 就緒度的判定範圍(重要,不可誇大):get_otp_readiness() 檢查的是ROOT 的 SMTP 設定存在且 host/port/from 三欄齊全,不是「真的寄得出去」——真試寄需要收件人且有數秒延遲,不適合擋在存檔路徑上。故三層文案措辭一律是「可能寄不出」。
reason |
意義 |
|---|---|
smtp_not_configured |
尚未設定郵件伺服器 |
smtp_incomplete |
設定不完整,missing_fields 列出缺哪幾欄 |
smtp_check_failed |
讀取 SMTP 設定時發生例外——視為未就緒(檢查本身壞掉不可以被解讀成「一切正常」,那正是防呆要防的方向) |
5. UI 設計¶
版面骨架(單卡片表單、四個區塊,無 tab、無列表):
┌──────────────────────────────────────────────────────────┐
│ 安全政策設定 │
│ ℹ 除「登入憑證有效時間」需重啟外,其餘儲存後立即生效 │
├──────────────────────────────────────────────────────────┤
│ 兩階段驗證 │
│ ⚠ 郵件伺服器尚未就緒…(未就緒時常駐紅色)[前往郵件設定] │
│ ✓ OTP 信件通道正常(就緒時綠色) │
│ [開關] 強制兩階段驗證(MFA) │
├──────────────────────────────────────────────────────────┤
│ 登入與鎖定 │
│ 登入失敗幾次後鎖定帳號 [ 3 ] │
│ 帳號鎖定時間(分鐘) [ 15 ] │
│ 多久沒登入就停用帳號(天)[ 180 ] │
├──────────────────────────────────────────────────────────┤
│ 密碼政策 │
│ 密碼有效天數 [ 90 ] │
├──────────────────────────────────────────────────────────┤
│ 登入憑證有效時間 │
│ ⚠ 此區設定需重新啟動服務後才會生效(常駐) │
│ 登入憑證有效時間(秒) [ 300000 ] │
│ 換發憑證有效時間(秒) [ 720000 ] │
├──────────────────────────────────────────────────────────┤
│ [ 儲存 ] │
└──────────────────────────────────────────────────────────┘
📸 實機截圖待補(亮色模式)。待補清單:OTP 就緒(綠訊息)、OTP 未就緒(紅警告 + 缺欄清單)、開啟 MFA 的確認框、存檔後的需重啟 toast、無權限時全欄位反灰。
狀態呈現:
loading期間以LoadingState(size="page")取代整張表單。- 頁首固定一則說明訊息,明講「除憑證效期外其餘即時生效」——不讓使用者誤以為全部都要重啟,也不讓他以為憑證效期會即時生效。
- OTP 就緒時也顯示綠色確認訊息:如實告知狀態,不是只有壞的時候才講。
- 憑證效期區塊的「需重啟」提示由 BE 的
restart_required_keys驅動、不在 FE 寫死——BE 改了畫面自動跟上。 - 儲存鈕反灰時以 tooltip 顯示原因(無權限/授權唯讀/讀取失敗),不隱藏按鈕。
- 存檔後以 BE 回的實際結果回填再拍快照,不用送出的 payload 拍——否則 BE 沒照收的欄位會被誤認為已存。
6. API 規格¶
Envelope:成功 {"code": 1, "data": …}、失敗 {"code": 0, "msg": "…"}(見專案 CLAUDE.md Response Format)。
6.1 總清單(本頁呼叫的全部 endpoint)¶
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 讀取 | GET /api/1.0/system/security-policy |
讀政策 + OTP 就緒度 + 需重啟鍵清單 | §6.2 |
| 儲存 | PUT /api/1.0/system/security-policy |
部分更新(未帶的鍵沿用現值) | §6.2 |
為什麼不走既有的泛用 system config 端點:泛用端點是
{group, key, value}的逐列 CRUD——一頁七個參數就要打七次 PUT,且前端得自己知道每列的 uid。RUNTIME_CONFIG語意上是「一組政策」而非「一堆設定列」,故給它一支 group 粒度的讀寫端點,內部再拆成逐 key 寫入。路徑刻意不放在
/system/config/<group>/<key>底下——那是動態規則,會把這條路徑吃掉。無 uid、無列表端點:系統層單一設定語意,路徑不帶識別碼。
6.2 讀取 / 儲存¶
GET 回應:
| 欄位 | 型別 | 說明 |
|---|---|---|
policy |
dict | 七個政策鍵的目前生效值 |
otp_readiness |
object | {ready, reason, missing_fields},見 §4 |
restart_required_keys |
string[] | 固定為兩支 JWT 效期鍵 |
policy的值來源與登入流程完全一致(get_runtime_config():內建預設 ← DB 覆寫),不直接讀 DB 列——否則 DB 沒有那一列時頁面會顯示空白,而登入流程實際上正照著內建預設在跑,兩邊對不上。
PUT 請求:七個政策鍵皆選填(部分更新),另有 confirm_risk(boolean,預設 false)。
- schema 的數值欄位一律用
Raw而非Integer:型別轉換與範圍檢查集中在 app service,避免 schema 與 service 兩處各有一套驗證、日後改一邊漏一邊。schema 只負責「哪些 key 可以進來」。 unknown = EXCLUDE:FE 會把 GET 回來的整包(含otp_readiness等唯讀欄位)原樣回送,嚴格模式會整包 400 掉。confirm_risk不是政策值本身,不會被寫進 DB(app service 只認DEFAULTS內的鍵)。- route 用
schema.load()而非use_kwargs注入:本端點是部分更新,需要區分「沒帶這個 key」與「帶了 None」,kwargs 展開後兩者都是 None。
PUT 回應:GET 的全部欄位,另加 changed_keys(本次真的變了的鍵)與 restart_required(本次是否改到需重啟的鍵)。
Error code
| Code | HTTP | 意義 |
|---|---|---|
SECURITY_POLICY_400001 |
400 | 數值格式不正確 |
SECURITY_POLICY_400002 |
400 | 數值超出允許範圍 |
SECURITY_POLICY_412001 |
412 | OTP 通道未就緒時要開啟強制 MFA,需帶 confirm_risk |
SYSTEM_CONFIG_SHARED_WRITE_FAILED |
400 | 寫入 ROOT 設定列失敗 |
7. 前端檔案地圖(compliance-manager-fe/)¶
| 檔案 | 角色 |
|---|---|
src/views/security-policy/SecurityPolicyForm.vue |
唯一頁面元件(表單、防鎖死警告與確認框、分鐘↔秒換算) |
src/config/router/index.js |
路由 /system/security-policy(name=security-policy) |
src/config/api/api.js |
SECURITY_POLICY: getUrl('/system/security-policy') |
src/config/locales/i18n/{zh-tw,en}/security-policy.json |
頁面文案(中英) |
src/config/locales/i18n/{zh-tw,en}/menu.json |
選單名與說明 |
src/composables/useLicenseReadonly.js |
授權唯讀降級(共用) |
8. 後端檔案地圖¶
| 檔案 | 角色 |
|---|---|
api/system_config/routes/security_policy_route.py |
兩支端點;require_capability 守門;把 confirm_risk 從 HTTP 邊界轉譯進 app service |
api/system_config/serializers/security_policy.py |
request / response schema |
app/system_config/service/security_policy_app_service.py |
核心:讀寫政策、OTP 就緒度、🔴 防鎖死守門、數值驗證、稽核 |
infra/system_config/runtime_config.py |
DEFAULTS 與 get_runtime_config()(登入流程與本頁共用的讀取器) |
infra/system_config/system_config_root_reader.py |
ROOT 租戶列的讀寫器(read_root_config_value / write_root_config_value) |
common/authz/license.py |
INFRASTRUCTURE_MODULES 豁免清單 |
common/code/error_code.py |
三支 SECURITY_POLICY_* error code |
common/enum/event_code.py |
SECURITY_POLICY_UPDATED = 6110 |
di_containers/system_config/system_config_containers.py |
DI wiring |
scripts/sql/2026-08-29-cm1423-security-policy-page.sql |
能力點 seed/授予/ui_routes 選單登記/route_capabilities 綁定 |
9. DB¶
9.1 資料表總清單(本頁讀寫的全部表)¶
| 表 | 用途 | 讀/寫 |
|---|---|---|
config.system_configs |
政策值本體(ROOT 租戶列,group='RUNTIME_CONFIG');另讀 group='SMTP' 判 OTP 就緒 |
讀寫 |
public.capabilities |
security-policy.{read,update} 兩個能力點 |
讀(migration 寫) |
public.role_capabilities |
能力點授予 | 讀(migration 寫) |
public.ui_routes |
選單列(/system/security-policy,掛系統設定群 sort=15) |
讀(migration 寫) |
public.route_capabilities |
路由↔能力點綁定 | 讀(migration 寫) |
public.system_logs |
稽核事件 6110 |
寫 |
無新增資料表——政策值沿用既有的
system_configs。
9.2 核心設定鍵¶
七個鍵全部位於 config.system_configs 的 ROOT 租戶列、group='RUNTIME_CONFIG',值為 JSONB。
| 鍵 | 預設 | 允許範圍 | 生效方式 |
|---|---|---|---|
MFA_REQUIRED |
環境變數 MFA_REQUIRED,否則 false |
boolean | 即時 |
LOGIN_MAX_LOCK_COUNT |
3 | 1–100 | 即時 |
LOGIN_USER_LOCK_TIME |
900(秒) | 30–86400 | 即時 |
LOGIN_INACTIVITY_THRESHOLD_DAYS |
180 | 1–3650 | 即時 |
CHANGE_PASSWORD_THRESHOLD_DAYS |
90 | 1–3650 | 即時 |
JWT_ACCESS_TOKEN_EXPIRES |
300000(秒) | 60–31536000 | 需重啟 |
JWT_REFRESH_TOKEN_EXPIRES |
720000(秒) | 60–31536000 | 需重啟 |
MFA_REQUIRED的預設值刻意吃環境變數:三環境.env都明寫此項,若改成硬預設false,會讓「有設 true 的環境」在 DB 未 seed 時行為悄悄改變。優先序:DBRUNTIME_CONFIG> 環境變數MFA_REQUIRED>False。上界不是技術限制而是防呆:把 JWT 效期設成 10 秒、把鎖定次數設成 0,效果等同把系統弄壞,而使用者多半是手誤而非本意。
10. 頁面邏輯與資料對應¶
| 畫面元素 | 資料來源 | 備註 |
|---|---|---|
| 七個欄位初值 | GET 的 policy |
值來自 get_runtime_config()(DB 覆寫 ← 內建預設) |
| OTP 警告/確認訊息 | GET 的 otp_readiness |
reason 決定文案,missing_fields 決定「缺哪幾欄」 |
| 需重啟提示 | GET 的 restart_required_keys |
不在 FE 寫死 |
| 「這次是不是從關到開」 | 進頁時記下的 MFA 實際狀態(mfaWasOn) |
不能用畫面上的值判——使用者已經把開關撥開了 |
| 鎖定時間欄位 | LOGIN_USER_LOCK_TIME ÷ 60 |
換算只在進出口各做一次(toForm / toPayload) |
| 儲存後回填 | PUT 回應的 policy |
用 BE 實際結果,不用送出的 payload |
| 離開確認 | 快照比對 | 改回原值不算髒 |
11. 背景行為與外部依賴¶
- 登入流程是唯一的消費端:
MFA_REQUIRED等鍵由登入服務在每次登入時即時讀,故改了立刻生效、不需重啟。這是本頁「多半是接線不是新做」的原因。 - JWT 效期是例外:由
flask_jwt_extended在服務啟動時讀進app.config(core/app_factory.py啟動期app.config.update(get_runtime_config())),改 DB 不影響已跑起來的進程。 - 寫入落點是 ROOT 租戶列:與讀取端同一列。寫進呼叫者自己的租戶列會變成「存成功但沒人讀」的靜默失效(CM-1283 的教訓)。
- 稽核:政策異動寫
system_logs,event_code=6110,帶操作者與changed_keys(改 MFA 開關等於改變全站登入強度,屬稽核關切點)。 - 能力點分流表:
RUNTIME_CONFIG → security-policy已補進 CM-1331 的 GROUP→能力點對照表。本頁走專屬端點,但這組列仍可能被泛用 by-uid 端點碰到;不掛進表會 fallback 到誰都沒有的system_config.*,變成「專屬端點改得動、泛用端點永遠 403」的兩套門鎖。
12. 邊界情況與已知坑¶
- JWT 效期改了不重啟就沒生效:設定值確實已寫進 DB,但跑著的進程仍用舊值。頁面已常駐提示+存檔後 toast,但沒有任何機制強制重啟——落地版請配合
guidant restart。 - 🔴 落地版 OTP 的 Redis 缺口(本頁的防呆擋不到):jedi-mfa 存驗證碼用的 Redis client 只認舊的 JSON 格式
REDIS_SECRET,不認主專案.env現行的平鋪格式(REDIS_USER/REDIS_PASSWORD),.env.sample已有警語。安裝版的guidant.env沒有REDIS_SECRET這個 key——若該部署的 Redis 有設密碼(正式部署應該要有),jedi-mfa 會連不上 Redis → 驗證碼存不進去 → 開了 MFA 就登不進系統,症狀與 SMTP 沒設好一模一樣。本頁的防呆檢查的是郵件伺服器設定,不檢查 Redis,擋不到這個。 屬 installer 範圍,補起來之前落地版不建議開啟強制 MFA。DEV 之所以測得通是環境剛好寬鬆而非設定正確:DEV 的
.env根本沒有REDIS_SECRET,jedi-mfa 以「無帳密」連 Redis,而 DEV 那台 Redis 恰好允許免帳密連線。 - 鎖定時間 UI 是分鐘、DB 是秒:改 FE 時注意換算只在
toForm/toPayload各做一次,不要散在各處。直接讀 DB 值比對畫面數字會對不上。 - OTP 就緒度不等於「寄得出去」:只檢查 SMTP 設定齊全(
host/port/from),沒有真試寄。文案措辭一律是「可能寄不出」,不可改成斷言式說法。 - 離開頁面時即使未編輯也可能跳「尚未儲存」確認框:既有
LogForwardingForm實測同樣行為,屬該頁型樣共通問題,非本頁引入,另案處理。 ui_routes那一列不可少:缺了不只是選單看不到,FE 的hasCap()也拿不到能力(capability 掛在路由節點下回給 FE),整頁會反灰。FR-068 T-1 曾漏過一次。
13. 開發與驗證¶
- 跑起來:BE
python main.py(port 8000,log 在log/app.log);FE 在compliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload)。 - 測試帳號:需具
security-policy.*能力(DEV 上為 Administrator 角色;比照 SMTP 設定頁的權限範圍)。帳號見 memoryreference_dev_login/.env(憑證不寫入本文件)。 - 導航路徑:登入 → 側選單「系統設定」→「安全政策設定」(
/system/security-policy)。 - 前置資料:DEV 需已套
scripts/sql/2026-08-29-cm1423-security-policy-page.sql。該 migration 只套 DEV,STG/POC 未動。 - 驗防鎖死的方式:暫時弄壞 SMTP 設定(清掉
host)→ 進頁應顯示紅色警告 → 開 MFA 存檔應回 412 且確認 DB 未被寫入 → 帶confirm_risk應成功 → 關 MFA 應任何情況都放行。 - E2E:
compliance-manager-test/repo(Cucumber + Playwright);本頁尚無 feature 檔。 - 相關文件:同群設定頁對照 SMTP 伺服器設定、LDAP 設定、Log 轉發設定;站內操作紀錄查詢見 操作日誌。