mirror of
https://github.com/hanzoai/kms.git
synced 2026-10-17 11:18:16 +00:00
* docs(llm): explicit Upstream attribution line
LICENSE-attested or repo-attested upstream is now called out at the
top of LLM.md alongside the project description, so downstream
audits (universe/docs/PRODUCTS.md) can rely on a single canonical
location for the OSS lineage statement.
* feat: cloud Mount() per HIP-0106
Adds pkg/kms.Mount(*zip.App, cloud.Deps) error so the unified Hanzo
Cloud binary can blank-import and register the in-process KMS
server alongside every other Hanzo subsystem.
- pkg/kms/mount.go: native /v1/kms/health + /v1/kms/readyz zip
handlers and zip.AdaptNetHTTP wrap of the existing Embed mux at
/v1/kms/* + /healthz. init() registers with cloud.Register("kms",
10, …) so blank import wires the subsystem in.
- cmd/kmsd: rewritten as a HIP-0106 thin shim — cloud.LoadConfig +
cloud.BuildDeps + zip.New + kms.Mount + app.Listen. The Embed
surface is unchanged; only assembly moves through zip.
- schema/kms.zap: minimal Health ZAP interface; full secret/key
surfaces land in follow-up PRs.
Standalone kmsd binary still ships via the existing Dockerfile and
CI workflow.
* refactor: collapse pkg/kms/ → root for clean import path
Subsystem code lives in package kms at repo root. Importers now use
`github.com/hanzoai/kms` (no /pkg/kms/ nesting). Standalone shim moved
to cmd/kms/ where applicable.
Per Hanzo Go-stdlib pattern: repo IS the package.
* refactor: flatten import path — github.com/hanzoai/cloud (drop /pkg/cloud)
111 lines
3.3 KiB
Go
111 lines
3.3 KiB
Go
package kms
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestEmbed asserts the public Embed() entry point boots a working
|
|
// KMS server (in-memory ZapDB, dev-mode auth, no listener) and the
|
|
// returned HTTPHandler answers /healthz with 200.
|
|
//
|
|
// Runs in <2s with no external services. Mirrors the iam.Embed()
|
|
// shape so a future fused hanzo binary can call Embed() the same way
|
|
// for both services.
|
|
func TestEmbed(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping kms.Embed live test in -short mode")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
// Stand up a fake JWKS so Embed has somewhere to point ExpectedIssuer
|
|
// and JWKSURL. There is no permissive env any more — every Hanzo
|
|
// user supplies these four fields or Embed refuses to boot.
|
|
jwks := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"keys":[]}`))
|
|
}))
|
|
t.Cleanup(jwks.Close)
|
|
|
|
em, err := Embed(ctx, EmbedConfig{
|
|
DataDir: filepath.Join(t.TempDir(), "kms"),
|
|
AuditDB: filepath.Join(t.TempDir(), "audit.db"),
|
|
Env: "dev",
|
|
IAMEndpoint: jwks.URL,
|
|
ExpectedIssuer: jwks.URL,
|
|
ExpectedAudience: "kms-test",
|
|
JWKSURL: jwks.URL + "/.well-known/jwks",
|
|
SkipListen: true, // mount via httptest.Server
|
|
ZAPPort: -1, // disable ZAP (no master key in env)
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Embed: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
stopCtx, stopCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer stopCancel()
|
|
if err := em.Stop(stopCtx); err != nil {
|
|
t.Errorf("Stop: %v", err)
|
|
}
|
|
})
|
|
|
|
srv := httptest.NewServer(em.HTTPHandler())
|
|
t.Cleanup(srv.Close)
|
|
|
|
t.Run("healthz", func(t *testing.T) {
|
|
resp, err := http.Get(srv.URL + "/healthz")
|
|
if err != nil {
|
|
t.Fatalf("GET /healthz: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("GET /healthz: status %d, want 200", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("v1_kms_health", func(t *testing.T) {
|
|
resp, err := http.Get(srv.URL + "/v1/kms/health")
|
|
if err != nil {
|
|
t.Fatalf("GET /v1/kms/health: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("GET /v1/kms/health: status %d, want 200", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("secret_route_requires_auth", func(t *testing.T) {
|
|
// Sanity check: even though Env=dev tolerates missing JWT config
|
|
// at boot, every secret route still demands a verified bearer
|
|
// token at request time. Unauthenticated → 401.
|
|
resp, err := http.Get(srv.URL + "/v1/kms/orgs/hanzo/secrets/foo/bar")
|
|
if err != nil {
|
|
t.Fatalf("GET secrets: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("unauthenticated GET: status %d, want 401", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("stop_is_idempotent", func(t *testing.T) {
|
|
// Stop twice in a row must not panic or leak goroutines.
|
|
ctx1, c1 := context.WithTimeout(context.Background(), time.Second)
|
|
defer c1()
|
|
if err := em.Stop(ctx1); err != nil {
|
|
t.Errorf("first Stop: %v", err)
|
|
}
|
|
ctx2, c2 := context.WithTimeout(context.Background(), time.Second)
|
|
defer c2()
|
|
if err := em.Stop(ctx2); err != nil {
|
|
t.Errorf("second Stop: %v", err)
|
|
}
|
|
})
|
|
}
|