mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
Two things wore the name "platform" here, and neither is the PaaS at
platform.hanzo.ai. That collision is why "where are my Bases?" had no answer a
person could act on: one prefix, two products, and the one you reached was
whichever you happened to mean.
A Base is what an org gets — its own collections, its own files, its own key —
so the resource a client addresses is /v1/bases. Not /v1/orgs: the org is IAM's
noun and Base does not get a second one. Everything scoped to an org now hangs
off the Base it belongs to (/v1/bases/{id}/config, /creds, /customers), and
compliance moves to /v1/compliance, a sibling of /v1/iam rather than a child of
Base's own resource. The package is `org`, which seven of its files already
were. Nothing addressed the old prefix — no client, no UI, no manifest — so this
renames a surface rather than breaking one.
The auth proxy is DELETED, not renamed. Its five routes reshaped bodies and
forwarded to /v1/iam/login, /signup, /userinfo and /send-verification-code —
which the opaque /v1/iam/{path...} passthrough beside it already reaches. So
Base carried a hand-written login, signup and one-time-code flow whose only job
was to be a second way to reach IAM. Auth is IAM's, and one way is the whole
point of that rule.
The prefix test that already banned /api/ bans this too, so the name cannot come
back: it reads route LITERALS through go/ast, so a comment explaining the ban is
prose and a route is a route. Verified to fail on the exact regression.
123 lines
4.4 KiB
Go
123 lines
4.4 KiB
Go
// Package kmskeyring binds store.RootSource to Hanzo KMS.
|
|
//
|
|
// Each org's 32-byte KEK is held in KMS under one fixed secret name, scoped
|
|
// per org. The KEK is the ONLY key material in KMS; the per-DB data keys are
|
|
// random age identities wrapped under it by store.Keyring and stored as
|
|
// sidecars next to the DBs (envelope encryption). Rotating an org KEK in KMS
|
|
// therefore never rewrites a DB — see store.Keyring.Rotate.
|
|
//
|
|
// # Wiring
|
|
//
|
|
// kmskeyring depends only on the minimal SecretStore contract so the store
|
|
// substrate stays orthogonal to any specific KMS transport. Production wires
|
|
// the canonical Base→KMS facade (plugins/org.KMSClient) with a trivial
|
|
// adapter:
|
|
//
|
|
// type kmsAdapter struct{ c *platform.KMSClient }
|
|
// func (a kmsAdapter) Get(_ context.Context, org, name string) (string, error) {
|
|
// v, err := a.c.GetSecret(org, name)
|
|
// if isNotFound(err) { return "", kmskeyring.ErrNotFound }
|
|
// return v, err
|
|
// }
|
|
// func (a kmsAdapter) Put(_ context.Context, org, name, val string) error {
|
|
// return a.c.SetSecret(org, name, val)
|
|
// }
|
|
//
|
|
// src, _ := kmskeyring.New(kmsAdapter{c: kmsClient})
|
|
// keyring, _ := store.NewKeyring(src, objectStore)
|
|
// mtStore, _ := store.New(store.Options{ObjectStore: objectStore, Keys: keyring})
|
|
package kmskeyring
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// KEKSecretName is the fixed KMS secret leaf holding an org's store KEK. One
|
|
// KEK per org; the secret's org scoping is the SecretStore's responsibility.
|
|
const KEKSecretName = "base/store-kek"
|
|
|
|
// kekLen is the KEK length in bytes (AES-256).
|
|
const kekLen = 32
|
|
|
|
// ErrNotFound signals an absent secret. Adapters MUST map their backend's
|
|
// not-found to this (via errors.Is) so OrgRoot can create-on-first-use.
|
|
var ErrNotFound = errors.New("kmskeyring: secret not found")
|
|
|
|
// SecretStore is the minimal, per-org KMS surface kmskeyring needs.
|
|
// Implementations are goroutine-safe. Get returns ErrNotFound (errors.Is)
|
|
// when the named secret is absent for the org.
|
|
type SecretStore interface {
|
|
Get(ctx context.Context, orgID, name string) (string, error)
|
|
Put(ctx context.Context, orgID, name, value string) error
|
|
}
|
|
|
|
// Source implements store.RootSource over a SecretStore.
|
|
type Source struct {
|
|
kms SecretStore
|
|
name string
|
|
}
|
|
|
|
// New builds a Source over kms, using the canonical KEK secret name.
|
|
func New(kms SecretStore) (*Source, error) {
|
|
if kms == nil {
|
|
return nil, errors.New("kmskeyring: SecretStore is required")
|
|
}
|
|
return &Source{kms: kms, name: KEKSecretName}, nil
|
|
}
|
|
|
|
// OrgRoot returns the org's 32-byte KEK, creating and persisting a fresh
|
|
// random KEK in KMS on first use, and returning a fresh copy each call (the
|
|
// caller may zero it).
|
|
//
|
|
// Create-on-first-use is a convenience for the lazy path; the correct
|
|
// production posture is to PROVISION the KEK once at org-creation time under a
|
|
// single writer (or via a KMS compare-and-set Put) so two pods can never race
|
|
// two different KEKs into existence. On a create race with a last-writer-wins
|
|
// backend, both pods converge by re-reading the authoritative value here.
|
|
func (s *Source) OrgRoot(ctx context.Context, orgID string) ([]byte, error) {
|
|
if orgID == "" {
|
|
return nil, errors.New("kmskeyring: empty orgID")
|
|
}
|
|
|
|
val, err := s.kms.Get(ctx, orgID, s.name)
|
|
if err == nil {
|
|
return decodeKEK(orgID, val)
|
|
}
|
|
if !errors.Is(err, ErrNotFound) {
|
|
return nil, fmt.Errorf("kmskeyring: get KEK for %s: %w", orgID, err)
|
|
}
|
|
|
|
// Absent — mint and persist a fresh KEK.
|
|
kek := make([]byte, kekLen)
|
|
if _, err := rand.Read(kek); err != nil {
|
|
return nil, fmt.Errorf("kmskeyring: generate KEK: %w", err)
|
|
}
|
|
if err := s.kms.Put(ctx, orgID, s.name, hex.EncodeToString(kek)); err != nil {
|
|
return nil, fmt.Errorf("kmskeyring: persist KEK for %s: %w", orgID, err)
|
|
}
|
|
|
|
// Re-read the authoritative value to converge if another pod won the
|
|
// create race; adopt whichever KEK KMS now holds.
|
|
val, err = s.kms.Get(ctx, orgID, s.name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("kmskeyring: reread KEK for %s: %w", orgID, err)
|
|
}
|
|
return decodeKEK(orgID, val)
|
|
}
|
|
|
|
// decodeKEK hex-decodes and length-checks a stored KEK.
|
|
func decodeKEK(orgID, val string) ([]byte, error) {
|
|
kek, err := hex.DecodeString(val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("kmskeyring: decode KEK for %s: %w", orgID, err)
|
|
}
|
|
if len(kek) != kekLen {
|
|
return nil, fmt.Errorf("kmskeyring: KEK for %s must be %d bytes, got %d", orgID, kekLen, len(kek))
|
|
}
|
|
return kek, nil
|
|
}
|