本卡屬 FR-096(母卡 CM-1803),第 2 棒:系統設定在主專案這一側的接線(BE repo,25 檔 2,672 行)。只掃不修。
套件只管「設定表怎麼增刪改查」,「誰改得動、改了之後值落在哪裡」的答案全在主專案這一側。這一棒掃的就是這一側。
要回答三個問題:① 主專案包了一層「守門版」服務,有沒有路徑繞過它直接用原版? ② 改設定的權限怎麼判,一般客戶的管理員改得到平台層的設定嗎? ③ 有六處程式碼明確關掉了資料庫的客戶隔離,範圍有沒有收窄?
跨 repo 必須分棒(掃描根目錄不同,工具一次只吃一個根)。而且前面的經驗反覆證明套件把安全決策外包給宿主時,風險大半落在宿主這一側(FR-079 B2、FR-077 R3 都是這個形狀)。這支套件的 docstring 自己就寫著「四道守門一律走 port、由宿主注入」「權限分流表是產品的角色矩陣、留在宿主」——那張表就在本棒範圍內。
這些是首腦 2026-09-15 親自開檔看過的,行號與內容屬實。不是要你重新確認,是要你從這裡往外追:
GuardedSystemConfigService,DI 容器 di_containers/system_core/system_core_containers.py 注入的是這個包過的子類,檔案 docstring 明說這是「單一收口點,所有 consumer 拿到同一個過濾後視圖」。但另外四支容器各自組裝了未包裝的原版:di_containers/auth/auth_containers.py:180、di_containers/notification/notification_containers.py:20、di_containers/user_change_password/user_change_password_containers.py:46 三處是 providers.Factory(SystemConfigService, ...)(套件原版),di_containers/login/login_containers.py:14-15 直接接 domain service 與 repo。這四支的 consumer 是登入、寄信、改密碼——全是會碰到 SMTP 密碼的路徑。要追:這四條路徑實際會讀到什麼、出廠快照對它們可不可見、共用設定讀寫落在誰的那一列。core/plugins/system_core.py 的 _GROUP_CAPABILITY_RESOURCE 把設定分組映到分頁式權限(儲存/SMTP/LDAP/通知/議題整合/登入安全政策),表裡沒有的分組退回一個誰都沒有的權限而永遠擋下(fail-closed,設計正確)。要追的是反面:有沒有哪個分組被映到太寬鬆的權限;以及 _ACTION_ALIAS 把通知設定的「新增/刪除」都映成「修改」(理由是 seed 沒發新增/刪除權限),會不會讓「只能改」的人其實能刪。infra/system_config/system_config_root_reader.py 的 60/96/122/179/242/300 行各一句 SET LOCAL app.is_super_admin = 't',用意是讀寫「總部那一列」共用設定(一般客戶受隔離看不到總部的列)。設計上有理由,但 FR-086 B2-B 已證明同款樣板被上傳服務拿去當「讀不到就借別人的」退路而出事。要逐支看:繞過的範圍有沒有收窄到只讀該讀的那一列(首腦看到 read_root_config_row_by_uid 明確限定總部、屬收窄得好的;但同檔六處要逐一看),以及呼叫者有沒有把它當 fallback 用。write_root_config_value()(約 170-220 行)手寫「先 UPDATE、影響 0 列才 INSERT」,並自開一個獨立交易+關掉隔離。要追:併發下兩個請求同時寫會怎樣(先讀後寫的競態、重複塞入),以及這支的呼叫者有沒有守門——它繞過隔離寫入,如果上游守門鬆,等於任何人都能改全系統的共用設定。api/system_config/routes/system_config_route.py 的兩個 resource 只掛 @jwt_required()(只驗有沒有登入),權限檢查靠函式呼叫補在方法內(:103/:111/:146/:158)。要追:有沒有哪個方法漏掉那一行——:130-131 的 GET by 分組+鍵沒有呼叫權限檢查(只有 @jwt_required()),而這條正是 SMTP/LDAP 共用設定讀取最集中的入口。對照 security_policy_route.py 用的是 decorator 形式(:30-31),兩種寫法混用時漏掉的那一個看不出來。system_config_route.py:62 的 _MASK_CONFIG = SystemCorePluginConfig() 自己建一個設定實例來取遮罩名單,註解說「刻意不複製字串,免得兩份名單只補一邊」。要追:這個意圖有沒有達成——主專案有沒有別的地方另外硬寫了一份名單,以及漏掉 STORAGE_CONFIG 這件事(見第 1 棒重點②)在主專案這一側會不會被放大。app/system_config/service/guarded_system_config_service.py 把 STORAGE_CONFIG/FACTORY_DEFAULT(內含內建物件儲存的帳密)對所有讀寫路徑回 404,對外故事是「沒有這個鍵」。要追:有沒有路徑繞過——第①點那四支原版服務、AI 儀表板的資料 API(docstring 提到 system_config.get_system_configs)、上傳服務、以及 storage_config_restore_app_service 的恢復流程。app/system_config/service/tenant_storage_config_seeder.py 與 infra/system_config/tenant_config_seed_writer.py 讓新客戶繼承總部的儲存設定(含帳密)。要追:繼承的是不是應該繼承的那一份、誰能觸發它、有沒有辦法讓 A 客戶繼承到 B 客戶的設定。DEV 實查(首腦 2026-09-15 唯讀):system_configs 有隔離(rls=true、4 條規則),23 筆分佈 6 個客戶,其中 6 筆的密碼欄位有值。
同款舊發現當對照:FR-086 B2-A「物件儲存帳密明文回給任何登入者」(HIGH)與 B2-B「讀不到本客戶設定就去借別家的」(MEDIUM,成因就是同一份繞隔離樣板)、FR-085 C2「密碼與 JWT 原文進 log」(HIGH)。撞到同一件事請標「與 CM-xxxx 同根因」不另計新發現。
本棒有幾個檔曾在別棒被讀到但不是當時的主題(core/plugins/_host.py、infra/notification/tenant_notify_config_reader.py)——這次是正面掃,照掃即可。