사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
219 lines
6.8 KiB
Go
219 lines
6.8 KiB
Go
package credentialstore
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
func TestProjectionGenerationCommitsWithMutation(t *testing.T) {
|
|
ctx := context.Background()
|
|
registry := newFakeKeyRegistry()
|
|
registry.RegisterKey("k1", 1)
|
|
store, err := Open(ctx, filepath.Join(t.TempDir(), "projection-generation.db"), WithEnvelopeKeyRegistry(registry))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
|
|
assertGeneration := func(want uint64) {
|
|
t.Helper()
|
|
got, err := store.ProjectionGeneration(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("generation: got %d want %d", got, want)
|
|
}
|
|
}
|
|
assertGeneration(0)
|
|
|
|
issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "projection-principal"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertGeneration(1)
|
|
if _, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "projection-principal"}); !errors.Is(err, ErrPrincipalAlreadyExists) {
|
|
t.Fatalf("duplicate create: got %v", err)
|
|
}
|
|
assertGeneration(1)
|
|
|
|
slot, err := store.CreateSlot(ctx, CreateSlotInput{
|
|
PrincipalID: issued.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer,
|
|
Alias: "primary", Envelope: SecretEnvelope{
|
|
Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1,
|
|
Nonce: []byte("123456789012"), Ciphertext: []byte("opaque-ciphertext"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertGeneration(2)
|
|
|
|
route, err := store.CreateRoute(ctx, CreateRouteInput{
|
|
PrincipalID: issued.Principal.ID, SlotID: slot.ID, Alias: "model-primary",
|
|
ProfileID: "openai", UpstreamModel: "upstream-model",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertGeneration(3)
|
|
|
|
if _, err := store.DisableRoute(ctx, issued.Principal.ID, route.ID, route.Revision); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertGeneration(4)
|
|
currentSlot, err := store.GetSlot(ctx, issued.Principal.ID, slot.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rotatedSlot, err := store.RotateSlotSecret(ctx, RotateSlotSecretInput{
|
|
PrincipalID: issued.Principal.ID, SlotID: slot.ID, CurrentRevision: currentSlot.Revision,
|
|
Envelope: SecretEnvelope{
|
|
Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1,
|
|
Nonce: []byte("abcdefghijkl"), Ciphertext: []byte("rotated-opaque-ciphertext"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertGeneration(5)
|
|
if _, err := store.DisableSlot(ctx, issued.Principal.ID, slot.ID, rotatedSlot.Revision); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertGeneration(6)
|
|
if _, err := store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, issued.Token.Revision); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertGeneration(7)
|
|
}
|
|
|
|
func TestProjectionGenerationPersistsAcrossReopen(t *testing.T) {
|
|
ctx := context.Background()
|
|
dbPath := filepath.Join(t.TempDir(), "projection-reopen.db")
|
|
store, err := Open(ctx, dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "persistent-projection"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want, err := store.ProjectionGeneration(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reopened, err := Open(ctx, dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer reopened.Close()
|
|
got, err := reopened.ProjectionGeneration(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("persisted generation: got %d want %d", got, want)
|
|
}
|
|
if _, err := reopened.RevokeToken(ctx, issued.Principal.ID, issued.Token.TokenRef, issued.Token.Revision); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, err := reopened.ProjectionGeneration(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if after != want+1 {
|
|
t.Fatalf("generation after reopen mutation: got %d want %d", after, want+1)
|
|
}
|
|
}
|
|
|
|
func TestProjectionSnapshotOmitsInactiveRecords(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, issued, slot, _ := setupRouteTestFixture(t)
|
|
route, err := store.CreateRoute(ctx, CreateRouteInput{
|
|
PrincipalID: issued.Principal.ID,
|
|
SlotID: slot.ID, Alias: "projected-route", ProfileID: "openai", UpstreamModel: "upstream-model",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
clock := func() time.Time { return time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC) }
|
|
|
|
snapshot, err := store.BuildPrincipalProjection(ctx, ProjectionBuildOptions{TTL: time.Minute, Clock: clock})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(snapshot.GetTokens()) != 1 || len(snapshot.GetRoutes()) != 1 {
|
|
t.Fatalf("active snapshot: tokens=%d routes=%d", len(snapshot.GetTokens()), len(snapshot.GetRoutes()))
|
|
}
|
|
if snapshot.GetTokens()[0].GetTokenDigestSha256() != issued.Token.Digest {
|
|
t.Fatal("snapshot did not contain the active token digest")
|
|
}
|
|
projectedRoute := snapshot.GetRoutes()[0]
|
|
if projectedRoute.GetCredentialSlotRef() != slot.ID || projectedRoute.GetRouteId() != route.ID {
|
|
t.Fatalf("route projection mismatch: %+v", projectedRoute)
|
|
}
|
|
encoded, err := proto.Marshal(snapshot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(encoded), "cipher1") || strings.Contains(string(encoded), "AES-256-GCM") || strings.Contains(string(encoded), "k1") {
|
|
t.Fatal("projection contains credential envelope material")
|
|
}
|
|
|
|
if _, err := store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, issued.Token.Revision); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
currentSlot, err := store.GetSlot(ctx, issued.Principal.ID, slot.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.DisableSlot(ctx, issued.Principal.ID, slot.ID, currentSlot.Revision); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inactiveSnapshot, err := store.BuildPrincipalProjection(ctx, ProjectionBuildOptions{TTL: time.Minute, Clock: clock})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(inactiveSnapshot.GetTokens()) != 0 || len(inactiveSnapshot.GetRoutes()) != 0 {
|
|
t.Fatalf("inactive rows leaked: tokens=%d routes=%d", len(inactiveSnapshot.GetTokens()), len(inactiveSnapshot.GetRoutes()))
|
|
}
|
|
if inactiveSnapshot.GetGeneration() <= snapshot.GetGeneration() {
|
|
t.Fatalf("generation did not advance: before=%d after=%d", snapshot.GetGeneration(), inactiveSnapshot.GetGeneration())
|
|
}
|
|
}
|
|
|
|
func TestProjectionSnapshotEnforcesBounds(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newTestStore(t)
|
|
if _, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "bounded-a"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "bounded-b"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := store.BuildPrincipalProjection(ctx, ProjectionBuildOptions{
|
|
TTL: time.Minute,
|
|
Limits: ProjectionLimits{MaxTokens: 1, MaxRoutes: 1, MaxBytes: 4096},
|
|
})
|
|
if !errors.Is(err, ErrProjectionTooLarge) {
|
|
t.Fatalf("token bound: got %v", err)
|
|
}
|
|
|
|
_, err = store.BuildPrincipalProjection(ctx, ProjectionBuildOptions{
|
|
TTL: time.Minute,
|
|
Limits: ProjectionLimits{MaxTokens: 2, MaxRoutes: 1, MaxBytes: 1},
|
|
})
|
|
if !errors.Is(err, ErrProjectionTooLarge) {
|
|
t.Fatalf("byte bound: got %v", err)
|
|
}
|
|
}
|