從 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>
175 lines
4.5 KiB
Go
175 lines
4.5 KiB
Go
package storage
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newTestStore(t *testing.T) *LocalFSStore {
|
|
t.Helper()
|
|
s, err := NewLocalFSStore(t.TempDir(), "http://localhost:3001/storage", "test-secret")
|
|
require.NoError(t, err)
|
|
return s
|
|
}
|
|
|
|
func TestLocalFSStore_PutGetStat(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
payload := []byte("hello-visiona")
|
|
key := "models/user-1/m1.nef"
|
|
|
|
require.NoError(t, s.Put(ctx, key, bytes.NewReader(payload), int64(len(payload)), nil))
|
|
|
|
// Stat
|
|
info, err := s.Stat(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(len(payload)), info.Size)
|
|
assert.Equal(t, key, info.Key)
|
|
|
|
// Get
|
|
rc, obj, err := s.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
defer rc.Close()
|
|
got, err := io.ReadAll(rc)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, payload, got)
|
|
assert.Equal(t, int64(len(payload)), obj.Size)
|
|
}
|
|
|
|
func TestLocalFSStore_Exists(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
ok, err := s.Exists(ctx, "nope.txt")
|
|
require.NoError(t, err)
|
|
assert.False(t, ok, "不存在應回 (false, nil)")
|
|
|
|
require.NoError(t, s.Put(ctx, "a.txt", strings.NewReader("x"), 1, nil))
|
|
|
|
ok, err = s.Exists(ctx, "a.txt")
|
|
require.NoError(t, err)
|
|
assert.True(t, ok)
|
|
}
|
|
|
|
func TestLocalFSStore_Get_NotFound(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_, _, err := s.Get(context.Background(), "missing.txt")
|
|
assert.ErrorIs(t, err, ErrNotFound)
|
|
}
|
|
|
|
func TestLocalFSStore_Stat_NotFound(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_, err := s.Stat(context.Background(), "missing.txt")
|
|
assert.ErrorIs(t, err, ErrNotFound)
|
|
}
|
|
|
|
func TestLocalFSStore_Delete(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
require.NoError(t, s.Put(ctx, "tmp.txt", strings.NewReader("x"), 1, nil))
|
|
require.NoError(t, s.Delete(ctx, "tmp.txt"))
|
|
|
|
ok, _ := s.Exists(ctx, "tmp.txt")
|
|
assert.False(t, ok)
|
|
|
|
// 刪除不存在的 key 不應回錯
|
|
assert.NoError(t, s.Delete(ctx, "never.txt"))
|
|
}
|
|
|
|
func TestLocalFSStore_List(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
require.NoError(t, s.Put(ctx, "models/u1/a.nef", strings.NewReader("A"), 1, nil))
|
|
require.NoError(t, s.Put(ctx, "models/u1/b.nef", strings.NewReader("B"), 1, nil))
|
|
require.NoError(t, s.Put(ctx, "models/u2/c.nef", strings.NewReader("C"), 1, nil))
|
|
|
|
listU1, err := s.List(ctx, "models/u1")
|
|
require.NoError(t, err)
|
|
assert.Len(t, listU1, 2)
|
|
|
|
listAll, err := s.List(ctx, "")
|
|
require.NoError(t, err)
|
|
assert.Len(t, listAll, 3)
|
|
|
|
listEmpty, err := s.List(ctx, "not-exist-prefix")
|
|
require.NoError(t, err)
|
|
assert.Empty(t, listEmpty)
|
|
}
|
|
|
|
func TestLocalFSStore_PathTraversal_Rejected(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
// 嘗試逃出 root
|
|
err := s.Put(ctx, "../../etc/passwd", strings.NewReader("pwned"), 5, nil)
|
|
assert.ErrorIs(t, err, ErrInvalidKey)
|
|
|
|
_, _, err = s.Get(ctx, "../secret.txt")
|
|
assert.ErrorIs(t, err, ErrInvalidKey)
|
|
|
|
_, err = s.Stat(ctx, "../secret.txt")
|
|
assert.ErrorIs(t, err, ErrInvalidKey)
|
|
|
|
// 空 key
|
|
err = s.Put(ctx, "", strings.NewReader("x"), 1, nil)
|
|
assert.ErrorIs(t, err, ErrInvalidKey)
|
|
}
|
|
|
|
func TestLocalFSStore_PresignedGetURL_AndVerify(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
key := "models/u1/m.nef"
|
|
require.NoError(t, s.Put(ctx, key, strings.NewReader("X"), 1, nil))
|
|
|
|
u, err := s.PresignedGetURL(ctx, key, 5*time.Minute)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, u, "http://localhost:3001/storage/")
|
|
assert.Contains(t, u, "expires=")
|
|
assert.Contains(t, u, "signature=")
|
|
|
|
// 解析並驗證
|
|
parsed, err := url.Parse(u)
|
|
require.NoError(t, err)
|
|
expires, err := strconv.ParseInt(parsed.Query().Get("expires"), 10, 64)
|
|
require.NoError(t, err)
|
|
sig := parsed.Query().Get("signature")
|
|
|
|
assert.NoError(t, s.VerifySignature("GET", key, expires, sig))
|
|
|
|
// 簽名錯誤
|
|
assert.ErrorIs(t, s.VerifySignature("GET", key, expires, "tampered"), ErrInvalidSignature)
|
|
|
|
// 已過期
|
|
assert.ErrorIs(t,
|
|
s.VerifySignature("GET", key, time.Now().Add(-1*time.Hour).Unix(), sig),
|
|
ErrInvalidSignature)
|
|
|
|
// method 不符
|
|
assert.ErrorIs(t, s.VerifySignature("PUT", key, expires, sig), ErrInvalidSignature)
|
|
}
|
|
|
|
func TestLocalFSStore_PresignedPutURL(t *testing.T) {
|
|
s := newTestStore(t)
|
|
u, err := s.PresignedPutURL(context.Background(), "models/u1/new.nef", 10*time.Minute)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, u, "mode=put")
|
|
}
|
|
|
|
func TestNewLocalFSStore_EmptyRoot(t *testing.T) {
|
|
_, err := NewLocalFSStore("", "", "")
|
|
assert.Error(t, err)
|
|
}
|