jim800121chen 22f0837ba8 feat(visionA-backend): Phase 0 → 0.7 雲端後端(雙 binary + OIDC BFF + stage 部署)
從 edge-ai-platform POC 轉為正式產品的雲端後端,含以下整合階段:

- Phase 0:雛形骨架 — `cmd/api-server` (REST :3721) + `cmd/remote-proxy`
  (tunnel :3800 / internal :3801) 雙 binary 共用 internal/,沿用 POC 的
  WebSocket+yamux tunnel 協定但解耦 relay 與 API
- Phase 0.6:OIDC BFF 接 Innovedus Member Center
  - internal/oidc package(coreos/go-oidc + PKCE S256 + state + nonce)
  - internal/usersession package(HMAC-SHA256 cookie + RotateSessionID
    防 session fixation, OWASP ASVS V3.2.1)
  - 4 個 OIDC handler(/api/auth/login|callback|me|logout)+ AuthMiddleware
  - 完全拔除 StaticAuthProvider,OIDC 是唯一認證路徑
  - 9 個 ADR(含 ADR-010 BFF / ADR-011 取代 static auth /
    ADR-012 pending session shared cookie / ADR-013 PKCE-only public client)
- Phase 0.7:A1 改造 + security audit 修復
  - OIDC ClientSecret 變選填,支援 stage MC 的 public PKCE-only client
    (AuthStyleInParams 強制 token endpoint 不送 client_secret)
  - 預留 ServiceClient* 欄位給未來 client_credentials grant
  - 移除 13+ 處 resolveUserID(uc, StaticUserID) fallback 改 strict mode
    (Audit C1:multi-tenant 隔離破口)
  - Pairing exchange MarkUsed 失敗 abort + revoke session token(Audit M3)
  - 新增 all_endpoints_require_auth_test 整合測試(51 endpoint × 401)

驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:21:20 +08:00

105 lines
3.9 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.

// proxy_store.go — ProxyClientStore 實作 Store interfaceapi-server 端使用)。
//
// 雛形雙 binary 架構下:
// - remote-proxy 持有 InMemoryStore是唯一的 session state 來源
// - api-server 持有 ProxyClientStore內部透過 ProxyClient 走 internal HTTP 查 remote-proxy
//
// 因為 api-server 是無狀態所以「寫入類」操作Register / Heartbeat / CleanupExpired
// 對 ProxyClientStore 都不適用 — 全部回 ErrNotSupported。
package session
import (
"context"
"errors"
"time"
)
// ProxyClientStore 是 Store 的 HTTP-client 實作,部署在 api-server 端。
//
// 它把所有讀取類操作 delegate 到 ProxyClientHTTP 呼叫 remote-proxy
// 寫入類操作Register / Unregister / Heartbeat / CleanupExpired一律回
// ErrNotSupported — 因為 session lifecycle 由 remote-proxy 唯一管理。
//
// Lookup 回傳的 Handle 是 RemoteHandle見下方它的 OpenStream 會走
// `forwarder.go` 的 raw forward 流程。
type ProxyClientStore struct {
client ProxyClient
forwarder *Forwarder // 用於建立 RemoteHandleOpenStream 時使用)
}
// NewProxyClientStore 建立一個 api-server 端的 SessionStore。
//
// 入參:
// - client用於 metadata 操作GetSession / ListSessions / CloseSession
// - forwarder用於 RemoteHandle.OpenStream 走 raw forward
//
// forwarder 可為 nil不需要 OpenStream只要 metadata 查詢時);但實務上
// api-server 必定需要轉發,所以呼叫方應同時注入兩者。
func NewProxyClientStore(client ProxyClient, forwarder *Forwarder) *ProxyClientStore {
return &ProxyClientStore{client: client, forwarder: forwarder}
}
// Register — ProxyClientStore 不支援session 註冊由 remote-proxy 在 tunnel
// upgrade 時完成。
func (s *ProxyClientStore) Register(ctx context.Context, token string, h Handle) error {
return ErrNotSupported
}
// Unregister — 在 api-server 端等同於「強制關閉 session」實際走
// CloseSession HTTP endpoint不存在時為 no-op對齊 InMemoryStore 行為)。
func (s *ProxyClientStore) Unregister(ctx context.Context, token string) error {
if err := s.client.CloseSession(ctx, token); err != nil {
// 不存在當作 no-op與 InMemoryStore 一致
if errors.Is(err, ErrSessionNotFound) {
return nil
}
return err
}
return nil
}
// Lookup 對應 ProxyClient.GetSession回傳 RemoteHandle。
//
// RemoteHandle 不持有 yamux session它在 remote-proxy 那邊);
// 它的 OpenStream 會透過 Forwarder 走 raw forward。
func (s *ProxyClientStore) Lookup(ctx context.Context, token string) (Handle, error) {
sum, err := s.client.GetSession(ctx, token)
if err != nil {
return nil, err
}
return newRemoteHandle(s.client, s.forwarder, sum), nil
}
// Exists 透過 GetSession 判斷;不存在回 (false, nil),其他錯誤回 (false, err)。
func (s *ProxyClientStore) Exists(ctx context.Context, token string) (bool, error) {
_, err := s.client.GetSession(ctx, token)
if err != nil {
if errors.Is(err, ErrSessionNotFound) {
return false, nil
}
return false, err
}
return true, nil
}
// List 對應 ProxyClient.ListSessions。
func (s *ProxyClientStore) List(ctx context.Context) ([]*Summary, error) {
return s.client.ListSessions(ctx)
}
// Heartbeat — ProxyClientStore 不支援;心跳由 yamux 的 keep-alive 自動維持,
// 並由 remote-proxy 在實體 tunnel 上更新 LastHeartbeat。
func (s *ProxyClientStore) Heartbeat(ctx context.Context, token string) error {
return ErrNotSupported
}
// CleanupExpired — ProxyClientStore 不支援;清理由 remote-proxy 的
// background goroutine 執行。
func (s *ProxyClientStore) CleanupExpired(ctx context.Context, expireAfter time.Duration) (int, error) {
return 0, ErrNotSupported
}
// 編譯時檢查:確保 ProxyClientStore 實作 Store。
var _ Store = (*ProxyClientStore)(nil)