iop/apps/control-plane/internal/credentiallease/service_test.go
toki 4c8441e6c9 feat(credential): Provider Credential Slot 라우팅을 구현한다
사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
2026-08-02 09:10:11 +09:00

187 lines
7.3 KiB
Go

package credentiallease
import (
"context"
"crypto/ecdh"
"crypto/ed25519"
"crypto/rand"
"google.golang.org/protobuf/proto"
"path/filepath"
"testing"
"time"
"iop/apps/control-plane/internal/credentialseal"
"iop/apps/control-plane/internal/credentialstore"
lease "iop/packages/go/credentiallease"
iop "iop/proto/gen/iop"
)
type acceptingRegistry struct{}
func (acceptingRegistry) HasEnvelopeKey(context.Context, string, uint64) (bool, error) {
return true, nil
}
type recordingOpener struct {
calls int
secret []byte
}
func (o *recordingOpener) Open(_ context.Context, _ credentialstore.SecretEnvelope, _ credentialseal.Context) ([]byte, error) {
o.calls++
return append([]byte(nil), o.secret...), nil
}
func TestAcquireChecksDurableBindingBeforeOpeningAndIssuesConsumableLease(t *testing.T) {
ctx := context.Background()
store, err := credentialstore.Open(ctx, filepath.Join(t.TempDir(), "credential.db"), credentialstore.WithEnvelopeKeyRegistry(acceptingRegistry{}))
if err != nil {
t.Fatal(err)
}
defer store.Close()
principal, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: "principal"})
if err != nil {
t.Fatal(err)
}
slot, err := store.CreateSlot(ctx, credentialstore.CreateSlotInput{
PrincipalID: principal.Principal.ID, Vendor: "openai", CredentialKind: credentialstore.CredentialKindBearer,
Envelope: credentialstore.SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "at-rest", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")},
})
if err != nil {
t.Fatal(err)
}
route, err := store.CreateRoute(ctx, credentialstore.CreateRouteInput{PrincipalID: principal.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o", ResourceSelector: "default"})
if err != nil {
t.Fatal(err)
}
slot, err = store.GetSlot(ctx, principal.Principal.ID, slot.ID)
if err != nil {
t.Fatal(err)
}
generation, err := store.ProjectionGeneration(ctx)
if err != nil {
t.Fatal(err)
}
issuerPublic, issuerPrivate, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
recipient, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
now := time.Unix(1700000000, 0).UTC()
opener := &recordingOpener{secret: []byte("provider-secret-sentinel")}
service, err := New(store, opener, "issuer-1", issuerPrivate, 30*time.Second, 8, func() time.Time { return now }, rand.Reader)
if err != nil {
t.Fatal(err)
}
binding := &iop.CredentialLeaseBinding{
PrincipalRef: principal.Principal.ID, CredentialSlotRef: slot.ID, RouteId: route.ID, ProfileId: route.ProfileID,
UpstreamTarget: route.UpstreamModel, NodeId: "node-1", RecipientKeyId: "recipient-1",
CredentialRevision: uint64(slot.Revision), RouteRevision: uint64(route.Revision), ProjectionGeneration: generation,
}
request := &iop.AcquireLeaseRequest{EdgeId: "edge-1", Binding: binding, RecipientPublicKey: recipient.PublicKey().Bytes()}
stale := proto.Clone(request).(*iop.AcquireLeaseRequest)
stale.Binding.ProjectionGeneration--
response, err := service.Acquire(ctx, stale)
if err != nil || response.GetError() == "" || opener.calls != 0 {
t.Fatalf("stale acquire response=%v err=%v opener_calls=%d", response, err, opener.calls)
}
response, err = service.Acquire(ctx, request)
if err != nil || response.GetLease() == nil || response.GetError() != "" || opener.calls != 1 {
t.Fatalf("valid acquire response=%v err=%v opener_calls=%d", response, err, opener.calls)
}
consumer, err := lease.NewConsumer("node-1", "recipient-1", recipient.Bytes(), "issuer-1", issuerPublic, 8, func() time.Time { return now })
if err != nil {
t.Fatal(err)
}
envelope, err := lease.FromProto(response.GetLease())
if err != nil {
t.Fatal(err)
}
material, err := consumer.Consume(ctx, envelope, lease.ExpectedFromProto(binding))
if err != nil {
t.Fatal(err)
}
if string(material.Secret) != "provider-secret-sentinel" || material.HeaderName != "Authorization" || material.Scheme != "Bearer" {
t.Fatal("unexpected lease material")
}
material.Zero()
}
// TestAcquireCanonicalizesAPIKeyProfileHeader proves that a built-in profile
// declaring a lowercase API-key header (seulgi_messages -> "x-api-key") is
// issued and consumed as the canonical "X-Api-Key" scope with no scheme, so
// the managed Messages route no longer fails closed at lease issuance.
func TestAcquireCanonicalizesAPIKeyProfileHeader(t *testing.T) {
ctx := context.Background()
store, err := credentialstore.Open(ctx, filepath.Join(t.TempDir(), "credential.db"), credentialstore.WithEnvelopeKeyRegistry(acceptingRegistry{}))
if err != nil {
t.Fatal(err)
}
defer store.Close()
principal, err := store.CreatePrincipalWithToken(ctx, credentialstore.CreatePrincipalInput{Alias: "principal"})
if err != nil {
t.Fatal(err)
}
slot, err := store.CreateSlot(ctx, credentialstore.CreateSlotInput{
PrincipalID: principal.Principal.ID, Vendor: "seulgi", CredentialKind: credentialstore.CredentialKindAPIKey,
Envelope: credentialstore.SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "at-rest", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")},
})
if err != nil {
t.Fatal(err)
}
route, err := store.CreateRoute(ctx, credentialstore.CreateRouteInput{PrincipalID: principal.Principal.ID, SlotID: slot.ID, ProfileID: "seulgi_messages", UpstreamModel: "seulgi-messages-1", ResourceSelector: "default"})
if err != nil {
t.Fatal(err)
}
slot, err = store.GetSlot(ctx, principal.Principal.ID, slot.ID)
if err != nil {
t.Fatal(err)
}
generation, err := store.ProjectionGeneration(ctx)
if err != nil {
t.Fatal(err)
}
issuerPublic, issuerPrivate, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
recipient, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
now := time.Unix(1700000000, 0).UTC()
opener := &recordingOpener{secret: []byte("api-key-secret-sentinel")}
service, err := New(store, opener, "issuer-1", issuerPrivate, 30*time.Second, 8, func() time.Time { return now }, rand.Reader)
if err != nil {
t.Fatal(err)
}
binding := &iop.CredentialLeaseBinding{
PrincipalRef: principal.Principal.ID, CredentialSlotRef: slot.ID, RouteId: route.ID, ProfileId: route.ProfileID,
UpstreamTarget: route.UpstreamModel, NodeId: "node-1", RecipientKeyId: "recipient-1",
CredentialRevision: uint64(slot.Revision), RouteRevision: uint64(route.Revision), ProjectionGeneration: generation,
}
request := &iop.AcquireLeaseRequest{EdgeId: "edge-1", Binding: binding, RecipientPublicKey: recipient.PublicKey().Bytes()}
response, err := service.Acquire(ctx, request)
if err != nil || response.GetLease() == nil || response.GetError() != "" || opener.calls != 1 {
t.Fatalf("api-key acquire response=%v err=%v opener_calls=%d", response, err, opener.calls)
}
consumer, err := lease.NewConsumer("node-1", "recipient-1", recipient.Bytes(), "issuer-1", issuerPublic, 8, func() time.Time { return now })
if err != nil {
t.Fatal(err)
}
envelope, err := lease.FromProto(response.GetLease())
if err != nil {
t.Fatal(err)
}
material, err := consumer.Consume(ctx, envelope, lease.ExpectedFromProto(binding))
if err != nil {
t.Fatal(err)
}
if string(material.Secret) != "api-key-secret-sentinel" || material.HeaderName != "X-Api-Key" || material.Scheme != "" {
t.Fatalf("unexpected api-key lease material header=%q scheme=%q", material.HeaderName, material.Scheme)
}
material.Zero()
}