mirror of
https://github.com/hanzoai/kms-operator.git
synced 2026-10-30 18:32:57 +00:00
Introduces a kmsapi.Transport interface so the reconciler can talk to
the canonical luxfi/kms surface over either JSON-over-HTTP or the
binary ZAP wire (opcodes 0x0040..0x0043) on the same KMS daemon.
Why: sub-100µs in-cluster secret CRUD for the operator, on the same
authoritative store as the HTTP path. The HTTP transport stays the
default — ZAP is opt-in per KMSSecret/KMSPushSecret via host scheme:
http(s)://… → kmsapi.Client (today's path, unchanged)
zap://host:port → kmszap.Client (new, NodeID-bound)
zap+mdns://_kms._tcp → kmszap.Client (mDNS discovery)
Auth model — important divergence from HTTP, documented in
packages/kmszap/client.go: ZAP authorises by peer NodeID + ACL
(KMS_ZAP_ACL), not by IAM bearer token. Operator deployments wanting
ZAP MUST register their NodeID with the kmsd ACL. clientId/clientSecret
on the CR's universalAuth credentialsRef are ignored on the ZAP path
(the call still works because Login() returns a synthetic
"zap:<nodeID>" token kept by the existing token cache).
Files:
packages/kmsapi/transport.go — Transport interface, IsZAPHost
packages/kmszap/client.go — ZAP-backed kmsapi.Transport impl
packages/util/transport.go — shared BuildTransport factory
controllers/kmssecret + kmspushsecret — use util.BuildTransport
packages/util/{auth,secrets,models}.go — signatures use Transport
109 lines
3.5 KiB
Go
109 lines
3.5 KiB
Go
// Package kmszap tests focus on the pure helpers that do not need a
|
|
// live luxfi/zap node. Round-trip integration tests against a real
|
|
// kmsd ZAP server live in the kms-operator e2e suite.
|
|
package kmszap
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/kms-operator/packages/kmsapi"
|
|
)
|
|
|
|
func TestParsePeerAddr(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
host string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{name: "direct addr", host: "zap://kms.hanzo.svc.cluster.local:9999", want: "kms.hanzo.svc.cluster.local:9999"},
|
|
{name: "ip + port", host: "zap://10.0.0.1:9999", want: "10.0.0.1:9999"},
|
|
{name: "mdns sentinel", host: "zap+mdns://_kms._tcp", want: ""},
|
|
{name: "case-insensitive scheme", host: "ZAP://Kms:9999", want: "Kms:9999"},
|
|
{name: "missing scheme", host: "kms:9999", wantErr: true},
|
|
{name: "empty", host: "", wantErr: true},
|
|
{name: "wrong scheme", host: "http://kms:8443", wantErr: true},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
got, err := parsePeerAddr(c.host)
|
|
if c.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("want error, got %q", got)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("parsePeerAddr(%q) = err %v, want %q", c.host, err, c.want)
|
|
}
|
|
if got != c.want {
|
|
t.Fatalf("parsePeerAddr(%q) = %q, want %q", c.host, got, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSyntheticToken(t *testing.T) {
|
|
c := New(Config{NodeID: "my-op"})
|
|
tok := c.syntheticToken()
|
|
if tok != "zap:my-op" {
|
|
t.Fatalf("syntheticToken = %q, want %q", tok, "zap:my-op")
|
|
}
|
|
anon := New(Config{}).syntheticToken()
|
|
if anon != "zap:anonymous" {
|
|
t.Fatalf("anon = %q, want %q", anon, "zap:anonymous")
|
|
}
|
|
}
|
|
|
|
func TestValidateInputsRejectsBadScope(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
org string
|
|
env string
|
|
path string
|
|
key string
|
|
want string
|
|
}{
|
|
{name: "empty org", env: "prod", key: "k", want: "org is required"},
|
|
{name: "empty env", org: "hanzo", key: "k", want: "env is required"},
|
|
{name: "empty name", org: "hanzo", env: "prod", want: "name is required"},
|
|
{name: "long org", org: strings.Repeat("a", kmsapi.MaxSlugLen+1), env: "prod", key: "k", want: "org > "},
|
|
{name: "long env", org: "hanzo", env: strings.Repeat("a", kmsapi.MaxSlugLen+1), key: "k", want: "env > "},
|
|
{name: "long path", org: "hanzo", env: "prod", path: strings.Repeat("a", kmsapi.MaxPathLen+1), key: "k", want: "path > "},
|
|
{name: "long key", org: "hanzo", env: "prod", key: strings.Repeat("a", kmsapi.MaxKeyLen+1), want: "name > "},
|
|
{name: "nul in org", org: "hanzo\x00", env: "prod", key: "k", want: "control bytes"},
|
|
{name: "nul in env", org: "hanzo", env: "prod\x00", key: "k", want: "control bytes"},
|
|
{name: "nul in path", org: "hanzo", env: "prod", path: "x\x00", key: "k", want: "control bytes"},
|
|
{name: "nul in name", org: "hanzo", env: "prod", key: "k\x00", want: "control bytes"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
err := validateInputs(c.org, c.env, c.path, c.key)
|
|
if err == nil {
|
|
t.Fatalf("want error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), c.want) {
|
|
t.Fatalf("err %q does not contain %q", err.Error(), c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInvalidateTokenIsNoOp(t *testing.T) {
|
|
c := New(Config{NodeID: "ut-node"})
|
|
// Should not panic, should not change anything observable.
|
|
c.InvalidateToken("any-host", "id", "secret")
|
|
c.InvalidateToken("", "", "")
|
|
}
|
|
|
|
func TestInterfaceCompliance(t *testing.T) {
|
|
var _ kmsapi.Transport = (*Client)(nil)
|
|
}
|
|
|
|
func TestCloseIsIdempotent(t *testing.T) {
|
|
c := New(Config{})
|
|
c.Close()
|
|
c.Close() // second call should not panic
|
|
}
|