jim800121chen c63886a194 feat(visionA-backend): Phase 0.9 模型庫存取 — FAA delegated download token(B1+B2)
對齊 ADR-017 v1.2:模型庫下載走 visionA 簽 MC delegated token → Client 直連 FAA。

B2 — MC download token client(internal/fileaccess):
- DownloadTokenIssuer: GetServiceToken(打 MC /oauth/token,client_credentials +
  scope files:download.delegate,含 token cache)+ IssueDownloadToken(打 MC Issue 簽 fdt_)
- secret / service token / fdt token 三層全程用 hashShort 遮罩不 log
- FileAccessConfig + VISIONA_FILE_ACCESS_* env + main.go wire(Enabled() 才接)

B1 — object_key 斷層:
- model.Model 加 FAAObjectKey(json:"-" 不揭露前端)
- PromoteToModels 寫入(用 promote response TargetObjectKey = models/{userID}/{jobID}.nef)
- 三方對映天然一致(visionA Issue / FAA path / MC validate)
- 第一階段框死只 Source=converted 類 model,上傳類 download 回 501

download endpoint:
- GET /api/models/:id/download(owner-only)→ {download_url, token, expires_at}
- 前端帶 Authorization: Bearer 直連 FAA(不經 visionA、不經 AWS)
- 401/403/404/501/502 分明,502 對外 mask 不洩漏 MC 內部狀態

測試: 13 + 8 unit test(mock MC + fake issuer,httptest 驗真 HTTP);go build/vet/test 全綠。
Reviewer: 0 Critical / 0 Major / 3 Minor / 4 Suggestion,通過。

技術債(正式上線前): 第一階段 PoC 共用 FAA service client,MC 規範禁止 client 混用
usage、secret 不共用,須 MC 配發 visionA 專屬 usage=file_api client。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 04:06:09 +08:00

206 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package api 實作 api-server 的 REST + WebSocket 入口。
//
// 對齊 `.autoflow/04-architecture/api/api-spec.md`。
//
// **B4 範圍**Router / Middleware / 結構化錯誤回應骨架 + 少數 handler/healthz、
// /api/system/health、/api/system/info、/api/pairing/token、/api/pairing/status
//
// **B5 範圍**(本檔):
// - Authlogin / logout / mestubregister → 501
// - Pairinglist tokens / revoke token
// - Deviceslist / get讀雲端 repo + 合併 tunnel 狀態scan / connect /
// disconnect / flash / inference.start/stop 走 proxyunpair 軟刪
// - Modelslist / get / init upload / finalize / delete
// - System/system/deps走 proxy
// - ClustersGET /clusters 回空陣列;其他 stub
// - Storage/storage/* 的 LocalFS 假 presigned URL 代理GET/PUT
// - WebSocket保留 501 stub詳見 stubs.goB7 TODO
package api
import (
"log/slog"
"github.com/gin-gonic/gin"
"visiona-backend/internal/auth"
"visiona-backend/internal/conversion"
"visiona-backend/internal/converter"
"visiona-backend/internal/device"
"visiona-backend/internal/fileaccess"
"visiona-backend/internal/model"
"visiona-backend/internal/oidc"
"visiona-backend/internal/session"
"visiona-backend/internal/storage"
"visiona-backend/internal/usersession"
)
// Deps 匯整 api router 所需的所有依賴;由 cmd/api-server/main.go 在啟動時注入。
//
// 之所以集中在一個 struct是為了
// 1. 讓 NewRouter 簽章穩定(之後加新依賴只改 struct不破壞既有 caller
// 2. integration test 容易組裝(只需建一個 Deps 物件)
// 3. 各 handler 透過 closure 取得依賴,不需要 global state
//
// OB52026-04-26起 OIDC 是唯一認證路徑OIDCProvider + SessionManager 必填,
// validate() 在 NewRouter 啟動時就會 panic 提前暴露 misconfiguration。
type Deps struct {
Logger *slog.Logger
// PairingStore 管理 Pairing Token 生命週期。為 nil 時 /api/pairing/* 會回 501。
PairingStore auth.PairingStore
// ─── OIDCOB5 起為必填) ───
// OIDCProvider 封裝 OIDC clientauthorization URL 組裝、token exchange、id_token 驗證)。
OIDCProvider oidc.Provider
// SessionManager 管理 cookie sessionStartSession / GetSession / EndSession
SessionManager *usersession.Manager
// OIDCPostLoginURL 是 callback 完成後 302 回 frontend 的 base URL。
// 例http://localhost:3000dev/ https://app.visiona.cloudprod
// 為空字串時 callback handler 會 fallback 到 same-origin "/"(不建議生產配置)。
OIDCPostLoginURL string
SessionStore session.Store
Forwarder *session.Forwarder
DeviceRepo device.Repository
ModelRepo model.Repository
Storage storage.Store
Converter converter.Client
// Conversion 是 Phase 0.8 轉檔功能的 Service interface5 個 endpoint 共用)。
// 為 nil 時 /api/conversion/* 5 個 endpoint 全回 501 NOT_IMPLEMENTED
// main.go 在 cfg.Conversion.Enabled() 為 false 時不 wire對齊 api-conversion.md。
//
// 設計選擇:用 conversion.Service interface 而非 concrete type — 方便 unit test 注入 stub。
Conversion conversion.Service
// FileAccessIssuer 是 Phase 0.9「模型庫 model 直連 FAA 下載」的 download token 簽發者
// ADR-017 (a))。為 nil 時 GET /api/models/:id/download 回 501 NOT_IMPLEMENTED
// main.go 在 cfg.FileAccess.Enabled() 為 false 時不 wire
// 用 interface 方便 unit test 注入 fake。
FileAccessIssuer fileaccess.DownloadTokenIssuer
// FAABaseURL 是 File Access Agent 對外 base URL不帶結尾斜線用來組回給 Client
// 的 download_url`{FAABaseURL}/files/{object_key}`)。
// 由 cfg.FileAccess.FAABaseURL 注入FileAccessIssuer 非 nil 時必非空main.go 確保)。
FAABaseURL string
// CORSAllowedOrigins 是允許的瀏覽器 Origin 白名單;空 slice 預設放行
// http://localhost:3000前端 dev server
CORSAllowedOrigins []string
// Phase 0.7 security fix C1 (見 .autoflow/05-implementation/review/phase-0.7-security-audit.md)
// StaticUserID 欄位已移除multi-tenant 環境下 fallback 到固定 user 是 latent multi-user
// 隔離破口OWASP A01 + A04。改 OIDC 後 AuthMiddleware 會擋下未登入請求,
// handler 拿不到 UserContext 一律 500safer than silent fallback
// dev seed / unit test 仍可獨立讀 cfg.Auth.StaticUserID env不再注入 Deps。
// MaxUploadSizeMB 是模型上傳大小上限MB0 代表不限(測試友善)。
// 對齊 feature-model-management.mdPhase 0 預設 100 MB由 config.Model.MaxSizeMB 注入)。
MaxUploadSizeMB int
// SessionTokenStore 保存 Pairing → Session 交換後發出的 Session Token。
// Phase 0 雛形用 in-memory 實作(由 main.go 注入Phase 1 改為 DB-backed。
// 為 nil 時 /api/pairing/exchange 會回 501 NOT_IMPLEMENTED。
SessionTokenStore auth.SessionTokenStore
// RelayPublicURL 是 agent 連 tunnel 用的 WSS URL對外可訪問
// 由 `POST /api/pairing/exchange` 回給 agent若為空會回預設 `wss://relay.visionA.cloud`(雛形 placeholder
// 對齊 build-deploy.md 的 VISIONA_RELAY_PUBLIC_URL 環境變數。
RelayPublicURL string
}
// validate 確認必要欄位都有;在 NewRouter 啟動時呼叫,避免 nil pointer panic 推到 runtime。
//
// 嚴格欄位(缺則 panic — fail fast避免半設定狀態跑進生產
// - OIDCProvider — OB5 起 OIDC 是唯一認證路徑
// - SessionManager — OIDC cookie session 必須
//
// 寬鬆欄位缺有預設Logger / CORSAllowedOrigins
//
// 其他欄位PairingStore / SessionStore 等)若為 nil 不擋 — 個別 handler 會回 501
// 允許「最小骨架」啟動跑 /healthz。
//
// Phase 0.7 security fix C1移除 StaticUserID 預設 "demo-user" 的 fallback。
func (d *Deps) validate() {
if d.Logger == nil {
d.Logger = slog.Default()
}
if len(d.CORSAllowedOrigins) == 0 {
d.CORSAllowedOrigins = []string{"http://localhost:3000"}
}
if d.OIDCProvider == nil {
panic("api.NewRouter: Deps.OIDCProvider is required (OB5: OIDC is the only auth path)")
}
if d.SessionManager == nil {
panic("api.NewRouter: Deps.SessionManager is required (OB5: OIDC cookie session is mandatory)")
}
}
// NewRouter 建立 Gin engine 並註冊所有路由與中介層。
//
// 為何回 *gin.Engine 而非 http.Handlercmd/api-server/main.go 需要 access
// engine.Run也可拿 .Handler() 給標準 http.Server 用,所以這個選擇沒讓
// caller 失去彈性)。
func NewRouter(deps Deps) *gin.Engine {
deps.validate()
// gin 的 ReleaseMode 由 caller 視環境設定cmd/api-server/main.go
// 這裡不主動設,避免測試環境被汙染。
r := gin.New()
// 註冊全域 middleware順序很重要Recovery 第一Logger 接著CORS 之後)
r.Use(RecoveryMiddleware(deps.Logger))
r.Use(RequestIDMiddleware())
r.Use(LoggerMiddleware(deps.Logger))
r.Use(CORSMiddleware(deps.CORSAllowedOrigins))
r.Use(ErrorMiddleware()) // 統一把 c.Errors 轉成 JSON
// /healthz 不需要 auth — K8s liveness/readiness 用
r.GET("/healthz", HealthzHandler())
// /storage/* 不走 AuthMiddleware改用 HMAC 簽章)— 對齊 api-spec.md §10
registerStorageRoutes(r, deps)
// /api/pairing/exchange 刻意不走 AuthMiddleware
// agent 尚未有 session token 時就得用 Pairing Token 換 Session Token
// Pairing Token 本身就是這個 endpoint 的憑證。詳見 security.md §1.2。
registerPairingPublicRoutes(r, deps)
// /ws/* 雛形全部 501B7 補齊 WebSocket proxy
registerWebSocketStubs(r)
// OIDC public routes不走 AuthMiddleware
// - GET /api/auth/login — 起始登入流程user 還沒登入)
// - GET /api/auth/callback — OIDC IdP 302 回來
// 必須註冊在 AuthMiddleware 群組之外,否則使用者沒登入根本進不去。
registerOIDCPublicRoutes(r, deps)
// /api 群組:所有路由都走 OIDC AuthMiddlewarecookie session → UserContext
apiGroup := r.Group("/api")
apiGroup.Use(AuthMiddleware(deps))
// B4 核心
registerSystemRoutes(apiGroup, deps)
registerPairingRoutes(apiGroup, deps)
// B5 新增:實際 handler
registerAuthRoutes(apiGroup, deps)
registerDeviceRoutes(apiGroup, deps)
registerModelRoutes(apiGroup, deps)
registerClusterRoutes(apiGroup, deps)
// Phase 0.8Conversion轉檔— 5 個 endpoint
// 對齊 .autoflow/04-architecture/api/api-conversion.md
registerConversionRoutes(apiGroup, deps)
// Stubs只註冊「還沒有實際 handler」的那些 endpoint
registerStubRoutes(apiGroup, deps)
return r
}