致命發現(grep MC + FAA source 確認):
- MC source 沒有 issue delegated download token endpoint
- MC source 沒有 validate delegated download token endpoint
- FAA MemberCenterDelegatedDownloadTokenValidator.cs 假設的 MC introspection endpoint 不存在
- ADR-014 §2 從 5/2 寫完到現在這條鏈一直是斷的、只是因為從未實際 e2e 跑通過所以沒被發現
使用者拍板硬約束:不動 MC + 不動 FAA
新增 ADR-016:
- visionA download 改用 converter GET /api/v1/jobs/{id}/result(新 endpoint)
- visionA backend 用既有 ConverterAPIKey 認證(不需新增 secret)
- 維持 T4 已實作的 stream proxy 結構(io.CopyN + Content-Disposition + size cap)
- promote 仍 PUT FAA(converter 內部用自己的 OAuth、與 visionA 無關)
- 不需動 MC + FAA + warrenchen
- 6 個替代方案逐一說明排除理由
修訂既有文件:
- ADR-014 v1.1 → v1.2:§2 download flow 標註被 ADR-016 部分 supersede
- ADR-015 v2.0 → v2.1:§2 visionA → FAA delegated token 設計(v2.0 從 v1.x 撤回的設計)再次撤回;§9 env 表撤回 v2.0 加回的 OIDC ServiceClient* / TenantID / FAABaseURL;visionA 端 server-to-server 只剩 ConverterAPIKey 一把
- conversion.md v0.5 → v0.6:§1 sequence diagram 重畫(移除 MC node)、§2 模組設計(mc_token_client.go 整檔刪除確認、faa_client.go 改名 converter_result_client.go)、§3.2 visionA → FAA 整段標撤回、§4.1 download handler 改 converter.GetResult、§6 錯誤碼撤回 mc/faa 三個 code 加 result_not_found / result_expired
- api-conversion.md v0.5 → v0.6:檔頭 Auth 段落改寫、§4 download endpoint 改述、error code 表撤回 mc_token_unavailable / download_token_failed
- oidc-tdd.md v0.3 → v0.4:§13.1 環境變數表 OIDC ServiceClient* / TenantID / FAABaseURL 從「重新啟用」改回「再次廢棄」、§13.1.1 stage env 範例移除 service client / tenant_id / FAA URL、§13.1.3 改寫為「v0.4 單線設計」說明
整體影響:
- 不需復活 mc_token_client.go(commit 86b7175 砍除狀態維持)
- 不需復活 OIDCConfig.ServiceClientID/Secret/TenantID(commit 86b7175 移除狀態維持)
- visionA backend faa_client.go 要改名為 converter_result_client.go、改呼叫 converter.GetResult
- visionA backend flow.go DownloadStream / PromoteToModels 改用 converter.GetResult
- jimchen 跨 repo 任務:converter scheduler 加 GET /api/v1/jobs/{id}/result endpoint
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
629 lines
46 KiB
Markdown
629 lines
46 KiB
Markdown
# ADR-015:visionA → converter 採 pre-shared API key 認證(取代 OAuth client_credentials)— 範圍縮限至 visionA → converter
|
||
|
||
## 狀態
|
||
Accepted — 2026-05-11 / **範圍縮限 — 2026-05-16 (v2.0)** / **§2 visionA → FAA 整段再次撤回 — 2026-05-16 (v2.1)**
|
||
|
||
> **v2.1 撤回摘要(2026-05-16 下午)**:v2.0 §2「visionA → FAA 回到 ADR-014 §2(MC service token + delegated download token)」**整段再次撤回**。原因:對 MC source 完整驗證後發現 MC **從未有** issue / validate delegated download token endpoint—— ADR-014 §2 從 2026-05-02 起即為 broken design、v2.0 沿用該設計也是 fictional。
|
||
>
|
||
> **v2.1 新設計**:visionA download 改走 [ADR-016](./adr-016-download-via-converter.md)(converter 新增 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉)。visionA 端不再有任何 visionA → MC / visionA → FAA 的 server-to-server 路徑。**visionA → converter API key 路線(§1)維持完全不變**。
|
||
>
|
||
> **v2.0 範圍縮限摘要(保留歷史)**:v1.x 原本決策「visionA → converter / FAA 兩條 server-to-server 線都改 API key」。2026-05-16 使用者拍板**撤回 visionA → FAA 改 API key 部分**,FAA 線回到 ADR-014 §2 原設計。**v2.0 後 v2.1 再次撤回** — FAA 線完全交由 ADR-016 取代(converter 中轉,不再有 visionA → FAA / visionA → MC 鏈)。
|
||
|
||
## 上位 / 同層 ADR
|
||
|
||
- **部分 supersedes**:[ADR-014](./adr-014-conversion-integration.md) §5「Service token cache — 仿 converter scheduler 模式」中 **converter 部分**(visionA → converter 的 service token / scope `converter:job.write/read` 取消)、§7「失敗模式 retry 矩陣」中 converter MC token row(converter 線不再經 MC)。
|
||
- **v2.0 修正(保留歷史)**:v2.0 一度把 FAA 部分維持 ADR-014 §2 原設計,但 **v2.1 整段撤回**——FAA 部分的 supersede 改由 [ADR-016](./adr-016-download-via-converter.md) 接手(visionA download 改走 converter result endpoint,視 ADR-014 §2 / 本 ADR v2.0 §2 為 broken design)。
|
||
- **v2.1 後**:本 ADR 對 ADR-014 的 supersede 範圍維持「§5 中 converter 部分 / §7 中 converter MC token row」;ADR-014 §2 / §5 中 FAA 部分 / §6 / §7 中 FAA + MC delegated token row 由 ADR-016 統一 supersede。
|
||
- ADR-014 的其他段落(upload streaming proxy、半自動 promote 原則、不擴 model schema、模組劃分)仍有效。
|
||
- **被 supersede**(v2.1 新增):本 ADR §2「visionA → FAA」整段被 [ADR-016](./adr-016-download-via-converter.md) supersede。visionA download 不再有 visionA → FAA / visionA → MC 任何鏈路。
|
||
- **不影響**:[ADR-013](./adr-013-public-client.md) — user login 的 OIDC public PKCE client 仍照舊。本 ADR v2.1 後只動「server-to-server visionA → converter」這條線;「user login」維持不變;「server-to-server visionA → FAA」這條線**直接不存在**(由 ADR-016 撤回)。
|
||
- 沿用:[ADR-006](./adr-006-no-redis-in-prototype.md)(in-memory state)、[ADR-010](./adr-010-oidc-bff.md)(user login 的 OIDC BFF Pattern)、[ADR-011](./adr-011-supersede-adr-005.md)(取代 StaticAuth)
|
||
|
||
## 背景 (Context)
|
||
|
||
### Phase 0.8 OAuth client_credentials 鏈路失敗事件(2026-05-09 ~ 11)
|
||
|
||
ADR-014 原本設計 visionA backend → converter / FAA 的 server-to-server 認證走 Member Center(MC)的 OAuth `client_credentials` grant:
|
||
|
||
1. visionA backend 啟動時讀 `VISIONA_OIDC_SERVICE_CLIENT_ID` / `SECRET`
|
||
2. 第一次需要時打 MC `POST /oauth/token` 換 service token(scope `converter:job.write/read`、`files:download.read/delegate`)
|
||
3. 帶 `Authorization: Bearer <service-token>` 打 converter / FAA
|
||
4. converter / FAA 端 middleware 驗 JWKS 簽章 + 驗 scope + 驗 tenant
|
||
|
||
Phase 0.8 stage 部署實際跑到才發現**整條鏈路有 4 個串行 blocker**:
|
||
|
||
| # | Blocker | 影響 |
|
||
|---|---------|------|
|
||
| 1 | MC stage 沒註冊 `converter:job.read/write` 兩個 scope(progress.md 5/2 `assume 都有` 證實是錯的)| `POST /oauth/token` 直接 400 `invalid_scope` |
|
||
| 2 | stage 上 converter image 是 5 週前舊版,沒 OAuth middleware、沒 `/api/v1/jobs` endpoint | 即使 MC 補 scope,converter 仍無法接受 service token |
|
||
| 3 | converter 缺 `MEMBER_CENTER_*` env 設定 | converter 無法 init OIDC middleware |
|
||
| 4 | FAA stage 也可能要 OAuth 整合(warrenchen 維護)| 不確定狀態,需跨人協調 |
|
||
|
||
要修齊這 4 個 blocker 必須跨 3 個 repo 改 + 同步 MC team + redeploy converter + warrenchen 配合 FAA — 對「2 條 1:1 trust 關係的 server-to-server 整合」明顯過度設計。
|
||
|
||
### v1.x 過度設計的訊號(converter 線適用)
|
||
|
||
OAuth `client_credentials` + JWKS + scope 機制適合的場景:
|
||
|
||
- **多個 client 對同一個 resource server**(如 SaaS 平台對外開放 API、第三方 dev 串接),需要區分不同 client 權限、需要短 TTL token 限制 blast radius、需要 scope 細粒度
|
||
- **不同團隊 / 不同信任邊界**,client 端的 secret 不能由 server 端管理
|
||
|
||
**visionA → converter** 的場景**完全不符合上面任一個**:
|
||
|
||
- **1:1 trust 關係**(visionA 是 converter 唯一的 server-to-server caller,沒有第三方)
|
||
- **使用者同時維護 visionA + converter**(jimchen),可單方拍板改 middleware
|
||
- **全部 internal trust**(不是給外部 dev 用,沒有 untrusted client)
|
||
- **無需 scope 細分**(converter 只關心「是否為 visionA」,單一布林)
|
||
|
||
而成本:
|
||
|
||
- **複雜度**:MC 端要 onboard scope、issuer JWKS 要可達、token cache 要寫對、TTL 邊界要處理、4 個 5xx / 4xx 失敗模式要 retry & graceful degrade
|
||
- **鏈路長度**:visionA → MC → cache → converter,任一節點掛掉都不能轉檔
|
||
- **可觀測性負擔**:要追的 metrics 包含 token cache hit rate、MC 失敗率、scope 對齊狀態
|
||
|
||
### 為什麼 v2.0 撤回 FAA 改 API key(FAA 線適用相反邏輯)
|
||
|
||
v1.0 把同一套「過度設計」邏輯外推到 FAA,但 FAA 線的實際情況不同:
|
||
|
||
1. **FAA 是 warrenchen 維護的公司共用 repo**:協調成本不對稱(converter 是 jimchen 自己的、可單方改;FAA 改一行都要走跨人協調)
|
||
2. **MC 端針對 FAA 的 scope 早已備妥**:使用者於 2026-05-16 提供的 stage service client(`4242ba63099d4f318dd3f143d27ef4c5`)含 `files:upload.write files:metadata.read files:delete files:download.delegate` 4 個 scope,**完整 cover FAA 4 個 endpoint**(PUT / GET metadata / HEAD / DELETE / GET file),不需要 MC team 額外 onboard
|
||
3. **FAA 已內建 dual-auth 設計**:`/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs` line 184-254 顯示 `GET /files/{key}` 下載 endpoint **沒掛 `RequireAuthorization()`**,改用 `IDelegatedDownloadTokenValidator.ValidateAsync(...)` 驗 delegated download token;其他 endpoint(`PUT` / `metadata` / `HEAD` / `DELETE`)才走 JWT Bearer + `EnsureJwtScopeAndTenant`。
|
||
→ **FAA 設計上 download 就是要 delegated token、不接 service token**;如果硬要把 FAA 全 endpoint 改成 API key middleware,等於要 warrenchen 重寫整套 dual-auth、把既有 delegated token validator 拔掉,遠超「補 middleware」的成本
|
||
4. **5/9 撞 MC scope 沒註冊的痛主因在 converter 線**(`converter:job.read/write` 兩個 scope 不存在);FAA 線 4 個 scope 已備妥的事實在當時尚未驗證
|
||
5. **converter 線 v1.0 改 API key 已能解決「最痛的」blocker**(不必動 MC、不必動 FAA、不必跨人);FAA 線本就 1:N(FAA 之後可能服務多個 visionA-like 產品線),維持 OAuth 框架對 FAA 端架構演進更友善
|
||
|
||
→ v2.0 縮限至「**只動 converter 線;FAA 線回到 ADR-014 §2 原設計**」是更精確的責任邊界劃分。
|
||
|
||
### 已洩漏的 stage service client secret(v1.x 觀察事實 — v2.0 部分仍適用)
|
||
|
||
`RciRUyiCkbd60ikkZGkfQ2xV4r02VW3/j0ASKV/DD/E=` 已在對話中外洩(progress.md 2026-05-11 紀錄)。
|
||
|
||
- **v1.x 的處理**:改用 API key 後此 secret 直接作廢、不需 rotate
|
||
- **v2.0 的處理**:FAA 線改回 service token 路線後,使用者於 2026-05-16 提供的**新** stage service client `4242ba63099d4f318dd3f143d27ef4c5` 取代舊 client(舊的洩漏值仍作廢、不重用);新 secret **僅放 stage host `.env.stage` 與部署 secret store、絕不進 git / 文件**(本 ADR、TDD、env example 一律不寫真實 secret 值)
|
||
|
||
## 決策 (Decision)
|
||
|
||
### v2.0 範圍:採 **pre-shared API key + `Authorization: Bearer <api-key>` header** 取代 OAuth client_credentials,**僅適用於 visionA → converter**。visionA → FAA 維持 ADR-014 §2 原設計(MC service token + delegated download token)。
|
||
|
||
### 1. visionA → converter(v1.0 採用 / v2.0 維持)
|
||
|
||
```
|
||
visionA backend 啟動時
|
||
↓
|
||
讀 env VISIONA_CONVERTER_API_KEY
|
||
↓
|
||
[轉檔請求進來]
|
||
↓
|
||
打 converter:
|
||
Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
|
||
```
|
||
|
||
converter 端 middleware:
|
||
|
||
```
|
||
讀 env CONVERTER_API_KEY
|
||
↓
|
||
[收到請求]
|
||
↓
|
||
parse Authorization header → 取 token
|
||
↓
|
||
subtle.ConstantTimeCompare(token, CONVERTER_API_KEY)
|
||
↓
|
||
match → 放行;mismatch → 401
|
||
```
|
||
|
||
### 2. ⚠️ visionA → FAA(v2.1:整段撤回,改走 ADR-016 converter 中轉)
|
||
|
||
> **v2.1 撤回(2026-05-16 下午)**:v2.0 在本節「FAA 線回到 ADR-014 §2 原設計(MC service token + delegated download token)」**整段撤回**。
|
||
>
|
||
> **理由(致命發現 2026-05-16)**:
|
||
> 1. MC source 沒有 `POST /file-access/download-tokens` endpoint(visionA 無法跟 MC 換 delegated token)
|
||
> 2. MC source 沒有 FAA `IDelegatedDownloadTokenValidator` assume 的 introspection endpoint(即使有 token 也無法 validate)
|
||
> 3. FAA `GET /files/{key}` 強制只接 delegated token、不接 service token
|
||
>
|
||
> → ADR-014 §2 與本 ADR v2.0 §2 描述的「visionA → MC → FAA delegated token 鏈」**完全是 fictional**(從 2026-05-02 起未曾 e2e 跑通過)。
|
||
>
|
||
> **v2.1 採用的設計**:visionA download 改走 [ADR-016](./adr-016-download-via-converter.md)(converter 新增 `GET /api/v1/jobs/{id}/result` endpoint + visionA stream 中轉)。visionA 端不再有任何 visionA → MC server-to-server 路徑、不再有任何 visionA → FAA 直接呼叫。
|
||
>
|
||
> 本節以下內容**僅作歷史保留**、實作以 ADR-016 為準;v2.1 後對應的 visionA 端 code 應撤回(mc_token_client.go 不需復活,已於 commit `86b7175` 砍除 → 維持砍除;OIDCConfig.ServiceClientID/Secret + ConversionConfig.TenantID 維持 v1.x 廢棄狀態)。
|
||
|
||
**v2.0 原採用的設計(即 ADR-014 §2 原設計,v2.1 整段撤回,僅作歷史保留)**:
|
||
|
||
```
|
||
visionA backend 啟動時
|
||
↓
|
||
讀 OIDCConfig.ServiceClientID / ServiceClientSecret + ConversionConfig.TenantID
|
||
↓
|
||
[需要打 FAA,例如「加到模型庫」server-to-server pull、或「下載」proxy]
|
||
↓
|
||
visionA → MC POST {issuer}/oauth/token
|
||
grant_type=client_credentials
|
||
client_id=<ServiceClientID>
|
||
client_secret=<ServiceClientSecret>
|
||
scope=files:upload.write files:metadata.read files:delete files:download.delegate
|
||
↓
|
||
MC 回 service access_token(cache 至 exp - 15s)
|
||
↓
|
||
A 路線(FAA 寫 / metadata / delete / s2s download):
|
||
visionA 帶 Authorization: Bearer <service-token> 打 FAA
|
||
FAA 端 .RequireAuthorization() + EnsureJwtScopeAndTenant 驗
|
||
├─ JWT 簽章(FAA `AddJwtBearer` Authority = MC issuer,自動 JWKS)
|
||
├─ Audience(FAA `Auth:Audience`)
|
||
├─ scope claim(PUT 要 files:upload.write;GET metadata / HEAD 要 files:metadata.read;DELETE 要 files:delete)
|
||
└─ tenant_id claim 必須等於 instanceOptions.TenantId
|
||
→ 通過則放行
|
||
|
||
B 路線(download stream proxy — visionA backend 中轉到 browser):
|
||
1. visionA 帶 service-token 打 MC POST /file-access/download-tokens
|
||
(scope: files:download.delegate;針對特定 object_key + GET method + 5 分鐘 TTL)
|
||
2. MC 回 delegated download token (opaque)
|
||
3. visionA 帶 Authorization: Bearer <delegated-token> 打 FAA GET /files/{key}
|
||
4. FAA 端 GET /files/{key} 沒掛 .RequireAuthorization(),
|
||
改走 IDelegatedDownloadTokenValidator.ValidateAsync(...)
|
||
├─ token active
|
||
├─ tenant_id match
|
||
├─ object_key match
|
||
└─ method == "GET"
|
||
→ 通過則 stream NEF binary
|
||
5. visionA backend io.CopyN(...) 中轉回 browser
|
||
```
|
||
|
||
**為什麼 v2.0 設計「download 線必須是 delegated token」而非「service token」**:
|
||
|
||
FAA 端的 dual-auth 設計(已實作於 `/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs`)強制如此:
|
||
|
||
| FAA endpoint | line range | auth 機制 | 適用 scope / token type |
|
||
|--------------|-----------|----------|------------------------|
|
||
| `GET /files/metadata/{**objectKey}` | 80-111 | `.RequireAuthorization()` + `EnsureJwtScopeAndTenant` | `files:metadata.read` (service token) |
|
||
| `HEAD /files/{**objectKey}` | 113-148 | `.RequireAuthorization()` + `EnsureJwtScopeAndTenant` | `files:metadata.read` (service token) |
|
||
| `PUT /files/{**objectKey}` | 150-182 | `.RequireAuthorization()` + `EnsureJwtScopeAndTenant` | `files:upload.write` (service token) |
|
||
| **`GET /files/{**objectKey}`** | **184-254** | **無 `.RequireAuthorization()`;用 `IDelegatedDownloadTokenValidator.ValidateAsync(...)`** | **`files:download.delegate` 換出來的 delegated token** |
|
||
| `DELETE /files/{**objectKey}` | 256-287 | `.RequireAuthorization()` + `EnsureJwtScopeAndTenant` | `files:delete` (service token) |
|
||
|
||
→ FAA `GET /files/{key}` **不接 service token**,必須用 MC 簽的 delegated download token。
|
||
→ visionA download flow 必須做「換 delegated token」這一步、不能省。
|
||
→ visionA「加到模型庫」server-to-server pull 流程因為走的也是 `GET /files/{key}` 下載端點,**也要走 delegated download token 路徑**(v2.0 修正:v1.x 與 ADR-014 §3 「scope `files:download.read`」描述不精確;FAA 端 source 真相是 download endpoint 一律用 delegated token,scope `files:download.delegate` 是 service client 用來「向 MC 換 delegated token」的能力,不是 FAA 端 endpoint 接收的 scope)
|
||
|
||
**stage 端證據(使用者 2026-05-16 提供)**:
|
||
|
||
- FAA stage URL:`https://stage-9527.innovedus.com:5081`
|
||
- TenantId:`732270c0-449c-489c-bfad-321e9bf89b3d`
|
||
- ServiceClientId:`4242ba63099d4f318dd3f143d27ef4c5`(取代 v1.x 提到的舊 client `23605e14...`)
|
||
- ServiceScopes:`files:upload.write files:metadata.read files:delete files:download.delegate`
|
||
- ServiceClientSecret:放 stage host `.env.stage`,不進 git / 文件
|
||
|
||
**待 verify(合規性段落追蹤)**:MC stage 端是否確實註冊上述 4 個 scope 並對該 service client 生效,需 stage redeploy 前實測 `POST /oauth/token` 拿到含 4 scope 的 access_token。
|
||
|
||
### 3. 單一下游:converter(v1.x「每個下游各自獨立的 key」表格 v2.0 縮限)
|
||
|
||
**v1.x 的 key 表格** 縮限至 converter 一條:
|
||
|
||
| Key | 持有者 | 用途 |
|
||
|-----|--------|------|
|
||
| `VISIONA_CONVERTER_API_KEY`(visionA 端) / `CONVERTER_API_KEY`(converter 端) | jimchen | visionA → converter |
|
||
|
||
**v1.x 中的 FAA key row(`VISIONA_FAA_API_KEY` / `FAA_API_KEY`)撤回**——FAA 改回 MC service token 路徑、不需要 pre-shared API key。
|
||
|
||
理由(converter 維持):每條 trust boundary 各自獨立。converter 線 1:1 trust,雙方都由 jimchen 維護,rotate 對齊成本可控。
|
||
|
||
### 3.5 Reference Middleware Implementation(v2.0 縮限至 converter 端)
|
||
|
||
本節提供 converter 端 middleware 的可直接照抄 reference snippet。
|
||
|
||
#### 3.5.1 converter 端(Go — net/http 標準 middleware pattern)
|
||
|
||
採 `net/http` 標準 middleware pattern,可直接套用 `chi` / `gorilla/mux` / 原生 `http.ServeMux`。`subtle.ConstantTimeCompare` 是 Go 標準庫提供的 constant-time 比較函式(避免 timing attack)。
|
||
|
||
```go
|
||
// internal/middleware/apikey.go
|
||
package middleware
|
||
|
||
import (
|
||
"crypto/subtle"
|
||
"errors"
|
||
"log/slog"
|
||
"net/http"
|
||
"strings"
|
||
)
|
||
|
||
// ErrAPIKeyNotConfigured 啟動時 server 端 API key 未設定 — 應在 main() init 時 fail-fast、
|
||
// 不要等到第一個 request 才發現
|
||
var ErrAPIKeyNotConfigured = errors.New("CONVERTER_API_KEY env not set")
|
||
|
||
// NewAPIKeyAuth 回傳一個驗證 Authorization: Bearer <api-key> 的 middleware。
|
||
// 若 expectedKey 為空字串,會直接 panic(啟動 fail-fast)— 避免「未設定 = 全部放行」的災難。
|
||
func NewAPIKeyAuth(expectedKey string, logger *slog.Logger) func(http.Handler) http.Handler {
|
||
if expectedKey == "" {
|
||
// 啟動時偵測 — 不允許 server 在沒有 key 的狀態下啟動
|
||
panic(ErrAPIKeyNotConfigured)
|
||
}
|
||
expectedBytes := []byte(expectedKey)
|
||
|
||
return func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
authHeader := r.Header.Get("Authorization")
|
||
|
||
// missing Authorization header
|
||
if authHeader == "" {
|
||
logger.Warn("api key auth failed",
|
||
"reason", "missing_authorization_header",
|
||
"path", r.URL.Path,
|
||
"remote", r.RemoteAddr)
|
||
writeUnauthorized(w)
|
||
return
|
||
}
|
||
|
||
// 不是 Bearer prefix(必須有 "Bearer " 前綴 + 空格)
|
||
const prefix = "Bearer "
|
||
if !strings.HasPrefix(authHeader, prefix) {
|
||
logger.Warn("api key auth failed",
|
||
"reason", "missing_bearer_prefix",
|
||
"path", r.URL.Path,
|
||
"remote", r.RemoteAddr)
|
||
writeUnauthorized(w)
|
||
return
|
||
}
|
||
|
||
token := strings.TrimSpace(authHeader[len(prefix):])
|
||
|
||
// token 為空("Bearer " 後面什麼都沒有)
|
||
if token == "" {
|
||
logger.Warn("api key auth failed",
|
||
"reason", "empty_token",
|
||
"path", r.URL.Path,
|
||
"remote", r.RemoteAddr)
|
||
writeUnauthorized(w)
|
||
return
|
||
}
|
||
|
||
// constant-time compare — 即使長度不同 ConstantTimeCompare 也會回 0,但為了
|
||
// 提早 short-circuit、先檢查長度可避免 hash 不必要的工作(長度本身不是 secret)
|
||
if subtle.ConstantTimeCompare([]byte(token), expectedBytes) != 1 {
|
||
logger.Warn("api key auth failed",
|
||
"reason", "token_mismatch",
|
||
"path", r.URL.Path,
|
||
"remote", r.RemoteAddr)
|
||
// 注意:log 絕對不印 token 本身
|
||
writeUnauthorized(w)
|
||
return
|
||
}
|
||
|
||
// 通過 — 放行到 next handler
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
}
|
||
|
||
// writeUnauthorized 統一回 401 — 不洩漏「key 對 / 不對」/ 「missing / mismatch」的差異
|
||
func writeUnauthorized(w http.ResponseWriter) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
_, _ = w.Write([]byte(`{"error":"unauthorized"}`))
|
||
}
|
||
```
|
||
|
||
main 端使用範例:
|
||
|
||
```go
|
||
// cmd/converter/main.go(節錄)
|
||
expectedKey := os.Getenv("CONVERTER_API_KEY")
|
||
authMiddleware := middleware.NewAPIKeyAuth(expectedKey, logger)
|
||
// 套用到所有 /api/v1/* routes
|
||
mux.Handle("/api/v1/", authMiddleware(apiHandler))
|
||
```
|
||
|
||
#### 3.5.2 ~~FAA 端(C# ASP.NET Core middleware)~~(v2.0 撤回 — 整段刪除)
|
||
|
||
> **v2.0 撤回**:v1.1 在此處提供的 FAA 端 ASP.NET Core middleware snippet(含 Classic Middleware Class 寫法 A + Minimal API Inline Middleware 寫法 B)**整段刪除**。
|
||
>
|
||
> **理由**:FAA 線回到 ADR-014 §2 原設計(MC service token + delegated download token)後,FAA 端**不需要新增任何 API key middleware**:
|
||
>
|
||
> - 既有 `AddJwtBearer` + `RequireAuthorization()` + `EnsureJwtScopeAndTenant` 已涵蓋 PUT / metadata / HEAD / DELETE 4 個 endpoint
|
||
> - 既有 `IDelegatedDownloadTokenValidator` 已涵蓋 GET download endpoint
|
||
> - **FAA 端零變更**——這是 v2.0 撤回的核心收益(不必動公司共用 FAA repo、不必跟 warrenchen 協調)
|
||
|
||
#### 3.5.3 部署檢查清單(v2.0 縮限至 converter 端)
|
||
|
||
不分 client 端 / server 端,部署前 converter 兩側必須逐項確認:
|
||
|
||
| # | 檢查項 | 為什麼 |
|
||
|---|--------|--------|
|
||
| 1 | env 已設定且非空(啟動 fail-fast)| 避免「未設定 = 全部放行」災難;server 應在啟動時 panic / throw、不要等到第一個 request 才發現 |
|
||
| 2 | constant-time compare(Go `subtle.ConstantTimeCompare`)| 避免 timing attack 反推 key |
|
||
| 3 | 401 response body 統一(不洩漏「key 對 / 不對」/ 「missing / mismatch」差異)| 對外只回 `{"error":"unauthorized"}`,差異只記在 server 端 log |
|
||
| 4 | log 絕對不印 token 本身 | 即使是失敗的 token 也不印(攻擊者可能用半正確的 token 試探);只印 reason / path / remote |
|
||
| 5 | Bearer prefix 嚴格驗證(缺 prefix 也 401)| 不允許 `Authorization: <token>` 這種格式(即使內容對也 reject、強制 client 用標準 Bearer scheme) |
|
||
| 6 | 每環境(dev / stage / prod)獨立 key | 嚴格分環境產 key(`openssl rand -hex 32` 各 64 字元 hex),不重用 |
|
||
| 7 | key 不進 git | `.gitignore` 嚴格 ignore `.env*`;CI / CD secret 從 Secrets Manager / Vault 注入 |
|
||
| 8 | 健康檢查 endpoint 是否要 bypass auth | 通常 `/healthz` / `/readyz` 應 bypass(讓 LB / k8s 可探測),但業務 endpoint 全部要 auth |
|
||
|
||
> **FAA 端不需要本清單**——v2.0 起 FAA 端使用既有 OAuth + delegated token 機制,無新增 API key middleware。FAA 端的 OAuth / delegated token 部署檢查請參考 ADR-014 §2 與 `conversion.md` §3.2。
|
||
|
||
### 4. 不再有 scope 概念(converter 線適用;FAA 線 v2.0 撤回)
|
||
|
||
**converter 線**:OAuth `client_credentials` 設計中兩個 converter scope(`converter:job.write/read`)取消。
|
||
|
||
- 單一 API key 就是「visionA 有權打 converter」的完整證明
|
||
- 不再有「同一個 client 但拿不同 scope」的細粒度區分(在 1:1 trust 中本來就沒意義)
|
||
- converter 端 middleware 也不需要驗 scope
|
||
|
||
**FAA 線**:v1.x 取消 `files:upload.write / files:metadata.read / files:delete / files:download.delegate` 4 個 scope 的決策**撤回**。FAA 4 個 scope 全部恢復、由 MC service client `4242ba63...` 持有,並由 FAA `EnsureJwtScopeAndTenant` 驗。
|
||
|
||
### 5. tenant 概念
|
||
|
||
**converter 線**:visionA → converter 不再帶 tenant_id。converter 端的 user_id 從 multipart body `user_id` field 拿(仍由 visionA 從 OIDC sub 灌入,這條 trust boundary 不變)。converter 端不需要 tenant 概念。
|
||
|
||
**FAA 線(v2.0 修正 — 從 v1.x「不再有 tenant」改為「FAA 線需要 tenant」)**:
|
||
|
||
- FAA 端 `EnsureJwtScopeAndTenant` 函式會驗 service token 內的 `tenant_id` claim 等於 `instanceOptions.TenantId`(見 FAA `Program.cs` line 303-312)
|
||
- delegated download token 路徑也驗 `validationResult.TenantId.Value != instanceOptions.TenantId`(line 218-221)
|
||
- → visionA 的 service token / delegated download token 必須含正確的 `tenant_id` claim
|
||
- 來源:MC service client `4242ba63...` 註冊時對應的 tenant,stage 為 `732270c0-449c-489c-bfad-321e9bf89b3d`
|
||
- → `VISIONA_OIDC_TENANT_ID` env **重新啟用**(v1.x 標廢棄;v2.0 撤回廢棄)
|
||
|
||
### 6. visionA backend 移除的程式碼(v1.x 移除清單,v2.0 部分復活)
|
||
|
||
| 項目 | v1.x 處理 | **v2.0 處理** |
|
||
|------|-----------|---------------|
|
||
| `internal/conversion/mc_token_client.go`(整個 package) | 整個檔案刪除(~440 行) | **部分復活** — 保留 service token cache + delegated download token issue 邏輯(給 FAA 用),但不再被 converter_client 引用 |
|
||
| `internal/conversion/converter_client.go` 內呼叫 `MCTokenClient.ServiceToken()` | 改成讀 `cfg.Conversion.ConverterAPIKey` 直接 set header | **同 v1.x**(不變)—— converter 線維持 API key |
|
||
| `internal/conversion/faa_client.go` 內呼叫 `MCTokenClient.ServiceToken()` | 改成讀 `cfg.Conversion.FAAAPIKey` 直接 set header | **撤回**——回到 v1.x 之前:呼叫 `MCTokenClient.ServiceToken()` 拿 service token、帶 `Authorization: Bearer <service-token>`;download 路徑額外呼叫 `MCTokenClient.IssueDelegatedDownload(...)` |
|
||
| `internal/conversion/flow.go` 內呼叫 `mc.IssueDelegatedDownload()` | 「delegated download token 路徑取消」 | **撤回**——download stream proxy 路徑要呼叫 `IssueDelegatedDownload(...)` 拿 token、再呼叫 `faa.DownloadWithDelegated(token, objectKey)` |
|
||
| `internal/config/config.go` 內 `OIDCConfig.ServiceClientID` / `ServiceClientSecret` 兩個欄位 | 廢棄(保留 struct field 為 backward compat、不再使用);env `VISIONA_OIDC_SERVICE_CLIENT_ID` / `SECRET` 從 `.env*.example` 移除 | **重新啟用**——FAA 線需要;env 加回 `.env.stage.example` |
|
||
| `internal/config/config.go` 內 `ConversionConfig.TenantID` 欄位 + env `VISIONA_OIDC_TENANT_ID` | conversion 模組不再依賴;如其他模組未使用即可移除 | **重新啟用**——FAA 端 `EnsureJwtScopeAndTenant` 驗 tenant_id claim、token 必須帶 |
|
||
|
||
config 欄位 v2.0 樣貌:
|
||
|
||
```go
|
||
// internal/config/config.go
|
||
type ConversionConfig struct {
|
||
ConverterBaseURL string // 既有
|
||
FAABaseURL string // 既有
|
||
ConverterAPIKey string // v1.0 新增 — env VISIONA_CONVERTER_API_KEY(v2.0 維持)
|
||
// FAAAPIKey string // v1.0 加的;v2.0 撤回(不再需要)
|
||
TenantID string // v1.x 廢棄;v2.0 重新啟用 — env VISIONA_OIDC_TENANT_ID
|
||
}
|
||
|
||
// OIDCConfig 維持有 ServiceClientID / ServiceClientSecret 兩欄位(v2.0 重新啟用)
|
||
type OIDCConfig struct {
|
||
// ... user login 相關欄位(不變)...
|
||
ServiceClientID string // v2.0 重新啟用 — env VISIONA_OIDC_SERVICE_CLIENT_ID
|
||
ServiceClientSecret string // v2.0 重新啟用 — env VISIONA_OIDC_SERVICE_CLIENT_SECRET
|
||
}
|
||
|
||
// Enabled 改判定:
|
||
func (c ConversionConfig) Enabled() bool {
|
||
return c.ConverterBaseURL != "" &&
|
||
c.FAABaseURL != "" &&
|
||
c.ConverterAPIKey != "" &&
|
||
// FAA 線判定 OIDCConfig 有 ServiceClientID / Secret + ConversionConfig.TenantID
|
||
// — 由 main.go 啟動時組合判斷,這裡只判 conversion 自己的欄位
|
||
c.TenantID != ""
|
||
}
|
||
```
|
||
|
||
### 7. Delegated download token 路徑的處理(v2.0 撤回 v1.x 的撤回,回到 ADR-014 §2 設計)
|
||
|
||
ADR-014 §2 原設計 download 流程:
|
||
|
||
```
|
||
browser → visionA /download → MC issue delegated token → 302 → browser → FAA?access_token=...
|
||
```
|
||
|
||
**v1.x 的選項 A(短期 server-side proxy with API key)撤回**:v1.0 / v1.1 在本節原本提案「visionA backend 直接用 `Authorization: Bearer <FAA_API_KEY>` 拉 FAA、stream 回 browser」整段全部撤回。
|
||
|
||
**v2.0 採用的設計(保留 server-side stream proxy 不退回 302、但 token 來源改回 delegated download token)**:
|
||
|
||
```
|
||
browser → visionA /download → visionA backend
|
||
↓
|
||
ownership 檢查
|
||
↓
|
||
ensurePromoted(拿 target_object_key)
|
||
↓
|
||
MCTokenClient.ServiceToken() → MC service access_token
|
||
↓
|
||
MCTokenClient.IssueDelegatedDownload(token, object_key, "GET", 5min)
|
||
→ MC POST /file-access/download-tokens
|
||
↓
|
||
FAA GET /files/{key}
|
||
Authorization: Bearer <delegated-token>
|
||
↓
|
||
FAA IDelegatedDownloadTokenValidator.ValidateAsync(...)
|
||
↓
|
||
FAA stream NEF binary
|
||
↓
|
||
visionA backend io.CopyN → browser
|
||
```
|
||
|
||
**為什麼保留 server-side stream proxy(不退回 ADR-014 §2 的 302 redirect)**:
|
||
|
||
- T4 已經在 Phase 0.8 把 download 改成 server-side stream proxy(`conversion.md` v0.4 / `api/api-conversion.md` v0.4),實測 frontend `<a href download>` 流程已驗證
|
||
- 退回 302 redirect 等於 frontend 行為改變、要重做 e2e 驗證、無收益
|
||
- delegated token 在 server-side(不洩漏給 frontend JS / browser URL bar)反而比 302 模式更安全
|
||
- 流量成本(每次下載繞 visionA backend N×)Phase 0.8 MVP 量小可接受;Phase 1 量大時再評估升級
|
||
|
||
**為什麼 token 來源改回 delegated download token(不繼續用 v1.x 的 visionA API key)**:
|
||
|
||
- FAA `GET /files/{key}` endpoint 強制使用 delegated token(line 184-254 沒掛 `RequireAuthorization()`、改用 `IDelegatedDownloadTokenValidator`),FAA 端不接受其他 token type
|
||
- v1.x 若要用 visionA API key,需要 warrenchen 在 FAA 端為 download endpoint 額外實作「API key middleware」並改寫 dual-auth 路由——成本遠超 v2.0 撤回的收益
|
||
|
||
**選項 B(Phase 1+ HMAC token 升級路徑)保留為 follow-up**:
|
||
|
||
如果 Phase 1 流量壓力大要回 302 redirect 模式,visionA 可以自己簽 short-TTL HMAC token(不需要 MC 介入),FAA 端 middleware 多加一條「驗 visionA HMAC token」的路徑:
|
||
|
||
```
|
||
browser → visionA /download → visionA 用 HMAC_KEY 簽 short-TTL token
|
||
↓
|
||
302 → browser
|
||
↓
|
||
browser → FAA?access_token=<visionA-signed-hmac>
|
||
↓
|
||
FAA middleware:JWT (s2s) OR delegated (current) OR HMAC (browser direct) 三選一
|
||
```
|
||
|
||
此升級路徑與本 ADR v2.0 決策無衝突,記入 Phase 1 follow-up(同 v1.x 規劃)。
|
||
|
||
### 8. user_id 注入 trust boundary 不變(v1.x / v2.0 一致)
|
||
|
||
ADR-014 §6「visionA backend 是唯一灌 user_id 的點」邏輯不變:
|
||
|
||
- user_id 仍從 OIDC cookie session 拿(OIDC sub)
|
||
- 仍透過 multipart streaming 注入 converter request 的 `user_id` field(converter 端視 visionA 為 trusted caller)
|
||
- API key(converter)/ service token(FAA)證明的是「caller 是 visionA」,user_id 的真實性由 visionA 內部的 OIDC 機制保證 — 兩條獨立鏈
|
||
|
||
### 9. 部署層的 env 注入(v2.1 修訂)
|
||
|
||
Phase 0.8b v2.1 採用(visionA 端 server-to-server secret 只剩 converter API key 一把):
|
||
|
||
| Env | Stage | Production | v2.1 變更 |
|
||
|-----|-------|-----------|----------|
|
||
| `VISIONA_CONVERTER_API_KEY`(visionA 端) | `.env.stage`(jimchen 持有,不進 git) | AWS Secrets Manager / Vault | 維持(v1.0 新增、v2.0 維持、v2.1 維持) |
|
||
| `CONVERTER_API_KEY`(converter 端) | `.env`(jimchen 持有,不進 git) | 同上 | 維持 |
|
||
| `VISIONA_CONVERTER_BASE_URL` | `.env.stage` | Secrets Manager | 維持(既有) |
|
||
| ~~`VISIONA_FAA_API_KEY`(visionA 端)~~ | — | — | **撤回**(v1.0 加的;v2.0 移除;v2.1 維持移除) |
|
||
| ~~`FAA_API_KEY`(FAA 端)~~ | — | — | **撤回**(v1.0 加的;v2.0 移除;v2.1 維持移除)|
|
||
| ~~`VISIONA_OIDC_SERVICE_CLIENT_ID`~~ | — | — | **v2.1 再次撤回**(v1.x 廢棄;v2.0 重新啟用;v2.1 撤回 v2.0 啟用)|
|
||
| ~~`VISIONA_OIDC_SERVICE_CLIENT_SECRET`~~ | — | — | **v2.1 再次撤回**(同上) |
|
||
| ~~`VISIONA_OIDC_TENANT_ID`~~ | — | — | **v2.1 再次撤回**(同上) |
|
||
| ~~`VISIONA_FAA_BASE_URL`~~ | — | — | **v2.1 撤回**(visionA 端不再直接打 FAA、走 converter 中轉)|
|
||
|
||
key 產生方式:
|
||
- converter API key(兩端對齊):`openssl rand -hex 32`(64 字元 hex)
|
||
- ~~service client secret~~(v2.1 不需要)
|
||
|
||
**v2.1 後 visionA 端 server-to-server 鏈路收斂為單條**:visionA → converter(API key),download 也走同一條(converter `GET /api/v1/jobs/{id}/result`,詳見 ADR-016)。
|
||
|
||
## 考慮過的替代方案 (Alternatives Considered)
|
||
|
||
### 方案 A:維持 OAuth client_credentials(ADR-014 原方案 — converter / FAA 兩條都走)
|
||
|
||
| 評估 | 內容 |
|
||
|------|------|
|
||
| 優點 | 標準化、跨團隊可重用、短 TTL token、scope 細粒度可控 |
|
||
| 缺點 | 需要 MC team 配合 onboard converter scope、converter / FAA 都要重寫 middleware、JWKS 取得失敗時 graceful degrade 複雜、stage e2e 鏈路 4 個 blocker 全要修齊 |
|
||
| 排除原因 | 對 1:1 internal trust 場景(converter)過度設計;Phase 0.8 stage 部署實際遇到的 4 個 blocker 證明這條路 ROI 不好 |
|
||
|
||
### 方案 B:mTLS(mutual TLS)
|
||
|
||
| 評估 | 內容 |
|
||
|------|------|
|
||
| 優點 | 不需傳遞 secret in plaintext(憑證綁定)、cert rotation 機制成熟 |
|
||
| 缺點 | converter / FAA 都要支援 mTLS、需要 CA 管理、ingress(nginx / Caddy / ALB)也要配合 client cert termination、stage 環境部署成本高 |
|
||
| 排除原因 | 對 1:1 trust 過度設計;公司 stage 環境 ingress(host nginx)未對外開放 mTLS 配置;維運成本不成比例 |
|
||
|
||
### 方案 C:API key + IP allowlist 雙層防護
|
||
|
||
| 評估 | 內容 |
|
||
|------|------|
|
||
| 優點 | 即使 API key 洩漏,攻擊者也需從特定 IP 才能用 |
|
||
| 缺點 | visionA 上 AWS 後 IP 不固定(NAT gateway / ALB 對外多 IP);converter / FAA 在公司內網 IP allowlist 維護成本高;對「不是內網」的 prod 場景幾乎沒用 |
|
||
| 排除原因 | Phase 0.8 stage 仍在公司內網(visionA stage 在 192.168.0.x),加 IP allowlist 在 stage 可行但對 prod 沒有延展性;不採用 |
|
||
|
||
### 方案 D:共用一把 API key(不分 converter / FAA)
|
||
|
||
| 評估 | 內容 |
|
||
|------|------|
|
||
| 優點 | env 少一個、部署設定簡單 |
|
||
| 缺點 | 一處洩漏兩處連坐;converter rotate 必須同步 FAA;違反「每條 trust boundary 各自獨立」原則 |
|
||
| 排除原因 | 在 v1.x 兩條都 API key 的設計中作為反方案被排除;v2.0 因 FAA 撤回 API key、議題自然不存在 |
|
||
|
||
### 方案 E(v2.0 採用):visionA → converter API key、visionA → FAA 仍走 MC service token + delegated download token
|
||
|
||
| 評估 | 內容 |
|
||
|------|------|
|
||
| 優點 | (1) 不必動 FAA repo(warrenchen 維護成本歸零);(2) 不必動 MC 端 scope onboarding(4242ba63 service client 已備妥 4 scope);(3) FAA 既有 dual-auth 設計(JWT for write / metadata / delete + delegated token for download)零修改;(4) converter 線 v1.0 已得到的「不必動 MC、不必協調」收益**對 converter 而言維持**;(5) FAA 線維持 OAuth 框架對 FAA 多 client 演進更友善(FAA 之後可能服務多個 visionA-like 產品線)|
|
||
| 缺點 | (1) visionA 仍要保留 mc_token_client 的 service token cache + delegated download token issue 邏輯(v1.x 砍掉的 ~440 行要部分復活);(2) MC 仍是 FAA 線的依賴(MC 掛 → FAA 用不了,但 converter 不受影響);(3) 兩條線的認證機制不對稱(converter API key、FAA OAuth),心智負擔略高 |
|
||
| 排除 v1.0 方案(兩條都 API key)的原因 | 使用者 2026-05-16 拍板:(1) 不希望動 FAA(共用 repo、warrenchen 維護);(2) 不希望動 MC(5/9 撞 scope 沒註冊的痛);(3) 但 5/16 提供的 service client `4242ba63...` 證明 MC 端針對 FAA 的 4 個 scope 已備妥 — v1.0 拒絕走 OAuth 的「MC scope 沒備妥」前提在 FAA 線**不成立**|
|
||
| 採用 | **v2.0 採用** |
|
||
|
||
## 後果 (Consequences)
|
||
|
||
### 正面影響
|
||
|
||
- **converter 線實作大幅簡化**(v1.0 收益保留):visionA backend 對 converter 的呼叫不查 cache、不打 MC、不重簽
|
||
- **converter / FAA stage e2e blocker 收斂**:converter 線 0 個 blocker;FAA 線靠使用者已備妥的 service client `4242ba63...` 驗證後即可上線(待 verify)
|
||
- **不必動 FAA repo、不必動 warrenchen**(v2.0 新增收益):v1.x 規劃要 warrenchen 配合改 FAA middleware 的工作完全取消
|
||
- **不必動 MC scope onboarding**(部分 v2.0 新增收益):FAA 線 4 個 scope 已備妥;converter 線本來就不依賴 MC
|
||
- **converter middleware 極簡**(v1.0 收益保留):只需「比對單一字串 + constant-time compare」
|
||
- **converter 失敗模式收斂**:原本「MC 5xx / MC 4xx / token cache miss / scope mismatch」四個失敗類型,收斂為「API key 對 / 不對」單一布林
|
||
- **可觀測性減負(部分)**:converter 線不需追 token cache hit rate / MC 失敗率;FAA 線仍需追,但範圍縮小一半
|
||
- **已洩漏的 stage service client secret `RciRUyi...` 直接作廢**:使用者於 v2.0 提供新 client `4242ba63...` 取代
|
||
|
||
### 負面影響(接受的取捨)
|
||
|
||
- **converter 線 API key 是 long-lived secret**:不像 OAuth token 有 TTL(通常 1 小時);rotate 需要 visionA + converter 同步換 env 並 redeploy;secret 管理責任更重
|
||
- **converter 線沒有 scope 細粒度**:未來如果 visionA 內部要區分「能 init job 但不能 promote」這種需求,要回頭加(但 1:1 trust 場景幾乎不會有此需求)
|
||
- **converter 線沒有 audit trail(誰用 token 做什麼)**:OAuth + JWT 的 sub claim 提供天然 audit;API key 模式下 converter 只知道「是 visionA」,需要靠 visionA 內部 log + request_id 串接才能追到 user_id(既有 request_id 機制可滿足)
|
||
- **FAA 線維持 OAuth client_credentials 鏈條的部分複雜度**(v2.0 新增):visionA 仍需 mc_token_client(service token cache + delegated download token issue + retry policy);MC 仍是 FAA 線的單點依賴;FAA 線的可觀測性負擔(token cache hit rate / MC 失敗率)保留
|
||
- **兩條線認證機制不對稱**(v2.0 新增):converter 線 API key、FAA 線 OAuth;運維 / 排查時需區分兩種失敗模式(converter 401 = key 不同步;FAA 401 = service token 取得失敗 / scope mismatch / tenant mismatch / delegated token 過期)
|
||
|
||
### 風險
|
||
|
||
| 風險 | 緩解 |
|
||
|------|------|
|
||
| converter API key 洩漏(git log、log shipping、Slack 訊息等)| (1) `.gitignore` 嚴格 ignore `.env*`;(2) log 不印 token;(3) stage / prod 用不同 key;(4) 一旦發現洩漏 → 換 env → redeploy(雙方協調 < 1 小時可完成)|
|
||
| converter API key Rotate 流程缺失 | 配套必須產出 rotate runbook(jimchen 對 converter)|
|
||
| FAA 線 service client secret 洩漏(v2.0 新增)| 同 v1.x 處理:MC team 換 client secret → visionA 同步 env → restart;運維事件,需跨 MC team 協調 |
|
||
| MC stage 端 4242ba63 service client 4 個 scope 是否真的有效 / 啟用(v2.0 新增)| **待 verify**:stage redeploy 前實測 `POST /oauth/token` 帶 4 scope 拿到 access_token;若部分 scope 缺失需 MC team 補(合規性段落追蹤)|
|
||
| FAA 線 MC 不可達 → 無法 issue delegated token → 下載失敗(v2.0 新增)| graceful degradation:對 frontend 回 502 `download_token_failed` / `mc_token_unavailable`(仍維持 ADR-014 §7 原 retry 矩陣)|
|
||
| 開發環境 / stage / prod 用同一把 converter API key | 嚴格分環境產 key(dev / stage / prod 各自 `openssl rand -hex 32`),不重用 |
|
||
|
||
## 合規性
|
||
|
||
- [x] 與使用者確認:v2.0 範圍縮限至 visionA → converter API key;visionA → FAA 回到 ADR-014 §2 原設計(2026-05-16)
|
||
- [x] 與 jimchen 確認(同時為 visionA + converter 維護者):converter middleware 改寫由 jimchen 處理(沿用 v1.0 步驟 4 規劃)
|
||
- [x] ~~與 warrenchen 確認:FAA 端 middleware 改寫由 warrenchen 處理~~(**v2.0 撤回此項;FAA repo 不需改動**)
|
||
- [ ] **與 MC team 驗證 stage 端 4242ba63 service client 4 個 scope 都可用**(v2.0 新增;step 6 stage redeploy 前必驗):
|
||
- `files:upload.write` ✅ 對應 FAA `PUT /files/...`
|
||
- `files:metadata.read` ✅ 對應 FAA `GET /files/metadata/...` + `HEAD /files/...`
|
||
- `files:delete` ✅ 對應 FAA `DELETE /files/...`
|
||
- `files:download.delegate` ✅ 用來向 MC `POST /file-access/download-tokens` 換 delegated download token,再打 FAA `GET /files/{key}`
|
||
- [x] 與 ADR-014 對齊(v2.0 修訂):本 ADR v2.0 起對 ADR-014 的 supersede 範圍縮限至「§5 中 converter 部分 service token / `converter:job.write/read` scope」;ADR-014 §2 / §5 中 FAA 部分 / §6 / §7 中 FAA 與 MC delegated token row 全部恢復有效
|
||
- [x] 與 ADR-013 對齊:本 ADR 不影響 user login 的 public PKCE client
|
||
- [ ] DevOps rotate runbook 待產出(Phase 0.9 follow-up;範圍縮限至 converter API key)
|
||
- [ ] 已洩漏的 stage service client secret `RciRUyi...` 自動作廢(不需 MC rotate;v2.0 由新 client `4242ba63...` 取代)
|
||
|
||
## 配套產出(給後續 Phase)
|
||
|
||
### Phase 0.8b 範圍內(v2.0 修訂)
|
||
|
||
- visionA backend 程式碼改造(backend agent 任務):
|
||
- converter_client.go 維持 v1.0 改造(API key)
|
||
- **faa_client.go 改回呼叫 MCTokenClient.ServiceToken() + 為 download 路徑增 DownloadWithDelegated 變體**
|
||
- **mc_token_client.go 部分復活**(service token cache + delegated download token issue 邏輯)
|
||
- flow.go download 路徑改回呼叫 IssueDelegatedDownload + DownloadWithDelegated
|
||
- config.go 重新啟用 OIDCConfig.ServiceClientID/Secret + ConversionConfig.TenantID
|
||
- converter middleware 改造(jimchen 跨 repo) — 維持 v1.0 規劃
|
||
- ~~FAA middleware 改造(warrenchen 跨 repo)~~ — **v2.0 撤回**
|
||
- `.env.stage.example` 更新(v2.0 修訂):
|
||
- 維持 `VISIONA_CONVERTER_API_KEY` 新增
|
||
- 撤回 `VISIONA_FAA_API_KEY`
|
||
- 加回 `VISIONA_OIDC_SERVICE_CLIENT_ID` / `VISIONA_OIDC_SERVICE_CLIENT_SECRET` / `VISIONA_OIDC_TENANT_ID` / `VISIONA_FAA_BASE_URL`
|
||
- **這部分 .env*.example 更新由 backend agent 下次任務(複活 mc_token_client 時)一併處理;本次架構任務範圍只動共享文件**
|
||
- 設計文件更新(conversion.md / api-conversion.md / oidc-tdd.md — 本 ADR v2.0 同步產出)
|
||
|
||
### Phase 0.9 / Phase 1 follow-up
|
||
|
||
- [ ] converter API key rotate runbook(含「產新 key → 雙方同步 env → restart → 驗證 → 拔舊 key」步驟;範圍縮限至 converter)
|
||
- [ ] 是否設 converter API key 有效期(例如 1 年到期自動提醒)— 由 SRE 流程決定
|
||
- [ ] FAA 上「visionA 自己簽 HMAC token」delegated 機制(§7 選項 B),用於 download 路徑回 302 redirect(同 v1.x 規劃)
|
||
- [ ] 觀察 server-side download proxy 在 Phase 1 量大時的效能 / 頻寬 cost(同 v1.x 規劃)
|
||
|
||
## 相關文件
|
||
|
||
- 部分 supersedes:[`adr-014-conversion-integration.md`](./adr-014-conversion-integration.md)(**v2.0 範圍縮限**:僅 §5 中 converter 部分 service token / scope;§2 / §5 中 FAA 部分 / §6 / §7 中 FAA + MC delegated token row 全部恢復有效)
|
||
- 不影響:[`adr-013-public-client.md`](./adr-013-public-client.md)(user login 部分)
|
||
- 詳細實作(本 ADR v2.0 同步更新):`conversion.md` v0.5、`api/api-conversion.md` v0.5、`oidc-tdd.md` v0.3
|
||
- 觸發背景:`progress.md` 「Phase 0.8b 啟動原因(2026-05-11)」+ 「Phase 0.8b 步驟 2 — Backend agent 任務範圍盤點」+ 「v2.0 範圍縮限(2026-05-16)」段落
|
||
- FAA dual-auth 設計參考:`/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs` line 41-57(JWT Bearer 設定)、line 184-254(download endpoint 不掛 RequireAuthorization、用 IDelegatedDownloadTokenValidator)、line 291-322(EnsureJwtScopeAndTenant + HasScope)
|
||
|
||
## 版本記錄
|
||
|
||
| 日期 | 版本 | 變更 |
|
||
|------|------|------|
|
||
| 2026-05-11 | 1.0 | 初版 — Phase 0.8b 將 server-to-server 認證從 OAuth client_credentials 改 pre-shared API key(visionA → converter / FAA 兩條線都改)|
|
||
| 2026-05-15 | 1.1 | 補 §3.5 Reference Middleware Implementation — Go (converter) + C# (FAA) snippet + 部署檢查清單,給跨 repo 改 middleware 時照抄 |
|
||
| 2026-05-16 | 2.0 | **範圍縮限** — 撤回 visionA → FAA API key 部分(FAA 不動、改回 ADR-014 §2 MC service token + delegated download token);visionA → converter API key 路線保留。撤回原因:(1) 使用者明示不希望動 FAA / MC、(2) 5/9 撞 MC scope 沒註冊的痛尚在、但 (3) 使用者今天提供 FAA stage service client (`4242ba63099d4f318dd3f143d27ef4c5`) 證明 MC 端 4 個 scope 都已備好(含 `files:download.delegate`)、舊路線可走通。涉及修訂:§2 整段標撤回 + 加 v2.0 設計(FAA dual-auth + delegated token download)、§3.5.2 C# FAA snippet 整段刪除、§5 tenant 概念分 converter/FAA 兩段、§6 mc_token_client 改部分復活 + OIDCConfig.ServiceClientID/Secret + ConversionConfig.TenantID 重新啟用、§7 download 路徑改回 delegated token(保留 server-side proxy 不退回 302)、§9 env 表加回 service client + tenant id + 撤回 FAA API key、新增方案 E 排除 v1.0 兩條都 API key 路線、後果重估、合規性「跟 warrenchen 確認」改為「跟 MC team 驗 4 scope」。對 commit `86b7175` 影響:visionA backend faa_client.go 要從 API key 改回 service token + delegated token(待下次 backend agent 處理);mc_token_client.go 要部分復活(保留 ServiceToken cache + IssueDelegatedDownload 邏輯);config.go 加回 ServiceClientID/Secret/TenantID 三欄位;`.env.stage.example` 加回對應 env、撤回 `VISIONA_FAA_API_KEY`。本次純文件修訂、source code 改造留給 backend agent 下次任務。 |
|
||
| 2026-05-16 | 2.1 | **§2 visionA → FAA 整段再次撤回** — 對 MC source 完整驗證後發現:(1) MC source 沒有 `POST /file-access/download-tokens` endpoint(visionA 無法跟 MC 換 delegated token)、(2) MC source 沒有 FAA `IDelegatedDownloadTokenValidator` assume 的 introspection endpoint(即使有 token 也無法 validate)、(3) FAA `GET /files/{key}` 強制只接 delegated token、不接 service token。→ ADR-014 §2 與本 ADR v2.0 §2 描述的「visionA → MC → FAA delegated token 鏈」**完全是 fictional**(從 2026-05-02 寫定起即為 broken design、未曾 e2e 跑通過)。**v2.1 採用 [ADR-016](./adr-016-download-via-converter.md)**(visionA download 改走 converter 新增的 `GET /api/v1/jobs/{id}/result` + visionA stream 中轉)。涉及修訂:狀態行加 v2.1 撤回說明、上位 ADR 區註明 §2 被 ADR-016 supersede、§2 整段標 v2.1 撤回(v2.0 內容保留歷史)、§9 env 表撤回 v2.0 加回的 `VISIONA_OIDC_SERVICE_CLIENT_ID/_SECRET` / `VISIONA_OIDC_TENANT_ID` / `VISIONA_FAA_BASE_URL`、改寫「v2.1 visionA 端 server-to-server 鏈路收斂為單條」。對 commit `86b7175` 影響:visionA backend mc_token_client.go **維持砍除狀態**(撤回 v2.0 「部分復活」規劃)、faa_client.go 改名為 converter_result_client.go(或併入 converter_client.go)、config.go 不需加回 ServiceClient* / TenantID / FAABaseURL、`.env.stage.example` 維持只有 converter env。本次純文件修訂、source code 改造留給 backend agent 下次任務(範圍含「converter 跨 repo 新增 GET result endpoint」)。**§1 visionA → converter API key(v1.0 / v2.0 / v2.1 都維持不變)**。 |
|