FR-110 · Drive OAuth 憑證從環境變數搬進 UI · 需求討論稿 · 2026-09-17(v1 · 待審)
目前要讓客戶的 Guidant AI 接得上 Google Drive,工程師得登進主機、編輯 .env、再重啟服務——落地版客戶端沒有這條動線。本案新開一頁「Google Drive 應用程式設定」,把 Google 那組應用程式帳號(client_id / client_secret)與兩個對外網址改成畫面上填、存資料庫、密鑰加密落庫,並附一顆「驗證憑證」按鈕當場告訴你填對沒有。做法整套照抄 FR-107 T-5.5 的 AI 服務設定,不發明新機制。
整案一句話:把「本系統在 Google 登記的那組應用程式帳號」從主機的設定檔搬到畫面上,讓不會 ssh 的人也設定得了,而且填完當場知道對不對。
給非工程讀者的背景:Google Drive 整合要能運作,需要兩層授權。第一層是「Guidant AI 這個軟體本身,在 Google 那邊登記過、拿到一組應用程式帳號」——這組帳號全站共用一份,目前寫死在主機的設定檔裡。第二層才是「每位客戶拿自己的 Google 帳號按下授權,把自己的雲端硬碟接過來」——這層早就有畫面(雲端空間整合頁)。本案只做第一層,第二層不動。
四階段,先後順序不可換
前一階段沒完成,後一階段連東西可測都沒有。
| 階段 | 做什麼 | 產出 | 完成怎麼判定(決策者親手檢查法) |
|---|---|---|---|
| ① 存讀與解析 | 資料庫存得下這組設定、密鑰加密、服務端讀得到(資料庫優先、設定檔後備)、依賴注入接線改成每次呼叫都重新解析 | 後端 service + resolver + DI 改線 | 用資料庫工具直接塞一筆設定進去 → 去雲端空間整合頁按「連接 Google Drive」→ 不必重啟服務就走到 Google 授權畫面 |
| ② 驗證與 API | 三支端點:讀設定(密鑰遮罩)、存設定、驗證憑證 | 後端 API + 守門 | Swagger 或 curl 打驗證端點:故意填錯的 client_secret 要回「憑證錯誤」,填對的要回「憑證正確」 |
| ③ 畫面與選單 | 新頁面「Google Drive 應用程式設定」放進「系統設定」群組;四個欄位+複製鈕+驗證鈕 | 前端頁面 + 選單 migration | 用平台管理員登入 → 側邊選單「系統設定」底下看得到新頁 → 填完存檔 → 重新整理,密鑰欄顯示「已設定」而不是明文;用一般租戶管理員登入則完全看不到這一頁 |
| ④ 設定檔退場 | 更新 .env.sample、部署文件、安裝包的 guidant.env 說明,標明這四項改由畫面設定 |
文件 | 打開 .env.sample,Drive 那段有一行白話寫「這幾項改到畫面上設,此處僅開發機後備」 |
依賴:①是全案地基,②③可在①完成後平行;④等③驗收通過才寫(不然文件會描述還沒上線的畫面)。
落地版(客戶自己機房裝一套)裝完之後,要開通 Google Drive 功能得有人登進主機、編輯 /srv/guidant-ai 的環境設定檔、重啟容器。這條動線有三個問題:
client_secret 填錯,唯一症狀是使用者按下「連接 Google Drive」之後,走到 Google 那邊被擋,錯誤訊息還是 Google 的英文頁面。沒有任何一個地方會說「是你的應用程式帳號填錯了」。config/config.py 目前跟 Drive 有關的共七項,本案只搬其中四項:
| 變數 | 用途(白話) | config.py 行號 | 本案去向 |
|---|---|---|---|
GOOGLE_DRIVE_OAUTH_CLIENT_ID |
Guidant AI 在 Google 登記的應用程式編號 | 160 | 搬進畫面(手填) |
GOOGLE_DRIVE_OAUTH_CLIENT_SECRET |
上述應用程式的密鑰 | 161 | 搬進畫面(手填,加密落庫) |
GOOGLE_DRIVE_OAUTH_REDIRECT_URI |
使用者在 Google 按完同意後,Google 要把人送回哪個網址 | 162 | 搬進畫面(自動算、唯讀顯示、可進階覆寫) |
DRIVE_WEBHOOK_PUBLIC_BASE_URL |
Google 要主動通知「檔案有變動」時,從外網打進來的站台位址 | 170 | 搬進畫面(自動算、唯讀顯示、可進階覆寫) |
DRIVE_TOKEN_ENCRYPTION_KEY |
加密用的鑰匙本身 | 163 | 留在設定檔(裝機時生成,密文與鑰匙必須分開放,同時落在資料庫就失去意義) |
DRIVE_SYNC_WORKER_POOL_SIZE |
同步工作執行緒數量 | 236 | 留在設定檔(部署調校參數,不是憑證) |
DRIVE_FILE_SIZE_LIMIT_MB |
單檔大小上限 | 255 | 留在設定檔(同上) |
FRONTEND_BASE_URL |
前端站台位址 | 257 | 留在設定檔(非 Drive 專屬,多處共用;但可能成為自動算網址的來源,見 D8) |
.env.sample 目前漏列 DRIVE_WEBHOOK_PUBLIC_BASE_URL
實查 .env.sample 只有 GOOGLE_DRIVE_OAUTH_* 三項與加密鑰、上限、執行緒數,沒有這一項。而 config.py:164-170 的註解記著這個坑:FR-016 實作計畫列了這項但落地時漏在 config 定義,症狀是授權走到註冊通知頻道那步拋 'NoneType' object has no attribute 'rstrip',而且在設定檔補這個變數也無效(當時沒有任何程式碼會去讀)。config 那邊已補上,但 .env.sample 至今沒補——本案第④階段要一起收掉。
新頁面與既有的「雲端空間整合」頁不重疊也不取代,是上下兩層:
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TB
subgraph P["平台層 — 本案新頁「Google Drive 應用程式設定」"]
P1["誰能設:平台(ROOT)管理員"]
P2["設什麼:本系統在 Google 登記的應用程式帳號<br/>client_id / client_secret / 兩個對外網址"]
P3["存哪:system_configs 的 ROOT 租戶那一列<br/>全站共用一份"]
end
subgraph T["租戶層 — 既有頁「雲端空間整合」"]
T1["誰能設:各租戶管理員"]
T2["設什麼:拿自己的 Google 帳號按下授權"]
T3["存哪:tenant_drive_integrations<br/>一個租戶一列"]
end
R["憑證解析器<br/>ROOT 設定 → 設定檔後備"]
U["實際用到憑證的四處"]
P3 --> R
R --> U
U --> T2
T2 -.->|"授權拿到的通行證<br/>加密後存這裡"| T3
白話說:平台層填一次、全站受用;租戶層每家客戶各自按一次授權。 沒有平台層那組帳號,租戶層的「連接 Google Drive」按下去必然失敗——本案順帶讓這個失敗變得看得懂(見 D11 下方與風險段)。
以下七項決策者已拍板,本段為記錄,不是待議項。
| # | 決策 | 內容 | 為什麼 |
|---|---|---|---|
| D1 | FR 編號 | 新開 FR-110,只做 Drive 半邊 | 前身是 FR-070 白話需求(AI 金鑰 + Drive 憑證兩件一起提)。AI 那半邊已隨 FR-107 T-5.5 落地,剩下的 Drive 半邊獨立成案 |
| D2 | 頁面形式 | 新開一頁「Google Drive 應用程式設定」,掛「系統設定」群組(pid=47,sort=39),與「AI 服務設定」並列同守門 |
與租戶層的「雲端空間整合」頁受眾不同、權限不同,混在同一頁會讓租戶管理員看到不該看的欄位。並列 AI 服務設定則是因為兩者本質相同:都是「填連線資訊與金鑰」 |
| D3 | 畫面四項 | ① client_id 手填 ② client_secret 手填 ③ 回呼網址自動算、唯讀顯示 + 複製鈕 ④ 通知用對外網址自動算、唯讀顯示 + 複製鈕。③④ 另有「進階」摺疊區可手動覆寫,預設摺疊 |
回呼網址必須跟客戶在 Google 後台登記的一字不差,所以要能一鍵複製貼過去;但它其實是算得出來的(對外站台位址 + 固定路徑),讓人手打只會打錯。留覆寫是給反向代理、非標準路徑等特殊部署用 |
| D4 | 密鑰加密 | client_secret 用既有的 DRIVE_TOKEN_ENCRYPTION_KEY 加密落庫,不新增環境變數。鑰匙是空的就直接拋錯,沒有存明文這條路 |
Drive 這條線本來就強制要有這把鑰匙(FernetCrypto.__init__ 空值直接 ValueError),使用者的授權通行證也是用它加密的。刻意與 AI 金鑰那把不同:AI 那把允許「空鑰匙=不加密」給開發機方便,這裡不給——因為這條線沒有「鑰匙可以不設」的狀態 |
| D5 | 驗證憑證按鈕 | 要做。拿一個假的授權碼去打 Google 的換票端點:Google 回 invalid_client 代表憑證填錯、回 invalid_grant 代表憑證是對的(只是那個授權碼本來就假)。一支 POST 端點 |
這是全案最有感的一顆按鈕。目前填錯要等使用者去按授權才發現,而且看到的是 Google 的錯誤頁;有了它,填完當場就知道 |
| D6 | 換憑證的處理 | 只警告,不自動改各租戶的連線狀態。畫面在存檔前若偵測到 client_id 有變更,跳確認框說明「所有租戶需重新授權」 |
換了應用程式帳號,舊的授權通行證在新帳號底下是無效的。但自動把所有租戶標成「未連接」風險太大(改錯一個字就把全站連線清掉),交由人確認 |
| D7 | 守門 | 路由只掛 @jwt_required,守門下沉到 service 層呼叫 require_platform_admin()(軸①,common/authz/platform.py)。選單能力點沿用 storage-config.{read,update},不新開 |
完全照 AI 服務設定的做法。不新開能力點的理由寫在 scripts/sql/2026-09-17-fr107-ai-service-config-menu.sql:新開一顆要 seed 到每個既有租戶的管理員角色,漏掉的症狀是那個租戶永遠 403 且沒有任何錯誤訊息。而沿用的那顆已配給每個租戶管理員,所以能力點之後還要再套一層平台管理員判定 |
兩層,不做租戶層:
system_configs,平台管理員在新頁填的)形狀照抄 app/system_config/service/ai_provider_key_resolver.py:145-151——那支是三層(租戶 → ROOT → 設定檔),本案砍掉租戶層那一圈,因為應用程式帳號本來就全站一份。
🔴 背景排程讀設定必須走繞過隔離的專用讀取器
core/scheduler.py 有兩支排程會用到 Drive 憑證:drive_sync_worker(每 5 秒)與 webhook_channel_renewer(每 6 小時)。它們以系統服務身分執行,沒有登入者、沒有租戶脈絡,一般的設定查詢路徑在這種情況下挑到哪一列是不確定的(看資料列 id 排序)。
必走 infra/system_config/system_config_root_reader.py 的 read_root_config_value(),不可裸呼叫依賴隔離機制收斂的單筆查詢。
這不是理論風險:2026-07-29 POC 的檔案上傳就是這樣把某租戶的檔存進別租戶的儲存桶,而另一個環境「沒重現」純粹是資料列 id 排序恰好對了——骰子點數不同而已。詳見 docs/claude/memory/feedback_background_job_storage_config_no_context_trap.md。
D8 — 自動算出來的網址,要從哪裡推導?
回呼網址與通知用對外網址都是「對外站台位址 + 固定路徑」,問題是「對外站台位址」從哪來。
| 選項 | 做法 | 代價 |
|---|---|---|
| A | 用既有的 FRONTEND_BASE_URL 環境變數 |
語意不對——那是前端站台位址,而 Google 要打的是後端 API。兩者同網域時剛好對,分開部署時靜默錯 |
| B | 用當次請求的 Host / X-Forwarded-Host 標頭 |
不必多填欄位,但值取決於「管理員是從哪個網址進來設定的」。走反向代理、走內網 IP 進來設定時,算出來的網址對外不可達,而且看起來完全正常 |
| C | 新增一個 ROOT 設定「系統對外網址」,由本頁一起填 | 多一個欄位要填 |
建議:C。部署的對外網址是一件明確的事實,不該靠推導——A 與 B 算錯時都不會報錯,症狀是使用者按授權後被 Google 擋、或是檔案變動通知永遠收不到,兩者都極難追。多一個欄位換掉兩種靜默失敗划算。但要點明:選 C 就是本頁從四個欄位變五個,且該欄位可能被其他功能共用(那是好事,但要想清楚它該不該叫「Drive 的」設定)。
D9 — 這組設定存在哪張表?
| 選項 | 做法 | 代價 |
|---|---|---|
| A | 進共用的 system_configs 表,新開一個群組 GOOGLE_DRIVE_APP_CONFIG |
沿用既有的讀寫殼、遮罩機制、守門機制,零新表 |
| B | 新開一張專屬表 | 欄位型別明確、可加約束,但要新 migration、新 repository、新 domain service,且與 AI 金鑰的做法分岔 |
建議:A。AI 金鑰(AI_PROVIDER_CONFIG)、SMTP、議題整合設定都在這張表,遮罩與守門的殼 GuardedSystemConfigService 已經長好,本案只要加一個群組名與對應的遮罩規則。選 B 等於為了四個欄位開一整套 DDD 分層。
D10 — 密鑰怎麼回給前端、怎麼寫回去?
照 guarded_system_config_service.py:174-200 的既有規則(_merge_ai_provider_secrets):
is_set: true/false 旗標——前端只要顯示「已設定 / 未設定」is_set 原樣送回來)=保留舊值;送了非空值=換新clear: true建議:照抄,一字不改。 這三條規則不是風格選擇,是踩出來的:底層寫入是整格覆寫,不補「缺的從舊值補回」這一步,使用者只改網址按存檔,密鑰就被抹掉,而畫面顯示「儲存成功」,要等下次有人按授權失敗才浮現。列在這裡是要讓決策者知道這條路徑存在、不是新發明的。
D11 — 安裝版裝機時要不要把設定檔的值寫進資料庫一次?
FR-107 T-5.5 的原廠 AI 金鑰是「裝機時寫進資料庫」。本案要不要比照?
| 選項 | 做法 | 代價 |
|---|---|---|
| A | 裝機時把環境變數的值 seed 進資料庫 | 裝完就「已設定」,但沒人填過也顯示「已設定」,分不出「原廠給的」與「客戶自己填的」 |
| B | 不 seed,純靠環境變數後備 | 環境變數有值就用得到,功能正常;資料庫有值才代表「有人在畫面上設過」 |
建議:B。兩者功能上等價(後備那層照樣讀得到),但語意差很多——「資料庫裡有=有人設過」是個乾淨的判準,日後要做「未開通」提示、要判斷該不該跳引導畫面,都靠它。而且 Drive 這組帳號與 AI 金鑰的性質不同:AI 金鑰是原廠掏錢買來送客戶用的,Drive 應用程式帳號通常是客戶自己去 Google 申請的。
D12 — 現有的依賴注入單例怎麼改?
di_containers/cloud_integration/cloud_integration_containers.py:107-112 目前把 GoogleOAuthClient 註冊成單例,三個參數在服務啟動時從設定檔灌進去、之後永不改變。憑證改成資料庫可調之後,這個單例會拿著開機當下的舊值。
| 選項 | 做法 | 代價 |
|---|---|---|
| A | 改成每次取用都重新建立(Factory) | 呼叫端拿到的物件生命週期變了,要確認沒有人把它存起來重複用 |
| B | 保留單例,內部改成每次呼叫方法時才去解析器拿當下的憑證 | 呼叫端一字不用改 |
受影響的呼叫端(已 grep 全庫確認):
| 檔案 | 用法 |
|---|---|
app/cloud_integration/service/google_drive_integration_service.py:53,61 |
注入後存成 self._oauth |
domain/cloud_integration/service/google_drive_token_manager.py:41,45 |
注入後存成 self._oauth |
di_containers/cloud_integration/cloud_integration_containers.py:132,267 |
兩處往下傳 |
scripts/evidence/classify/classify_evidence_drive.py:190 |
離線腳本,自己 GoogleOAuthClient(...) 直讀環境變數,不走依賴注入 |
建議:B。上表前兩個呼叫端都是「注入後存成成員變數」——選 A 改成每次建立,這兩處仍然只在建構時拿一次,等於白改,還得連它們一起動。選 B 把「去哪拿憑證」關進 GoogleOAuthClient 內部,外面四個接點一字不動。至於那支離線腳本,見下方接入點表的處置建議。
下表每一列都經過實檔查證(行號為 2026-09-17 當下)。本案動到的每一處都在這張表上;沒在表上的就是不動。
| 元件 | 檔案/位置 | 現況 | 本案動作 |
|---|---|---|---|
| 設定值宣告 | config/config.py:160-170 |
四項憑證從環境變數讀,預設空字串 | 保留(降為後備層),註解改寫說明現在的真相來源是資料庫 |
| 依賴注入單例 | di_containers/cloud_integration/cloud_integration_containers.py:107-112 |
GoogleOAuthClient 註冊成單例,三參數開機時灌入 |
改,做法待 D12 定案 |
| OAuth 客戶端 | infra/cloud_integration/google_drive/google_oauth_client.py:33 |
__init__(client_id, client_secret, redirect_uri) 三個值存成成員 |
若採 D12-B:內部改成呼叫時解析 |
| 通行證管理 | domain/cloud_integration/service/google_drive_token_manager.py:41,45 |
注入 GoogleOAuthClient 後存成 self._oauth |
D12-B 下不動 |
| 整合服務 | app/cloud_integration/service/google_drive_integration_service.py:53,61 |
同上 | D12-B 下不動 |
| 同步排程 | core/scheduler.py:155-167 |
drive_sync_worker 每 5 秒跑,system_context("drive_sync_worker") |
驗證它讀憑證時走的是繞過隔離的讀取器(見上方紅框) |
| 通知頻道續約 | core/scheduler.py:187-204 |
webhook_channel_renewer 每 6 小時,明說要掃全部租戶 |
同上 |
| 授權網址端點 | api/cloud_integration/__init__.py:34 → /api/1.0/integrations/google-drive/auth-url |
產生 Google 授權網址 | 不動(憑證由下層解析) |
| 回呼端點 | api/cloud_integration/__init__.py:35 → /api/1.0/integrations/google-drive/callback |
接 Google 送回的授權碼 | 不動;此路徑即回呼網址的固定尾段 |
| 離線分類腳本 | scripts/evidence/classify/classify_evidence_drive.py:190-194 |
自己 GoogleOAuthClient(...),三個值 os.environ[...] 直讀且無預設(缺了直接 KeyError) |
建議跟改成走解析器——否則資料庫設好之後這支腳本仍然只認環境變數,而且缺變數的失敗訊息是 KeyError,看不出所以然。列為子任務,不是必做(該腳本屬 FR-107 待刪的舊 Drive 分類線,見 followup_fr107_remove_legacy_drive_classification_line) |
| 前端整合卡片 | compliance-manager-fe/src/components/integrations/GoogleDriveIntegrationCard.vue |
租戶層的「連接 Google Drive」入口 | 建議加:平台層憑證未設定時顯示「系統尚未完成 Google Drive 應用程式設定,請聯繫平台管理員」,而不是讓人按下去撞 Google 的錯誤頁 |
| 平台管理員旗標 | common/constant/ai_service_config.py ↔︎ compliance-manager-fe/src/config/aiServiceConfig.js |
AI 服務設定的開放範圍旗標,前後端各一份、必須成對改 | 本案比照新增一組同型常數(Drive 版);兩邊一起建、一起改 |
| 選單登記 | scripts/sql/2026-09-17-fr107-ai-service-config-menu.sql、...-system-menu-regroup.sql |
AI 服務設定掛 pid=47、sort=37;儲存設備設定 sort=36 |
新增一支 migration:本頁掛 pid=47、sort=39,能力點沿用 storage-config.{read,update}(read=ALL / update=ANY) |
| 環境變數範本 | .env.sample:145-153 |
有 GOOGLE_DRIVE_OAUTH_* 三項,缺 DRIVE_WEBHOOK_PUBLIC_BASE_URL |
補上缺的那項 + 四項加註「改由畫面設定,此處為開發機後備」 |
假設 D9 選 A(進 system_configs),一列即可:
| 欄位 | 值 |
|---|---|
group |
GOOGLE_DRIVE_APP_CONFIG |
key |
CONFIG |
tenant_id |
ROOT 租戶(全站共用一份) |
value |
見下方 JSON |
{
"client_id": "<Google 給的應用程式編號,明文>",
"client_secret": "<加密後的密文>",
"redirect_uri_override": "",
"webhook_base_url_override": ""
}兩個 _override 欄位空著=用自動算的值,填了=以填的為準(對應 D3 的「進階」摺疊區)。
🔴 client_secret 存的是密文,且沒有存明文的路
加密用 infra/cloud_integration/crypto/fernet_crypto.py 的 FernetCrypto,鑰匙是 DRIVE_TOKEN_ENCRYPTION_KEY。該類別建構時鑰匙為空就 ValueError——這是既有行為,本案刻意沿用而非放寬。
密文在資料庫、鑰匙在主機設定檔,兩件分開放:單獨拿到資料庫備份解不開,單獨拿到設定檔也沒有密文可解。
| 方法 | 路徑 | 做什麼 | 守門 |
|---|---|---|---|
GET |
/api/1.0/system/config/GOOGLE_DRIVE_APP_CONFIG/CONFIG |
讀設定。client_secret 換成 is_set: true/false;另回自動算出的兩個網址供畫面顯示與複製 |
route @jwt_required + service 層 require_platform_admin() |
PUT |
同上 | 存設定。缺的密鑰從舊值補回;clear: true 才清除 |
同上 |
POST |
/api/1.0/integrations/google-drive/verify-credentials |
驗證憑證(D5) | 同上 |
讀取回應範例(密鑰已遮罩):
{
"status": true,
"data": {
"client_id": "1234567890-abcdefg.apps.googleusercontent.com",
"client_secret": { "is_set": true },
"redirect_uri": "https://guidant.example.com/api/1.0/integrations/google-drive/callback",
"redirect_uri_is_override": false,
"webhook_base_url": "https://guidant.example.com",
"webhook_base_url_is_override": false
}
}拿一個故意造假的授權碼去打 Google 的換票端點 https://oauth2.googleapis.com/token,看 Google 回哪一種錯:
| Google 回什麼 | 代表 | 畫面顯示 |
|---|---|---|
invalid_client |
應用程式帳號或密鑰錯了 | ❌ 憑證錯誤,請確認 client_id 與 client_secret |
invalid_grant |
帳號密鑰是對的(Google 認得它),只是那個授權碼假的 | ✅ 憑證正確 |
| 連不上 / 逾時 | 主機出不了外網 | ⚠️ 無法連線至 Google,請確認主機對外網路 |
這顆按鈕驗不到回呼網址
Google 在換票這一步不會去比對回呼網址是否已登記——那是在產生授權網址那一步才檢查的。所以「憑證正確」只代表帳號密鑰對,不代表客戶已經把回呼網址貼進 Google 後台。畫面上驗證成功的提示要一併寫明「別忘了把下方回呼網址貼進 Google Cloud Console 的已授權重新導向 URI」。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
participant A as 平台管理員
participant FE as 設定頁面
participant BE as 後端設定服務
participant DB as 資料庫
participant G as Google
participant T as 租戶管理員
A->>FE: 開啟「Google Drive 應用程式設定」
FE->>BE: GET 讀設定
BE->>BE: 檢查是不是平台管理員
BE->>DB: 讀 ROOT 那一列
BE-->>FE: client_id 明文/密鑰只回「已設定」<br/>回呼網址(自動算)
A->>FE: 貼上 client_id 與 client_secret
A->>FE: 按「驗證憑證」
FE->>BE: POST 驗證
BE->>G: 拿假授權碼換票
G-->>BE: invalid_grant(=帳號密鑰是對的)
BE-->>FE: ✅ 憑證正確
A->>FE: 複製回呼網址 → 貼進 Google Cloud Console
A->>FE: 按「儲存」
FE->>BE: PUT 存設定
BE->>BE: 密鑰加密(Fernet)
BE->>DB: 寫入 ROOT 那一列
Note over T,G: 之後任何租戶要接自己的 Drive
T->>BE: 按「連接 Google Drive」
BE->>DB: 解析憑證(ROOT → 設定檔後備)
BE->>G: 帶著 client_id 產生授權網址
G-->>T: 顯示 Google 同意畫面
T->>G: 同意
G->>BE: 帶授權碼回呼
BE->>G: 換通行證
BE->>DB: 通行證加密後存租戶那張表
本段是草案,等 D8–D12 定案後才開卡。 子任務顆粒度、依賴關係可能隨決策調整。
| 子需求 | 子任務 | 做什麼 | 驗收條件 | 依賴 |
|---|---|---|---|---|
| .1 存讀地基 | T-1.1 | 設定群組 + 遮罩規則 + 守門(GuardedSystemConfigService 加 GOOGLE_DRIVE_APP_CONFIG 分支) |
用平台管理員讀設定回 is_set;用租戶管理員讀回 403 |
— |
| T-1.2 | 憑證解析器(ROOT → 設定檔後備)+ 加密解密 | 資料庫塞值 → 解析器回資料庫的;清空 → 回設定檔的 | T-1.1 | |
| T-1.3 | 依賴注入接線改造(D12) | 改資料庫設定後不重啟,授權網址帶的是新 client_id |
T-1.2 | |
| T-1.4 | 背景排程讀取路徑驗證(走繞隔離讀取器) | 停掉環境變數只留資料庫,同步排程照常運作 | T-1.2 | |
| .2 API | T-2.1 | 讀 / 存兩支端點(含「缺的密鑰從舊值補回」與 clear 旗標) |
只改網址存檔,密鑰不被抹掉 | T-1.1 |
| T-2.2 | 驗證憑證端點(D5) | 錯的密鑰回「憑證錯誤」,對的回「憑證正確」 | T-1.2 | |
| T-2.3 | 自動算網址(D8 定案後) | 回呼網址與端點實際路徑一致 | D8 | |
| .3 畫面 | T-3.1 | 設定頁面(四/五欄 + 複製鈕 + 驗證鈕 + 進階摺疊區) | 平台管理員看得到、租戶管理員看不到 | T-2.1 |
| T-3.2 | 換 client_id 的確認框(D6) |
改動 client_id 按存檔會跳確認 |
T-3.1 | |
| T-3.3 | 選單 migration(pid=47、sort=39、沿用能力點) |
側邊選單「系統設定」底下出現新頁 | T-3.1 | |
| T-3.4 | 租戶層卡片的「尚未設定」提示 | 清空平台憑證後,租戶頁顯示提示而非讓人撞錯誤頁 | T-1.2 | |
| .4 設定檔退場 | T-4.1 | .env.sample 補缺項 + 四項加註 + 部署文件 + 安裝包 guidant.env 說明 |
打開範本看得懂哪些改到畫面設 | .3 驗收通過 |
| T-4.2(選配) | 離線分類腳本改走解析器 | 只在資料庫設定、不設環境變數時腳本仍可跑 | T-1.2 |
🔴 一:背景排程讀 tenant-scoped 設定會非確定性挑到別人的
drive_sync_worker(5 秒一次)與 webhook_channel_renewer(6 小時一次)都沒有登入者脈絡。裸呼叫依賴隔離機制收斂的單筆查詢,挑到哪一列看資料列 id 排序。
本案的憑證雖然只存 ROOT 一份、不像儲存設定那樣每租戶一列,但讀取路徑必須明確指定 ROOT,不能仰賴「反正只有一列」——那是資料現況不是程式約束,日後有人加第二列就靜默錯。
實作一律走 read_root_config_value()。驗收方式:把排程的讀取路徑打開來看,不是看功能有沒有壞(壞不壞看骰子)。
🔴 二:換了 client_id,所有租戶的既有授權全部失效
Google 的授權通行證是綁應用程式帳號發的。換帳號=舊通行證在新帳號底下無效,但資料庫裡那些通行證還在、狀態還顯示「已連接」。症狀是同步排程開始大量失敗、使用者以為還連著。
D6 決定只警告不自動改狀態,所以確認框的文案要寫得夠白話:不是「確定要儲存嗎」,而是「更換應用程式帳號後,所有已連接 Google Drive 的客戶都需要重新授權一次,確定要更換嗎?」
三:回呼網址與 Google 後台登記的不一致
Google 會逐字比對。差一個結尾斜線、差 http 與 https、走反向代理後對外網址與後端自己算出來的不同——都會在使用者按下授權後被 Google 擋,錯誤頁是 Google 的英文頁,看不出是我們這邊設定的問題。
這正是 D3 要「自動算 + 複製鈕」而不是讓人手打的原因,也是 D8 建議選 C(明確填對外網址)而非推導的原因。
四:.env.sample 缺 DRIVE_WEBHOOK_PUBLIC_BASE_URL
實查確認缺。config.py:164-170 記著同型的坑:這個變數曾經「config 沒定義」,於是依賴注入永遠取到 None,症狀是註冊通知頻道時拋 'NoneType' object has no attribute 'rstrip',而且在設定檔補這個變數也無效——排查時極易誤判成環境沒設好。config 已修,範本至今沒補,第④階段一起收。
五:平台管理員旗標前後端必須成對改
AI 服務設定用一組前後端各一份的常數控制「這頁只給平台管理員」:後端 common/constant/ai_service_config.py 的 AI_PROVIDER_CONFIG_PLATFORM_ADMIN_ONLY,前端 src/config/aiServiceConfig.js 的 AI_SERVICE_CONFIG_PLATFORM_ADMIN_ONLY。
只改前端=畫面開了但 API 仍 403;只改後端=API 開了但畫面進不去。 本案新增同型的一組(Drive 版),兩邊一起建,並在各自檔頭互指對方。
六:能力點不新開,但別忘了它擋不住租戶管理員
沿用 storage-config.{read,update} 的代價是:那顆能力點已經配給每個租戶的管理員角色,單看能力點租戶管理員是過關的。所以能力點之後必須再套一層平台管理員判定(D7),這一層才是真正的門。
FR-107 的 CM-1867 第二輪驗收就是在這裡被退回的——以為掛了能力點就安全。