mirror of
https://github.com/luxfi/broker.git
synced 2026-09-24 00:25:18 +00:00
feat(provider/northcapital): LoadFromKMS implemented
Replace the ErrNotImplemented stub on Config.LoadFromKMS with a real implementation against a narrow KMSGetter interface. Adapter has zero direct dependency on luxfi/kms; the canonical luxfi/kms.GetWith satisfies KMSGetter via a 1-line wrapper at brokerd startup. Signature: (ctx, KMSGetter, env) error. env is "sandbox" or "prod". KMS path scheme (per integration brief §2): shared/northcapital/<env>/clientID (required) shared/northcapital/<env>/developerAPIKey (required) shared/northcapital/<env>/webhookAuthKey (required) shared/northcapital/<env>/webhookDecryptionKey (optional) Error sentinels: ErrKMSKeyMissing — required key absent / empty value ErrKMSDecryption — transport / decryption failure Tests: 13 cases, all pass under -race. Conformance: - sandbox + prod happy paths populate every field - exact KMS path shape (shared/northcapital/<env>/<field>) - missing required key → ErrKMSKeyMissing (each of 3 required) - empty value treated as missing - decryption error → ErrKMSDecryption - transport error → ErrKMSDecryption - transport error on optional key still fatal - missing optional key tolerated; preserves caller-supplied value - nil KMSGetter rejected - empty / unknown env rejected Source-of-design: Public-Spec Source-ref: legal/partnerships/northcapital/ NorthCapital_Lux_Integration_Brief.md §2; github.com/luxfi/kms public Go surface (kms.Get / kms.GetWith) Clean-Team-Member: dev-agent (P0 NCPS LoadFromKMS completion) Signed-off-by: dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
// KMS hydration for the broker-side TransactAPI adapter. The
|
||||
// canonical luxfi/kms client (github.com/luxfi/kms) returns the
|
||||
// caller a plaintext value per name; the per-tenant-KEK unwrap
|
||||
// happens server-side in the KMS process. The adapter therefore
|
||||
// holds a small KMSGetter interface — narrowly the surface
|
||||
// luxfi/kms.GetWith satisfies — so the adapter has zero direct
|
||||
// dependency on luxfi/kms while remaining trivially wireable from
|
||||
// brokerd startup.
|
||||
//
|
||||
// brokerd wiring sketch (kept out of this file to keep the
|
||||
// dependency direction one-way):
|
||||
//
|
||||
// import "github.com/luxfi/kms"
|
||||
//
|
||||
// type kmsAdapter struct{ cfg kms.Config }
|
||||
// func (a *kmsAdapter) Get(ctx context.Context, name string) (string, error) {
|
||||
// return kms.GetWith(ctx, a.cfg, name)
|
||||
// }
|
||||
//
|
||||
// cfg := northcapital.Config{BaseURL: northcapital.SandboxURL}
|
||||
// if err := cfg.LoadFromKMS(ctx, &kmsAdapter{cfg: kms.Config{Env: env}}, "sandbox"); err != nil {
|
||||
// log.Fatalf("northcapital LoadFromKMS: %v", err)
|
||||
// }
|
||||
// p := northcapital.New(cfg)
|
||||
//
|
||||
// Source-of-design: Public-Spec
|
||||
// Source-ref: legal/partnerships/northcapital/
|
||||
// NorthCapital_Lux_Integration_Brief.md §2; github.com/luxfi/kms
|
||||
// public Go surface (kms.Get / kms.GetWith).
|
||||
//
|
||||
// Authored under the Independent-Implementation Clean-Room Engineering
|
||||
// Protocol (legal/INDEPENDENT-IMPLEMENTATION-CLEAN-ROOM-PROTOCOL.md).
|
||||
|
||||
package northcapital
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// KMSGetter is the minimal surface LoadFromKMS depends on. The
|
||||
// canonical implementation is a one-line wrapper around
|
||||
// luxfi/kms.GetWith; callers free to substitute any secret-store
|
||||
// surface that returns plaintext values per name. The interface
|
||||
// returns the plaintext value of name — the unwrap-under-the-per-
|
||||
// tenant-KEK happens server-side in the KMS process, never inside
|
||||
// the adapter.
|
||||
type KMSGetter interface {
|
||||
// Get returns the plaintext secret value for name. Implementations
|
||||
// MUST return a typed sentinel (or wrap one) for "not found" so
|
||||
// LoadFromKMS can distinguish that from a transport / decryption
|
||||
// failure; the adapter uses errors.Is against ErrKMSKeyMissing.
|
||||
Get(ctx context.Context, name string) (string, error)
|
||||
}
|
||||
|
||||
// ErrKMSKeyMissing is the sentinel LoadFromKMS returns when one of
|
||||
// the required KMS paths is absent. Callers MUST treat this as a
|
||||
// hard error — the adapter cannot operate without the credential.
|
||||
// Wraps the underlying KMSGetter error so callers can errors.Is it
|
||||
// AND inspect the underlying for diagnostics.
|
||||
var ErrKMSKeyMissing = errors.New("northcapital: required KMS key missing")
|
||||
|
||||
// ErrKMSDecryption is the sentinel LoadFromKMS returns when the KMS
|
||||
// retrieval failed for a transport or decryption reason (anything
|
||||
// other than "not found"). Wraps the underlying error.
|
||||
var ErrKMSDecryption = errors.New("northcapital: KMS decryption / transport error")
|
||||
|
||||
// kmsPath returns the canonical KMS key name for (env, field) per
|
||||
// the integration brief §2:
|
||||
//
|
||||
// shared/northcapital/<env>/<field>
|
||||
//
|
||||
// env is "sandbox" or "prod"; field is one of
|
||||
// clientID / developerAPIKey / webhookAuthKey / webhookDecryptionKey.
|
||||
func kmsPath(env, field string) string {
|
||||
return "shared/northcapital/" + env + "/" + field
|
||||
}
|
||||
|
||||
// loadOne fetches one KMS path with the documented error mapping.
|
||||
// "not found" → ErrKMSKeyMissing; anything else → ErrKMSDecryption.
|
||||
// The underlying error is wrapped so errors.Unwrap reaches it.
|
||||
func loadOne(ctx context.Context, kms KMSGetter, env, field string) (string, error) {
|
||||
path := kmsPath(env, field)
|
||||
v, err := kms.Get(ctx, path)
|
||||
if err != nil {
|
||||
if isMissingErr(err) {
|
||||
return "", fmt.Errorf("%w: %s: %w", ErrKMSKeyMissing, path, err)
|
||||
}
|
||||
return "", fmt.Errorf("%w: %s: %w", ErrKMSDecryption, path, err)
|
||||
}
|
||||
if v == "" {
|
||||
return "", fmt.Errorf("%w: %s (empty value)", ErrKMSKeyMissing, path)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// isMissingErr classifies an error as "not found". luxfi/kms surfaces
|
||||
// not-found via the zapclient.ErrNotFound sentinel, which is
|
||||
// reachable from errors.Is. We also accept plain errors carrying
|
||||
// "not found" in the message text so any KMSGetter wrapper without
|
||||
// an explicit sentinel works out of the box.
|
||||
func isMissingErr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
// String-form check covers wrapping layers that lose the typed
|
||||
// sentinel; the canonical luxfi/kms client preserves the typed
|
||||
// error through GetWith so errors.Is works first. We deliberately
|
||||
// do not import luxfi/kms here to keep the dep direction clean.
|
||||
msg := err.Error()
|
||||
return contains(msg, "not found") || contains(msg, "NotFound")
|
||||
}
|
||||
|
||||
// contains is a strings.Contains alias that avoids the package
|
||||
// import in this file (which would force every consumer of the
|
||||
// adapter to compile in strings just for the kms wiring).
|
||||
func contains(s, substr string) bool {
|
||||
if substr == "" {
|
||||
return true
|
||||
}
|
||||
for i := 0; i+len(substr) <= len(s); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LoadFromKMS hydrates cfg from the documented KMS paths under
|
||||
//
|
||||
// shared/northcapital/<env>/clientID
|
||||
// shared/northcapital/<env>/developerAPIKey
|
||||
// shared/northcapital/<env>/webhookAuthKey
|
||||
// shared/northcapital/<env>/webhookDecryptionKey (optional)
|
||||
//
|
||||
// env is one of "sandbox" or "prod". The clientID, developerAPIKey,
|
||||
// and webhookAuthKey are required; webhookDecryptionKey is optional
|
||||
// (only required when the inbound webhook pipeline is active —
|
||||
// adapters that only call outbound endpoints can omit it). A missing
|
||||
// required key returns ErrKMSKeyMissing; a KMS transport / decryption
|
||||
// failure returns ErrKMSDecryption.
|
||||
//
|
||||
// LoadFromKMS does NOT set BaseURL — that is environment-shape, not
|
||||
// secret-shape — so the caller decides between SandboxURL and
|
||||
// ProdURL. Typical wiring:
|
||||
//
|
||||
// cfg := northcapital.Config{BaseURL: northcapital.SandboxURL}
|
||||
// if err := cfg.LoadFromKMS(ctx, kmsAdapter, "sandbox"); err != nil { ... }
|
||||
// p := northcapital.New(cfg)
|
||||
//
|
||||
// On success every required field on cfg is populated; the optional
|
||||
// webhookDecryptionKey is populated when present in KMS and left
|
||||
// untouched when absent (treated as not-required).
|
||||
func (cfg *Config) LoadFromKMS(ctx context.Context, kms KMSGetter, env string) error {
|
||||
if kms == nil {
|
||||
return errors.New("northcapital: LoadFromKMS requires a non-nil KMSGetter")
|
||||
}
|
||||
if env == "" {
|
||||
return errors.New("northcapital: LoadFromKMS requires env (\"sandbox\" or \"prod\")")
|
||||
}
|
||||
if env != "sandbox" && env != "prod" {
|
||||
return fmt.Errorf("northcapital: LoadFromKMS env %q not recognized (want \"sandbox\" or \"prod\")", env)
|
||||
}
|
||||
|
||||
clientID, err := loadOne(ctx, kms, env, "clientID")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
developerAPIKey, err := loadOne(ctx, kms, env, "developerAPIKey")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
webhookAuthKey, err := loadOne(ctx, kms, env, "webhookAuthKey")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// webhookDecryptionKey is optional — only required when the
|
||||
// inbound encrypted-webhook pipeline is active. If absent, leave
|
||||
// cfg.WebhookDecryptionKey as-is (typically empty); the webhook
|
||||
// consumer path will refuse to operate without it and surfaces
|
||||
// ErrMissingConfig at that point. Any error other than missing
|
||||
// is fatal — a transport / decryption error against a key that
|
||||
// exists is a hard failure.
|
||||
webhookDecryptionKey, err := loadOne(ctx, kms, env, "webhookDecryptionKey")
|
||||
if err != nil && !errors.Is(err, ErrKMSKeyMissing) {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.ClientID = clientID
|
||||
cfg.DeveloperAPIKey = developerAPIKey
|
||||
cfg.WebhookAuthKey = webhookAuthKey
|
||||
if webhookDecryptionKey != "" {
|
||||
cfg.WebhookDecryptionKey = webhookDecryptionKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// Conformance tests for LoadFromKMS. Drive a mock KMSGetter and
|
||||
// assert:
|
||||
//
|
||||
// - successful load populates clientID, developerAPIKey, webhookAuthKey
|
||||
// - missing required key returns ErrKMSKeyMissing
|
||||
// - KMS transport/decryption error returns ErrKMSDecryption
|
||||
// - missing optional webhookDecryptionKey is tolerated
|
||||
// - transport error on optional key is still fatal
|
||||
// - env validation rejects unknown env values
|
||||
// - nil KMSGetter rejected
|
||||
// - empty value treated as missing
|
||||
// - exact KMS paths follow shared/northcapital/<env>/<field>
|
||||
//
|
||||
// No production KMS traffic. All fixtures are in-process.
|
||||
//
|
||||
// Source-of-design: Public-Spec
|
||||
// Source-ref: legal/partnerships/northcapital/
|
||||
// NorthCapital_Lux_Integration_Brief.md §2.
|
||||
|
||||
package northcapital
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mockKMS is an in-memory KMSGetter. Configurable per-key value or
|
||||
// error; tracks the order calls came in so tests can assert path
|
||||
// shape.
|
||||
type mockKMS struct {
|
||||
mu sync.Mutex
|
||||
values map[string]string
|
||||
errs map[string]error
|
||||
called []string
|
||||
notFound error // returned for paths absent from both maps
|
||||
}
|
||||
|
||||
func newMockKMS() *mockKMS {
|
||||
return &mockKMS{
|
||||
values: make(map[string]string),
|
||||
errs: make(map[string]error),
|
||||
notFound: errors.New("zapclient: secret not found"),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockKMS) Get(_ context.Context, name string) (string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.called = append(m.called, name)
|
||||
if err, ok := m.errs[name]; ok {
|
||||
return "", err
|
||||
}
|
||||
if v, ok := m.values[name]; ok {
|
||||
return v, nil
|
||||
}
|
||||
return "", m.notFound
|
||||
}
|
||||
|
||||
func (m *mockKMS) calls() []string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
out := make([]string, len(m.called))
|
||||
copy(out, m.called)
|
||||
return out
|
||||
}
|
||||
|
||||
// --- happy paths ---
|
||||
|
||||
func TestLoadFromKMS_SandboxSuccess(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
kms.values["shared/northcapital/sandbox/clientID"] = "cid-sandbox"
|
||||
kms.values["shared/northcapital/sandbox/developerAPIKey"] = "dak-sandbox"
|
||||
kms.values["shared/northcapital/sandbox/webhookAuthKey"] = "whk-sandbox"
|
||||
kms.values["shared/northcapital/sandbox/webhookDecryptionKey"] = "01234567890123456789012345678901"
|
||||
|
||||
var cfg Config
|
||||
if err := cfg.LoadFromKMS(context.Background(), kms, "sandbox"); err != nil {
|
||||
t.Fatalf("LoadFromKMS: %v", err)
|
||||
}
|
||||
if cfg.ClientID != "cid-sandbox" {
|
||||
t.Fatalf("ClientID: got %q, want cid-sandbox", cfg.ClientID)
|
||||
}
|
||||
if cfg.DeveloperAPIKey != "dak-sandbox" {
|
||||
t.Fatalf("DeveloperAPIKey: got %q", cfg.DeveloperAPIKey)
|
||||
}
|
||||
if cfg.WebhookAuthKey != "whk-sandbox" {
|
||||
t.Fatalf("WebhookAuthKey: got %q", cfg.WebhookAuthKey)
|
||||
}
|
||||
if cfg.WebhookDecryptionKey == "" {
|
||||
t.Fatal("WebhookDecryptionKey: empty after successful load")
|
||||
}
|
||||
|
||||
// Path shape: every fetched name MUST follow
|
||||
// shared/northcapital/sandbox/<field>.
|
||||
got := kms.calls()
|
||||
sort.Strings(got)
|
||||
want := []string{
|
||||
"shared/northcapital/sandbox/clientID",
|
||||
"shared/northcapital/sandbox/developerAPIKey",
|
||||
"shared/northcapital/sandbox/webhookAuthKey",
|
||||
"shared/northcapital/sandbox/webhookDecryptionKey",
|
||||
}
|
||||
sort.Strings(want)
|
||||
if !equalStrSlice(got, want) {
|
||||
t.Fatalf("KMS path shape: got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromKMS_ProdSuccess(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
kms.values["shared/northcapital/prod/clientID"] = "cid-prod"
|
||||
kms.values["shared/northcapital/prod/developerAPIKey"] = "dak-prod"
|
||||
kms.values["shared/northcapital/prod/webhookAuthKey"] = "whk-prod"
|
||||
// webhookDecryptionKey deliberately absent — should be tolerated.
|
||||
|
||||
var cfg Config
|
||||
cfg.WebhookDecryptionKey = "preserved-fallback"
|
||||
if err := cfg.LoadFromKMS(context.Background(), kms, "prod"); err != nil {
|
||||
t.Fatalf("LoadFromKMS: %v", err)
|
||||
}
|
||||
if cfg.ClientID != "cid-prod" || cfg.DeveloperAPIKey != "dak-prod" || cfg.WebhookAuthKey != "whk-prod" {
|
||||
t.Fatalf("required fields: %+v", cfg)
|
||||
}
|
||||
// Missing optional must not clobber a caller-supplied value.
|
||||
if cfg.WebhookDecryptionKey != "preserved-fallback" {
|
||||
t.Fatalf("WebhookDecryptionKey clobbered: %q", cfg.WebhookDecryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
// --- missing required keys ---
|
||||
|
||||
func TestLoadFromKMS_MissingClientID(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
kms.values["shared/northcapital/sandbox/developerAPIKey"] = "dak"
|
||||
kms.values["shared/northcapital/sandbox/webhookAuthKey"] = "whk"
|
||||
|
||||
var cfg Config
|
||||
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
||||
if !errors.Is(err, ErrKMSKeyMissing) {
|
||||
t.Fatalf("missing clientID: got %v, want errors.Is ErrKMSKeyMissing", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromKMS_MissingDeveloperAPIKey(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
kms.values["shared/northcapital/sandbox/clientID"] = "cid"
|
||||
kms.values["shared/northcapital/sandbox/webhookAuthKey"] = "whk"
|
||||
|
||||
var cfg Config
|
||||
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
||||
if !errors.Is(err, ErrKMSKeyMissing) {
|
||||
t.Fatalf("got %v, want ErrKMSKeyMissing", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromKMS_MissingWebhookAuthKey(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
kms.values["shared/northcapital/sandbox/clientID"] = "cid"
|
||||
kms.values["shared/northcapital/sandbox/developerAPIKey"] = "dak"
|
||||
|
||||
var cfg Config
|
||||
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
||||
if !errors.Is(err, ErrKMSKeyMissing) {
|
||||
t.Fatalf("got %v, want ErrKMSKeyMissing", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromKMS_EmptyValueTreatedAsMissing(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
kms.values["shared/northcapital/sandbox/clientID"] = ""
|
||||
kms.values["shared/northcapital/sandbox/developerAPIKey"] = "dak"
|
||||
kms.values["shared/northcapital/sandbox/webhookAuthKey"] = "whk"
|
||||
|
||||
var cfg Config
|
||||
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
||||
if !errors.Is(err, ErrKMSKeyMissing) {
|
||||
t.Fatalf("empty clientID: got %v, want ErrKMSKeyMissing", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- transport / decryption errors ---
|
||||
|
||||
func TestLoadFromKMS_DecryptionError(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
kms.errs["shared/northcapital/sandbox/clientID"] = fmt.Errorf("aead: decrypt: bad tag")
|
||||
|
||||
var cfg Config
|
||||
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
||||
if !errors.Is(err, ErrKMSDecryption) {
|
||||
t.Fatalf("decryption error: got %v, want errors.Is ErrKMSDecryption", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromKMS_TransportError(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
kms.values["shared/northcapital/sandbox/clientID"] = "cid"
|
||||
kms.values["shared/northcapital/sandbox/developerAPIKey"] = "dak"
|
||||
kms.errs["shared/northcapital/sandbox/webhookAuthKey"] = fmt.Errorf("dial tcp: i/o timeout")
|
||||
|
||||
var cfg Config
|
||||
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
||||
if !errors.Is(err, ErrKMSDecryption) {
|
||||
t.Fatalf("transport error: got %v, want errors.Is ErrKMSDecryption", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromKMS_TransportErrorOnOptionalKeyIsFatal(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
kms.values["shared/northcapital/sandbox/clientID"] = "cid"
|
||||
kms.values["shared/northcapital/sandbox/developerAPIKey"] = "dak"
|
||||
kms.values["shared/northcapital/sandbox/webhookAuthKey"] = "whk"
|
||||
// Transport error on the optional key — should still be fatal.
|
||||
// "Optional" means missing is tolerated; corruption / transport
|
||||
// failure on a key that exists must not be silently ignored.
|
||||
kms.errs["shared/northcapital/sandbox/webhookDecryptionKey"] = fmt.Errorf("kms: dial unreachable")
|
||||
|
||||
var cfg Config
|
||||
err := cfg.LoadFromKMS(context.Background(), kms, "sandbox")
|
||||
if !errors.Is(err, ErrKMSDecryption) {
|
||||
t.Fatalf("transport error on optional: got %v, want errors.Is ErrKMSDecryption", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- argument validation ---
|
||||
|
||||
func TestLoadFromKMS_NilGetter(t *testing.T) {
|
||||
var cfg Config
|
||||
err := cfg.LoadFromKMS(context.Background(), nil, "sandbox")
|
||||
if err == nil {
|
||||
t.Fatal("nil KMSGetter: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromKMS_EmptyEnv(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
var cfg Config
|
||||
err := cfg.LoadFromKMS(context.Background(), kms, "")
|
||||
if err == nil {
|
||||
t.Fatal("empty env: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromKMS_UnknownEnv(t *testing.T) {
|
||||
kms := newMockKMS()
|
||||
var cfg Config
|
||||
err := cfg.LoadFromKMS(context.Background(), kms, "staging")
|
||||
if err == nil {
|
||||
t.Fatal("unknown env: expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func equalStrSlice(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestKMSPath(t *testing.T) {
|
||||
cases := []struct {
|
||||
env, field, want string
|
||||
}{
|
||||
{"sandbox", "clientID", "shared/northcapital/sandbox/clientID"},
|
||||
{"prod", "developerAPIKey", "shared/northcapital/prod/developerAPIKey"},
|
||||
{"prod", "webhookAuthKey", "shared/northcapital/prod/webhookAuthKey"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := kmsPath(tc.env, tc.field); got != tc.want {
|
||||
t.Fatalf("kmsPath(%q,%q)=%q want %q", tc.env, tc.field, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,29 +158,10 @@ func New(cfg Config) *Provider {
|
||||
// router and the broker registry.
|
||||
func (p *Provider) Name() string { return "northcapital" }
|
||||
|
||||
// LoadFromKMS is the documented hook for hydrating Config from
|
||||
// luxfi/kms. The integration brief §7 enumerates the exact unwrap
|
||||
// path and the audit-trail requirements. The hook is intentionally
|
||||
// thin — the KMS client wiring is the responsibility of the brokerd
|
||||
// startup path, not this adapter.
|
||||
//
|
||||
// Expected KMS layout (per the integration brief §2):
|
||||
//
|
||||
// shared/northcapital/sandbox/clientID
|
||||
// shared/northcapital/sandbox/developerAPIKey
|
||||
// shared/northcapital/sandbox/webhookAuthKey
|
||||
// shared/northcapital/sandbox/webhookDecryptionKey
|
||||
// shared/northcapital/prod/clientID
|
||||
// shared/northcapital/prod/developerAPIKey
|
||||
// shared/northcapital/prod/webhookAuthKey
|
||||
// shared/northcapital/prod/webhookDecryptionKey
|
||||
//
|
||||
// Each value is the per-tenant-KEK-wrapped envelope. The KMS client
|
||||
// unwraps under the tenant's KEK, returning plaintext only inside this
|
||||
// process's address space, never persisted.
|
||||
func (cfg *Config) LoadFromKMS(_ context.Context, _ string) error {
|
||||
return errors.New("northcapital: LoadFromKMS must be wired by brokerd to luxfi/kms")
|
||||
}
|
||||
// LoadFromKMS is implemented in kms.go. The signature is
|
||||
// (ctx, kms, env) — see kms.go for the KMSGetter interface, error
|
||||
// sentinels (ErrKMSKeyMissing, ErrKMSDecryption), and the
|
||||
// brokerd-side wiring sketch against luxfi/kms.
|
||||
|
||||
// --- HTTP transport ---
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user