從 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>
105 lines
3.9 KiB
Go
105 lines
3.9 KiB
Go
// proxy_store.go — ProxyClientStore 實作 Store interface(api-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 到 ProxyClient(HTTP 呼叫 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 // 用於建立 RemoteHandle(OpenStream 時使用)
|
||
}
|
||
|
||
// 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)
|