293 lines
11 KiB
Markdown
293 lines
11 KiB
Markdown
# LINE / Telegram 驗證與授權生命週期說明
|
||
|
||
> 給未來修改 LINE / Telegram 驗證、配對、home-channel、email handoff、刪綁定與 admin 後台流程的人看。
|
||
>
|
||
> 這份文件是程式碼註解的唯一長文件入口。若你在 `gateway/user_verification.py`、`gateway/authz_mixin.py`、`plugins/platforms/telegram/adapter.py`、`plugins/platforms/line/adapter.py`、`hermes_cli/web_server.py` 看到引用,請回到這裡讀完整背景。
|
||
|
||
## 為什麼需要這份文件
|
||
|
||
LINE / Telegram 驗證與授權不是單一布林值,而是多層狀態的組合。過去重複出現的 regressions 幾乎都來自下面幾種錯誤假設:
|
||
|
||
1. **以為刪掉綁定 row 就等於失效**
|
||
- 實際上 pairing approval 仍可能放行。
|
||
2. **以為 adapter allowlist 與 runtime 驗證是同一層**
|
||
- 實際上 adapter intake gate 與 gateway authz 是兩層,不同平台策略也不同。
|
||
3. **以為 verified email handoff 只要看使用者輸入的 email**
|
||
- 實際上必須再比對 live 綁定,否則會把結果寄到過期或錯誤信箱。
|
||
4. **以為 LINE / Telegram 可以共用完全相同的 intake 邏輯**
|
||
- 實際上 Telegram 與 LINE 的 prefilter 行為、reply token、quota fallback、pairing/onboarding 路徑都不同。
|
||
5. **以為只要測 DB 或只要測 UI 就夠**
|
||
- 實際上需要同時驗證 DB、pairing store、adapter intake、gateway authz、admin route 與 user-visible 行為。
|
||
|
||
## 需求目標
|
||
|
||
系統對 LINE / Telegram 應滿足以下不變條件:
|
||
|
||
1. **未驗證的新 DM 使用者可以進入 onboarding / pairing / company-email 驗證流程。**
|
||
2. **已驗證且仍綁定的使用者,可以被辨識為 verified source,並安全使用 email handoff。**
|
||
3. **解除綁定時,所有仍可放行的授權面都要一起撤銷,而不是只刪一張表。**
|
||
4. **group / room / forum 等非 DM 面向,不能因為修 DM onboarding 而意外放開。**
|
||
5. **系統自動 email handoff 必須重新比對 live 綁定,不可依賴舊 session、壓縮後上下文或快取中的 email。**
|
||
6. **任何改動都要避免 fail-open:沒有明確 allowlist / pairing / verified binding 時,不應默默放行外部來源。**
|
||
|
||
## 關鍵資料面(授權不是只有一處)
|
||
|
||
### 1. Runtime DB:`gateway_users.db`
|
||
主要表:
|
||
|
||
- `principals`
|
||
- canonical principal(例如 `dk96@bremen.com.tw`)
|
||
- `external_identities`
|
||
- 平台帳號與 principal 的 verified 綁定關係
|
||
- `identity_enforcement`
|
||
- 某平台帳號目前驗證狀態(`new`, `pending_verification`, `verified`, `admin_blocked`, ...)
|
||
- `verification_requests`
|
||
- 驗證 email token 與 resend 節流相關資料
|
||
|
||
### 2. Pairing store:`~/.hermes/pairing/{platform}-approved.json`
|
||
這是**獨立授權面**。
|
||
|
||
- pairing approved 使用者在 gateway authz 層會被視為已授權
|
||
- 即使 `external_identities` row 被刪掉,只要 approved 還在,仍可能可以對話
|
||
|
||
### 3. Adapter intake policy
|
||
平台 adapter 在訊息剛進來時,可能先做第一層 gate:
|
||
|
||
- Telegram:`_is_user_authorized_from_message()`
|
||
- LINE:`_allowed_for_source(...)` 搭配 DM / group / room 分流
|
||
|
||
### 4. Gateway authz
|
||
在 adapter 之後,gateway 還會再做 `_is_user_authorized(...)`。
|
||
|
||
這一層會綜合:
|
||
|
||
- pairing approved
|
||
- env allowlists
|
||
- adapter policy
|
||
- allow-all flags
|
||
- 群組/論壇例外規則
|
||
|
||
## 真正的生命週期
|
||
|
||
### A. 新使用者從 Telegram / LINE DM 進來
|
||
|
||
1. adapter 收到 inbound message
|
||
2. adapter intake policy 決定:
|
||
- 是否立刻丟棄
|
||
- 是否把 DM 交給 gateway authz / verification
|
||
3. gateway authz 檢查:
|
||
- pairing approved?
|
||
- allowlist?
|
||
- verified binding?
|
||
4. 若尚未驗證,但平台策略允許 onboarding:
|
||
- 進入 company email 驗證流程
|
||
- 建立 `verification_requests`
|
||
5. 使用者點 email 驗證連結後:
|
||
- 寫入 `external_identities`
|
||
- 更新 `identity_enforcement.state = verified`
|
||
|
||
### B. 已驗證使用者正常對話
|
||
|
||
1. `is_verified_source(source)` 應同時成立:
|
||
- `identity_enforcement.state == verified`
|
||
- `external_identities` 仍存在對應 row
|
||
2. 若系統要把結果改寄 email:
|
||
- 必須使用 `bound_email_matches_source(...)`
|
||
- 嚴格比對 live 綁定中的 verified email
|
||
|
||
### C. Admin 後台 force bind
|
||
|
||
1. 建立 / 更新 `principals`
|
||
2. 建立 / 更新 `external_identities`
|
||
3. 將 `identity_enforcement` 設為 `verified`
|
||
4. 之後 email handoff / verified-source lookup 才能正確工作
|
||
|
||
### D. Admin 後台 unbind(最容易出 bug)
|
||
|
||
解除綁定不是只做一件事,正確流程應是:
|
||
|
||
1. 刪除 `external_identities(platform, external_user_id)`
|
||
2. 把 `identity_enforcement` 重設回 `new`
|
||
3. **同步撤銷 pairing approval**
|
||
4. 寫 audit log,最好記下 `pairing_revoked=true/false`
|
||
5. 必要時再檢查 home-channel / control-channel 是否仍引用該 chat
|
||
|
||
> 2026-07-28 修正:`GatewayUserStore.unbind_identity()` 已同步呼叫 `PairingStore().revoke(platform, external_user_id)`。
|
||
>
|
||
> 這是為了修正「刪綁定後仍能聊天」的通案問題,因為單刪 DB row 不足以撤銷所有授權面。
|
||
|
||
## 平台差異:Telegram vs LINE
|
||
|
||
### Telegram
|
||
|
||
重點:
|
||
|
||
- `plugins/platforms/telegram/adapter.py::_is_user_authorized_from_message`
|
||
- 這是 **intake prefilter**,在 text batching 與 event construction 之前執行
|
||
- 原則:
|
||
- 有明確 allowlist 時,可早期拒絕
|
||
- **未知 DM 且沒有 allowlist 時,不能過早 default-deny**
|
||
- 否則新使用者無法進入 pairing / 驗證流程
|
||
|
||
### LINE
|
||
|
||
重點:
|
||
|
||
- `plugins/platforms/line/adapter.py` 的 allowlist gate 與 verified-email handoff 路徑
|
||
- 原則:
|
||
- **DM user source 要能進 gateway authz / verification 層**
|
||
- group / room source 仍可在 adapter 層直接擋掉
|
||
- 若太早在 adapter 層拒絕 DM,使用者會看到「完全沒反應」,尤其是 restart 後最容易誤判成壞掉
|
||
- LINE 還有平台特有問題:
|
||
- reply token 60 秒上下壽命
|
||
- push quota / 429(月額度)
|
||
- 改寄 email 時仍需驗證 live 綁定 email
|
||
|
||
## 已知高風險 bug 類型
|
||
|
||
### 1. Unbind 只刪 DB,沒 revoke pairing
|
||
症狀:
|
||
- 後台顯示已解綁
|
||
- runtime DB 查不到 binding
|
||
- 但使用者仍可直接對話
|
||
|
||
根因:
|
||
- pairing approved 是獨立授權面
|
||
|
||
修法:
|
||
- `GatewayUserStore.unbind_identity()` 必須同步 revoke pairing
|
||
|
||
### 2. Telegram prefilter 太早 default-deny
|
||
症狀:
|
||
- 新使用者 DM 根本進不了 onboarding
|
||
- 看起來像 bot 靜音
|
||
|
||
根因:
|
||
- 在沒有 allowlist 的情況下,把未知 DM 直接擋掉
|
||
|
||
修法:
|
||
- 無 allowlist 時要讓未知 DM 進到正常 pairing / verification flow
|
||
|
||
### 3. LINE adapter 太早拒絕 DM
|
||
症狀:
|
||
- restart 後或新使用者第一次來訊時,LINE 看起來完全沒反應
|
||
|
||
根因:
|
||
- adapter 在 DM 層就把 unauthorized source 擋掉,沒讓 gateway authz 判斷
|
||
|
||
修法:
|
||
- user-type source 應 defer 到 gateway authz;group/room 才維持 adapter-gated
|
||
|
||
### 4. Email handoff 用到舊綁定或錯綁 email
|
||
症狀:
|
||
- LINE/Telegram 結果被寄到不該寄的信箱
|
||
- 或因綁定已變更而 handoff 失敗
|
||
|
||
根因:
|
||
- 沒用 `bound_email_matches_source(...)` 重新比對 live verified email
|
||
|
||
修法:
|
||
- 所有 system-initiated email handoff,在送出前都要做 live 比對
|
||
|
||
### 5. 只測 admin route,不測 user-visible 行為
|
||
症狀:
|
||
- API 200 OK,但使用者實際還是能聊 / 或根本進不了驗證
|
||
|
||
根因:
|
||
- 只驗 route contract,沒驗 pairing store / authz / adapter gating
|
||
|
||
修法:
|
||
- 同時驗 DB、pairing store、adapter、gateway authz 與對話行為
|
||
|
||
## 修改程式時必看檔案
|
||
|
||
### 核心驗證 / 綁定 / 解綁
|
||
- `gateway/user_verification.py`
|
||
- `is_verified_source`
|
||
- `get_verified_email_for_source`
|
||
- `bound_email_matches_source`
|
||
- `process_inbound_message`
|
||
- `force_bind_identity`
|
||
- `unbind_identity`
|
||
|
||
### Gateway authz
|
||
- `gateway/authz_mixin.py`
|
||
- pairing approved 與 allowlist / adapter policy 的聯集規則
|
||
|
||
### Telegram intake prefilter
|
||
- `plugins/platforms/telegram/adapter.py`
|
||
- `_is_user_authorized_from_message`
|
||
|
||
### LINE intake / fallback / verified-email handoff
|
||
- `plugins/platforms/line/adapter.py`
|
||
- allowlist gate(DM defer vs group/room reject)
|
||
- `_verified_email_for_chat`
|
||
- `_verified_email_target_if_bound`
|
||
- email fallback / quota fallback 路徑
|
||
|
||
### Admin route
|
||
- `hermes_cli/web_server.py`
|
||
- `/api/admin/verification/unbind`
|
||
- `/api/admin/verification/force-bind`
|
||
- `/api/pairing/revoke`
|
||
|
||
## 修改守則(請當成 checklist)
|
||
|
||
1. **不要只改一層。**
|
||
- 驗證問題通常同時跨 adapter、authz、runtime DB、pairing store。
|
||
2. **解除綁定一定要想 pairing。**
|
||
- 問自己:`external_identities` 沒了之後,還有哪個面會放行?
|
||
3. **處理 email handoff 一定要比對 live verified email。**
|
||
- 不可依賴 session 中舊 email。
|
||
4. **Telegram / LINE 新使用者 DM 要能進 onboarding。**
|
||
- 不可因為強化安全而把 onboarding 一起封死。
|
||
5. **group / room / forum 的規則要和 DM 分開想。**
|
||
- 很多 regressions 來自把 DM 修法錯套到群組面。
|
||
6. **若改到授權規則,請同步補回歸測試。**
|
||
- 尤其是 unbind、pairing、verified email handoff。
|
||
7. **如果 user-visible 行為依賴 restart 才生效,要在變更說明寫清楚。**
|
||
|
||
## 建議最少測試集
|
||
|
||
> 詳細測試地圖、對應檔案職責與建議執行順序,請直接看:
|
||
>
|
||
> - `references/line-telegram-test-map.md`
|
||
> - `references/line-telegram-must-run-commands.md`
|
||
|
||
### 已存在且這次相關的測試
|
||
- `tests/hermes_cli/test_web_verification_admin.py`
|
||
- `tests/gateway/test_pairing.py`
|
||
- `tests/gateway/test_pairing_allowlist_bypass.py`
|
||
- `tests/gateway/test_line_plugin.py`
|
||
- `tests/gateway/test_telegram_auth_check.py`
|
||
- `tests/gateway/test_verified_email_handoff_guard_helpers.py`
|
||
- `tests/gateway/test_line_telegram_verification_smoke.py`
|
||
|
||
### 變更 unbind / pairing 時至少要驗
|
||
1. force-bind 成功
|
||
2. unbind 後:
|
||
- `external_identities` row 消失
|
||
- `identity_enforcement.state == new`
|
||
- pairing approved 也消失
|
||
3. audit log 有記下是否撤銷 pairing
|
||
|
||
### 變更 LINE / Telegram intake 時至少要驗
|
||
1. 新 DM 使用者是否還能進 onboarding
|
||
2. group / room / forum 是否沒有被意外放開
|
||
3. 沒有 allowlist 時,是否仍維持預期安全邊界
|
||
|
||
### 變更 email handoff 時至少要驗
|
||
1. live verified email match 才能送
|
||
2. 綁定改掉後舊 email 不應再被使用
|
||
3. LINE 429 / quota fallback 時仍不能繞過 live 綁定檢查
|
||
|
||
## 給未來維護者的一句話
|
||
|
||
如果你正在改 LINE / Telegram 驗證流程,請把它當成 **多層授權狀態機**,不要當成「單一路由 + 一張表」的 CRUD 問題。
|
||
|
||
這套東西最怕的是:
|
||
- 看起來修好了
|
||
- 後台也 200 OK
|
||
- 但另一個授權面還在放行
|
||
|
||
那種 bug 會反覆回來嚇人,而且每次都像新 bug,其實只是同一顆地雷換角度爆炸。 |