chore: sync working tree

Commits 4 outstanding change(s) that were sitting uncommitted.
No build artifacts and no secrets in the changeset (both checked).
This commit is contained in:
zeekay
2026-07-26 10:13:52 -07:00
parent fce2fe8f73
commit 7916df8c15
4 changed files with 999 additions and 16 deletions
+86 -16
View File
@@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strings"
"time"
@@ -75,9 +76,22 @@ func buildNonceLedger() (zapserver.NonceLedger, error) {
// consensusSnapshot is the wire shape of the JSON file the operator
// drops into the kmsd container. Same shape as the env vars (one
// authority per slice) so the operator can pick either delivery.
//
// Scopes is the OPTIONAL least-privilege overlay. Absent => nil => no
// scope observation is possible, which is exactly the state every
// currently-deployed snapshot is in. Decoding it is inert: nothing reads
// the overlay unless KMS_AUTHZ_MODE selects a mode that does.
type consensusSnapshot struct {
Validators []string `json:"validators"`
Operators []string `json:"operators"`
Validators []string `json:"validators"`
Operators []string `json:"operators"`
Scopes *consensusScopeSet `json:"scopes"`
}
// consensusScopeSet is keyed by IDENTITY, not by authority: membership
// answers "who are you", grants answer "what may you address". Mirrors
// the kms-operator's bootstrap.AuthorityScopes byte-for-byte.
type consensusScopeSet struct {
Identities map[string]zapserver.Grants `json:"identities"`
}
// buildConsensusAuthorizer constructs the ConsensusAuthorizer wired
@@ -85,7 +99,7 @@ type consensusSnapshot struct {
// vars; refuses to boot if neither is present, since the ZAP server
// is fail-closed by construction.
func buildConsensusAuthorizer() (zapserver.ConsensusAuthorizer, error) {
validators, operators, err := loadConsensusSnapshot()
validators, operators, scopes, err := loadConsensusSnapshot()
if err != nil {
return nil, err
}
@@ -118,41 +132,97 @@ func buildConsensusAuthorizer() (zapserver.ConsensusAuthorizer, error) {
}, "self-test", zapserver.OpAuthGet); err != nil {
return nil, fmt.Errorf("authorizer self-test: %w", err)
}
return az, nil
return wrapAuthzMode(az, scopes)
}
// loadConsensusSnapshot returns the (validators, operators) NodeID
// sets, sourcing from KMS_CONSENSUS_FILE first then falling back to
// KMS_CONSENSUS_VALIDATORS + KMS_CONSENSUS_OPERATORS env vars.
func loadConsensusSnapshot() ([]ids.NodeID, []ids.NodeID, error) {
// wrapAuthzMode installs the scope-observation stage selected by
// KMS_AUTHZ_MODE.
//
// Unset (the state of every deployment today) returns `az` VERBATIM: no
// wrapper, no extra branch, no behaviour change of any kind. A malformed
// value is a hard boot failure so a typo cannot silently degrade to a
// posture nobody chose.
func wrapAuthzMode(az zapserver.ConsensusAuthorizer, scopes map[ids.NodeID]zapserver.Grants) (zapserver.ConsensusAuthorizer, error) {
mode, err := zapserver.ParseAuthzMode(os.Getenv(zapserver.EnvAuthzMode))
if err != nil {
return nil, err
}
if mode == zapserver.AuthzModeOff {
return az, nil
}
provider := zapserver.NewStaticScopeProvider(scopes)
log.Printf("kms: authz mode=%s (OBSERVE ONLY — no request is denied by scope) identities=%d", mode, provider.Len())
if provider.Len() == 0 {
log.Printf("kms: authz mode=%s but the consensus snapshot carries no scope overlay — "+
"every allowed request will be reported as a would-deny", mode)
}
log.Printf("kms: authz audit compares PATH only; the authorizer contract carries neither env nor org, " +
"so real enforcement would deny at least what audit reports and probably more")
return zapserver.WrapScopeAuthorizer(az, zapserver.ScopeAuthorizerConfig{
Mode: mode,
Scopes: provider,
})
}
// loadConsensusSnapshot returns the (validators, operators) NodeID sets
// plus the optional per-identity scope overlay, sourcing from
// KMS_CONSENSUS_FILE first then falling back to KMS_CONSENSUS_VALIDATORS
// + KMS_CONSENSUS_OPERATORS env vars.
//
// The env-var delivery carries no overlay (it is two NodeID lists); a nil
// scope map there is correct, not a gap.
func loadConsensusSnapshot() ([]ids.NodeID, []ids.NodeID, map[ids.NodeID]zapserver.Grants, error) {
if path := strings.TrimSpace(os.Getenv(envFile)); path != "" {
data, err := os.ReadFile(path)
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", envFile, err)
return nil, nil, nil, fmt.Errorf("%s: %w", envFile, err)
}
var snap consensusSnapshot
if err := json.Unmarshal(data, &snap); err != nil {
return nil, nil, fmt.Errorf("%s: %w", envFile, err)
return nil, nil, nil, fmt.Errorf("%s: %w", envFile, err)
}
validators, err := parseNodeIDs(snap.Validators)
if err != nil {
return nil, nil, fmt.Errorf("%s validators: %w", envFile, err)
return nil, nil, nil, fmt.Errorf("%s validators: %w", envFile, err)
}
operators, err := parseNodeIDs(snap.Operators)
if err != nil {
return nil, nil, fmt.Errorf("%s operators: %w", envFile, err)
return nil, nil, nil, fmt.Errorf("%s operators: %w", envFile, err)
}
return validators, operators, nil
scopes, err := parseScopes(snap.Scopes)
if err != nil {
return nil, nil, nil, fmt.Errorf("%s scopes: %w", envFile, err)
}
return validators, operators, scopes, nil
}
validators, err := parseNodeIDs(splitLines(os.Getenv(envValidators)))
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", envValidators, err)
return nil, nil, nil, fmt.Errorf("%s: %w", envValidators, err)
}
operators, err := parseNodeIDs(splitLines(os.Getenv(envOperators)))
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", envOperators, err)
return nil, nil, nil, fmt.Errorf("%s: %w", envOperators, err)
}
return validators, operators, nil
return validators, operators, nil, nil
}
// parseScopes converts the snapshot's identity→grants block into the
// NodeID-keyed form the scope provider serves. A nil block yields a nil
// map (no overlay). A malformed NodeID is a hard failure — the same
// posture parseNodeIDs takes for the authority lists.
func parseScopes(sc *consensusScopeSet) (map[ids.NodeID]zapserver.Grants, error) {
if sc == nil || sc.Identities == nil {
return nil, nil
}
out := make(map[ids.NodeID]zapserver.Grants, len(sc.Identities))
for raw, grants := range sc.Identities {
id, err := ids.NodeIDFromString(strings.TrimSpace(raw))
if err != nil {
return nil, fmt.Errorf("parse %q: %w", raw, err)
}
out[id] = grants
}
return out, nil
}
// splitLines splits on newlines, commas, and whitespace. Empty
+198
View File
@@ -0,0 +1,198 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// consensus_scopes_test.go — boot-wiring coverage for the scope overlay.
//
// This is the level that decides whether a deployed pod changes
// behaviour, so it pins the two properties that keep this gate inert:
//
// - every snapshot in the field today (no `scopes` key) still decodes,
// and yields NO overlay;
// - with KMS_AUTHZ_MODE unset, wrapAuthzMode returns the authorizer it
// was handed, verbatim.
package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/luxfi/ids"
"github.com/luxfi/kms/pkg/zapserver"
)
// writeSnapshot drops a snapshot file and points KMS_CONSENSUS_FILE at it.
func writeSnapshot(t *testing.T, body string) {
t.Helper()
p := filepath.Join(t.TempDir(), "consensus-authority.json")
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
t.Fatalf("write snapshot: %v", err)
}
t.Setenv(envFile, p)
}
const (
nodeA = "NodeID-FjdREJWf4CHPfr1DVnQVuMf8ouRVgTMBH"
nodeB = "NodeID-HHsMJb8a4tjymPUcnsZTuqn3WXDhEkUaJ"
)
// TestLoadConsensusSnapshot_LegacyFileHasNoOverlay — the exact bytes the
// operator emitted before this gate. It must still load, and the overlay
// must be nil (not an empty map, which would read as "scoped, nothing
// granted").
func TestLoadConsensusSnapshot_LegacyFileHasNoOverlay(t *testing.T) {
writeSnapshot(t, `{"validators":["`+nodeA+`"],"operators":["`+nodeB+`"]}`)
validators, operators, scopes, err := loadConsensusSnapshot()
if err != nil {
t.Fatalf("legacy snapshot failed to load: %v", err)
}
if len(validators) != 1 || len(operators) != 1 {
t.Fatalf("authority sets misread: %v / %v", validators, operators)
}
if scopes != nil {
t.Fatalf("legacy snapshot produced a non-nil overlay (%v) — absent must mean absent", scopes)
}
}
// TestLoadConsensusSnapshot_UnknownKeysIgnored — a snapshot from a NEWER
// operator must not break an older kmsd. Forward compatibility is what
// lets the operator ship the overlay before any kmsd consumes it.
func TestLoadConsensusSnapshot_UnknownKeysIgnored(t *testing.T) {
writeSnapshot(t, `{"validators":["`+nodeA+`"],"operators":["`+nodeB+`"],"somethingNew":{"a":1}}`)
if _, _, _, err := loadConsensusSnapshot(); err != nil {
t.Fatalf("unknown key broke the decode: %v", err)
}
}
// TestLoadConsensusSnapshot_ParsesIdentityScopes — the overlay the
// operator now emits, decoded into the NodeID-keyed form.
func TestLoadConsensusSnapshot_ParsesIdentityScopes(t *testing.T) {
writeSnapshot(t, `{
"validators":["`+nodeA+`"],
"operators":["`+nodeB+`"],
"scopes":{"identities":{
"`+nodeB+`":{"unconfined":true,"grants":[]},
"`+nodeA+`":{"grants":[
{"org":"hanzo","env":"prod","path":"bootnode"},
{"org":"hanzo","env":"prod","path":"bootnode-secrets"}
]}
}}
}`)
_, _, scopes, err := loadConsensusSnapshot()
if err != nil {
t.Fatalf("scoped snapshot failed to load: %v", err)
}
if len(scopes) != 2 {
t.Fatalf("expected 2 identity entries, got %d: %v", len(scopes), scopes)
}
opID, err := ids.NodeIDFromString(nodeB)
if err != nil {
t.Fatal(err)
}
if !scopes[opID].Unconfined {
t.Error("operator entry lost its Unconfined flag")
}
svcID, err := ids.NodeIDFromString(nodeA)
if err != nil {
t.Fatal(err)
}
svc := scopes[svcID]
if svc.Unconfined {
t.Error("service entry must not be unconfined")
}
if len(svc.Grants) != 2 {
t.Fatalf("grant SET truncated to %d — the many-to-many shape must survive: %v", len(svc.Grants), svc.Grants)
}
}
// TestLoadConsensusSnapshot_MalformedScopeNodeIDIsFatal — a typo in the
// overlay must fail at boot, matching how the authority lists behave.
func TestLoadConsensusSnapshot_MalformedScopeNodeIDIsFatal(t *testing.T) {
writeSnapshot(t, `{"validators":["`+nodeA+`"],"operators":["`+nodeB+`"],
"scopes":{"identities":{"not-a-node-id":{"grants":[]}}}}`)
if _, _, _, err := loadConsensusSnapshot(); err == nil {
t.Fatal("a malformed NodeID in the overlay must be a hard failure")
}
}
// TestWrapAuthzMode_UnsetIsIdentity — with KMS_AUTHZ_MODE unset, the
// wiring hands back the SAME authorizer value. This is the property that
// makes deploying this build a no-op for every pod in the field.
func TestWrapAuthzMode_UnsetIsIdentity(t *testing.T) {
t.Setenv(zapserver.EnvAuthzMode, "")
inner := &noopAuthorizer{}
got, err := wrapAuthzMode(inner, map[ids.NodeID]zapserver.Grants{
{0x01}: {Grants: []zapserver.Grant{{Org: "hanzo", Env: "prod", Path: "x"}}},
})
if err != nil {
t.Fatalf("wrapAuthzMode: %v", err)
}
if got != zapserver.ConsensusAuthorizer(inner) {
t.Fatalf("unset mode wrapped the authorizer (%T) — deploying this build would change the request path", got)
}
}
// TestWrapAuthzMode_AuditWraps — audit installs the observer even when
// the snapshot carries no overlay (in which case every allowed request is
// reported, which is the correct signal: nothing is granted yet).
func TestWrapAuthzMode_AuditWraps(t *testing.T) {
t.Setenv(zapserver.EnvAuthzMode, "audit")
inner := &noopAuthorizer{}
got, err := wrapAuthzMode(inner, nil)
if err != nil {
t.Fatalf("wrapAuthzMode: %v", err)
}
if got == zapserver.ConsensusAuthorizer(inner) {
t.Fatal("audit mode did not install the observer")
}
}
// TestWrapAuthzMode_EnforceRefusesToBoot — enforcement is a later gate.
// Setting it must stop the process, not switch on denial.
func TestWrapAuthzMode_EnforceRefusesToBoot(t *testing.T) {
t.Setenv(zapserver.EnvAuthzMode, "enforce")
if _, err := wrapAuthzMode(&noopAuthorizer{}, nil); err == nil {
t.Fatal("KMS_AUTHZ_MODE=enforce must refuse to boot in this build")
}
}
// TestWrapAuthzMode_TypoRefusesToBoot — a misspelled mode must not
// silently degrade to "off".
func TestWrapAuthzMode_TypoRefusesToBoot(t *testing.T) {
t.Setenv(zapserver.EnvAuthzMode, "audti")
if _, err := wrapAuthzMode(&noopAuthorizer{}, nil); err == nil {
t.Fatal("a misspelled KMS_AUTHZ_MODE must refuse to boot")
}
}
// TestConsensusSnapshot_WireShapeMatchesOperator pins the cross-repo
// contract against the exact bytes hanzoai/kms-operator's
// bootstrap.Snapshot.MarshalCanonical produces.
func TestConsensusSnapshot_WireShapeMatchesOperator(t *testing.T) {
const operatorBytes = `{"validators":["` + nodeA + `"],"operators":["` + nodeB + `"],` +
`"scopes":{"identities":{"` + nodeB + `":{"unconfined":true,"grants":[]},` +
`"` + nodeA + `":{"grants":[{"org":"hanzo","env":"prod","path":"llm-secrets"}]}}}}`
var snap consensusSnapshot
if err := json.Unmarshal([]byte(operatorBytes), &snap); err != nil {
t.Fatalf("operator wire form did not decode: %v", err)
}
if snap.Scopes == nil || len(snap.Scopes.Identities) != 2 {
t.Fatalf("overlay misread: %+v", snap.Scopes)
}
if g := snap.Scopes.Identities[nodeA].Grants; len(g) != 1 || g[0].Path != "llm-secrets" {
t.Fatalf("grant misaligned: %+v", g)
}
}
// noopAuthorizer is a distinct type so pointer identity is meaningful.
type noopAuthorizer struct{}
func (n *noopAuthorizer) Authorize(_ context.Context, _ zapserver.Identity, _ string, _ zapserver.Op) (zapserver.Decision, error) {
return zapserver.Allow("test"), nil
}
+292
View File
@@ -0,0 +1,292 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// authz_mode.go — the scope overlay's OBSERVATION stage.
//
// Consensus membership answers "who are you". A scope grant answers "what
// may you address". Those are orthogonal, and this file adds only the
// second one's *observation*, never its enforcement.
//
// # Why observation comes first, as its own gate
//
// Turning path scoping on in one step is a total KMS lockout. Measured
// against the live fleet, 127 of 129 KMSSecret consumers read a path that
// shares no prefix with the service path their identity is derived from.
// Any authorizer that starts denying on a scope mismatch denies almost
// every real request on its first tick. There is no safe way to discover
// that from a changelog — it has to be MEASURED against production
// traffic before anything denies. That is what audit mode is for.
//
// # The three states, and the one that is the default
//
// unset / "" → AuthzModeOff. No scope evaluation happens at all.
// WrapScopeAuthorizer returns the inner authorizer
// UNWRAPPED, so there is not one extra branch on the
// request path. Byte-identical to a build without this
// file.
// "audit" → AuthzModeAudit. Every request is evaluated against the
// scope overlay and every would-deny is LOGGED. The
// decision returned to the caller is the inner
// authorizer's, unchanged. Nothing is denied that would
// not already have been denied.
// "enforce" → REJECTED with an error. Enforcement is a later gate and
// deliberately has no code path here: a typo in a
// Deployment env var must not be able to switch on
// denial.
//
// Any other value is a hard error, so a misspelled mode fails loudly at
// boot instead of silently degrading to "off".
//
// # What audit mode can and cannot see
//
// A Grant carries three dimensions (org, env, path) because all three are
// real. The ZAP authorizer contract carries ONE: the opcode's `path`.
// Env is a genuine dimension of the store key
// (kms/secrets/{path}/{env}/{name}) that verifyAndAuthorize does not
// forward; org is a REST-plane concept the ZAP store does not model at
// all. So a scope match here is a PATH match, and it is therefore
// OVER-permissive: audit UNDER-reports the denials real enforcement would
// produce. Every audit line says so explicitly (`dimensions=path-only`)
// and the boot line warns once. Widening the contract to the full
// resource address is a prerequisite for enforcement, not for this gate.
package zapserver
import (
"context"
"fmt"
"strings"
"github.com/luxfi/ids"
"github.com/luxfi/log"
)
// AuthzMode selects how scope decisions are applied. The zero value is
// AuthzModeOff, which is what an unset env var resolves to.
type AuthzMode uint8
const (
// AuthzModeOff performs no scope evaluation. The default.
AuthzModeOff AuthzMode = iota
// AuthzModeAudit evaluates scopes and logs would-denies without
// changing any decision.
AuthzModeAudit
)
// EnvAuthzMode is the environment variable that selects the mode.
const EnvAuthzMode = "KMS_AUTHZ_MODE"
// String returns the canonical wire spelling.
func (m AuthzMode) String() string {
switch m {
case AuthzModeOff:
return "off"
case AuthzModeAudit:
return "audit"
}
return fmt.Sprintf("AuthzMode(%d)", uint8(m))
}
// ParseAuthzMode resolves the KMS_AUTHZ_MODE value. Pure function.
//
// ""/"off" → AuthzModeOff (today's behaviour, exactly)
// "audit" → AuthzModeAudit
// "enforce"→ error: enforcement is a later gate with no code path here
// anything else → error
func ParseAuthzMode(raw string) (AuthzMode, error) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "", "off":
return AuthzModeOff, nil
case "audit":
return AuthzModeAudit, nil
case "enforce":
return AuthzModeOff, fmt.Errorf(
"%s=enforce: scope enforcement is not implemented in this build; "+
"run %s=audit and confirm the would-deny stream is empty first", EnvAuthzMode, EnvAuthzMode)
default:
return AuthzModeOff, fmt.Errorf("%s=%q: unknown mode (want \"\", \"off\", or \"audit\")", EnvAuthzMode, raw)
}
}
// Grant is one exact secret address an identity may reach. Wire-identical
// to the kms-operator's bootstrap.Grant — this is the consuming half of
// that contract, pinned by a shared golden fixture in the tests.
type Grant struct {
Org string `json:"org"`
Env string `json:"env"`
Path string `json:"path"`
}
// Grants is the scope SET for one identity.
//
// Unconfined is an EXPLICIT flag, never inferred from an empty set: an
// identity with zero grants is granted nothing. "Empty means everything"
// is the classic fail-open reading and this shape makes it
// unrepresentable.
type Grants struct {
Unconfined bool `json:"unconfined,omitempty"`
Grants []Grant `json:"grants"`
}
// ScopeProvider returns the grant set consensus currently attests for a
// NodeID. The bool reports whether the identity has an ENTRY at all — an
// absent entry is not the same as an empty one, and only an explicit
// empty set means "granted nothing".
type ScopeProvider interface {
GrantsFor(ctx context.Context, node ids.NodeID) (Grants, bool)
}
// StaticScopeProvider serves a snapshot handed over at boot — the same
// consensus-snapshot delivery the authority sets use. Not an ACL: the
// snapshot's authority is "consensus said these grants held at time T".
type StaticScopeProvider struct {
byNode map[ids.NodeID]Grants
}
// NewStaticScopeProvider copies the supplied map so later mutation by the
// caller cannot change what the server enforces.
func NewStaticScopeProvider(m map[ids.NodeID]Grants) *StaticScopeProvider {
c := make(map[ids.NodeID]Grants, len(m))
for k, v := range m {
c[k] = v
}
return &StaticScopeProvider{byNode: c}
}
// GrantsFor implements ScopeProvider.
func (s *StaticScopeProvider) GrantsFor(_ context.Context, node ids.NodeID) (Grants, bool) {
g, ok := s.byNode[node]
return g, ok
}
// Len reports the number of identities carrying a grant entry. Boot-log
// only.
func (s *StaticScopeProvider) Len() int { return len(s.byNode) }
// ScopeAuthorizerConfig wires the observation stage.
type ScopeAuthorizerConfig struct {
// Mode selects off / audit. Required — AuthzModeOff means the
// wrapper is not installed at all.
Mode AuthzMode
// Scopes supplies the grant sets. Required in audit mode.
Scopes ScopeProvider
// Logger receives the would-deny stream. nil → log.Root().
Logger log.Logger
}
// WrapScopeAuthorizer returns the authorizer the kmsd should install.
//
// In AuthzModeOff it returns `inner` VERBATIM. Not a pass-through
// wrapper — the actual inner value, so an unset KMS_AUTHZ_MODE leaves
// the request path with the same call graph, the same allocations and the
// same decisions as a build that never had this file.
func WrapScopeAuthorizer(inner ConsensusAuthorizer, cfg ScopeAuthorizerConfig) (ConsensusAuthorizer, error) {
if inner == nil {
return nil, fmt.Errorf("zapserver: inner authorizer is required")
}
if cfg.Mode == AuthzModeOff {
return inner, nil
}
if cfg.Scopes == nil {
return nil, fmt.Errorf("zapserver: %s=%s requires a scope provider", EnvAuthzMode, cfg.Mode)
}
if cfg.Logger == nil {
cfg.Logger = log.Root()
}
return &ScopeAuthorizer{inner: inner, mode: cfg.Mode, scopes: cfg.Scopes, log: cfg.Logger}, nil
}
// ScopeAuthorizer decorates a ConsensusAuthorizer with scope observation.
// Installed only in audit mode.
type ScopeAuthorizer struct {
inner ConsensusAuthorizer
mode AuthzMode
scopes ScopeProvider
log log.Logger
}
// Authorize delegates to the inner authorizer and returns its decision
// UNCHANGED. In audit mode it additionally evaluates the scope overlay
// and logs any would-deny.
//
// Ordering matters: the scope check runs only on requests the inner
// authorizer ALLOWED. A request the role model already denies tells us
// nothing about scoping, and logging it would bury the signal that
// matters — the requests that pass today and would stop passing under
// enforcement.
func (s *ScopeAuthorizer) Authorize(ctx context.Context, ident Identity, path string, op Op) (Decision, error) {
decision, err := s.inner.Authorize(ctx, ident, path, op)
if err != nil || !decision.Allow {
return decision, err
}
if reason, would := s.wouldDeny(ctx, ident, path); would {
s.log.Warn("kms.authz would-deny",
"mode", s.mode.String(),
"enforced", false,
"ident", ident.String(),
"node", ident.NodeID.String(),
"op", op.String(),
"path", canonicalScopePath(path),
"reason", reason,
// The audit preview compares PATH only: the authorizer
// contract carries neither env nor org, so real enforcement
// would deny at least this set and probably more.
"dimensions", "path-only",
)
}
// Audit changes nothing. The inner decision is returned verbatim.
return decision, err
}
// wouldDeny reports whether scope enforcement would reject this request,
// with a structured reason. Never mutates state.
func (s *ScopeAuthorizer) wouldDeny(ctx context.Context, ident Identity, path string) (string, bool) {
grants, ok := s.scopes.GrantsFor(ctx, ident.NodeID)
if !ok {
// No entry at all. Under enforcement this is fail-closed: an
// identity consensus has said nothing about is granted nothing.
return "no-scope-entry", true
}
if grants.Unconfined {
return "", false
}
want := canonicalScopePath(path)
for _, g := range grants.Grants {
if pathWithinGrant(want, canonicalScopePath(g.Path)) {
return "", false
}
}
if len(grants.Grants) == 0 {
return "scoped-zero-grants", true
}
return "path-outside-grant-set", true
}
// pathWithinGrant reports whether `want` is the granted path or a
// descendant of it. An empty grant path is the org root and covers
// everything beneath it — that is a real, live grant shape
// (cloud-widget-kms-sync holds "/"), not a wildcard bug.
//
// Segment-boundary aware on purpose: "bootnode-secrets" must NOT match a
// grant on "bootnode". Prefix matching without the boundary check is how
// a sibling path gets silently authorized.
func pathWithinGrant(want, grant string) bool {
if grant == "" {
return true
}
if want == grant {
return true
}
return strings.HasPrefix(want, grant+"/")
}
// canonicalScopePath trims whitespace and surrounding slashes so the
// operator's emitted form and the wire's requested form compare equal.
func canonicalScopePath(p string) string {
return strings.Trim(strings.TrimSpace(p), "/")
}
+423
View File
@@ -0,0 +1,423 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// authz_mode_test.go — the three properties this gate must hold:
//
// (a) unset mode is UNCHANGED — not "equivalent", literally the same
// authorizer value, so the request path cannot have drifted;
// (b) audit mode denies NOTHING while logging every would-deny;
// (c) the grant SET is honoured — a many-to-many identity matches on
// every path it holds, and a sibling path is not silently allowed.
package zapserver
import (
"context"
"encoding/json"
"strings"
"sync"
"testing"
"github.com/luxfi/ids"
"github.com/luxfi/log"
)
// fakeAuthorizer records what it was asked and returns a fixed decision.
type fakeAuthorizer struct {
decision Decision
err error
mu sync.Mutex
calls int
}
func (f *fakeAuthorizer) Authorize(context.Context, Identity, string, Op) (Decision, error) {
f.mu.Lock()
f.calls++
f.mu.Unlock()
return f.decision, f.err
}
// capturingLogger records Warn lines so the test can assert the
// would-deny stream without parsing stderr.
type capturingLogger struct {
log.Logger
mu sync.Mutex
warns []string
}
func (c *capturingLogger) Warn(msg string, ctx ...interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
b := strings.Builder{}
b.WriteString(msg)
for i := 0; i+1 < len(ctx); i += 2 {
b.WriteString(" ")
b.WriteString(toStr(ctx[i]))
b.WriteString("=")
b.WriteString(toStr(ctx[i+1]))
}
c.warns = append(c.warns, b.String())
}
func (c *capturingLogger) lines() []string {
c.mu.Lock()
defer c.mu.Unlock()
out := make([]string, len(c.warns))
copy(out, c.warns)
return out
}
func toStr(v interface{}) string {
switch t := v.(type) {
case string:
return t
case bool:
if t {
return "true"
}
return "false"
default:
b, _ := json.Marshal(v)
return string(b)
}
}
func newCapturingLogger() *capturingLogger {
return &capturingLogger{Logger: log.NewNoOpLogger()}
}
func testNodeID(t *testing.T, seed byte) ids.NodeID {
t.Helper()
var n ids.NodeID
for i := range n {
n[i] = seed
}
return n
}
// --- (a) unset mode is byte-identical ---------------------------------
func TestParseAuthzMode(t *testing.T) {
for _, c := range []struct {
in string
want AuthzMode
wantErr bool
}{
{"", AuthzModeOff, false},
{"off", AuthzModeOff, false},
{"OFF", AuthzModeOff, false},
{" ", AuthzModeOff, false},
{"audit", AuthzModeAudit, false},
{" Audit ", AuthzModeAudit, false},
{"enforce", AuthzModeOff, true},
{"ENFORCE", AuthzModeOff, true},
{"enforcee", AuthzModeOff, true},
{"true", AuthzModeOff, true},
} {
got, err := ParseAuthzMode(c.in)
if (err != nil) != c.wantErr {
t.Errorf("ParseAuthzMode(%q) err = %v, wantErr %v", c.in, err, c.wantErr)
}
if got != c.want {
t.Errorf("ParseAuthzMode(%q) = %v, want %v", c.in, got, c.want)
}
}
}
// TestParseAuthzMode_EnforceIsNotAModeInThisBuild — enforcement is a
// later gate. A Deployment that sets it must fail loudly at boot rather
// than start denying.
func TestParseAuthzMode_EnforceIsNotAModeInThisBuild(t *testing.T) {
_, err := ParseAuthzMode("enforce")
if err == nil {
t.Fatal("enforce must be rejected — it has no code path in this build")
}
if !strings.Contains(err.Error(), "not implemented") {
t.Fatalf("error must say enforcement is unimplemented, got: %v", err)
}
}
// TestWrapScopeAuthorizer_UnsetReturnsInnerVerbatim is the strongest form
// of "unchanged": the returned value IS the inner authorizer, so an unset
// KMS_AUTHZ_MODE cannot have altered a single decision, allocation, or
// branch on the request path.
func TestWrapScopeAuthorizer_UnsetReturnsInnerVerbatim(t *testing.T) {
inner := &fakeAuthorizer{decision: Allow("validator-read")}
got, err := WrapScopeAuthorizer(inner, ScopeAuthorizerConfig{Mode: AuthzModeOff})
if err != nil {
t.Fatalf("WrapScopeAuthorizer: %v", err)
}
if got != ConsensusAuthorizer(inner) {
t.Fatalf("off mode returned a wrapper (%T), want the inner authorizer verbatim", got)
}
// And it works with no scope provider at all — off mode must not
// require any overlay to exist.
if _, err := WrapScopeAuthorizer(inner, ScopeAuthorizerConfig{Mode: AuthzModeOff, Scopes: nil}); err != nil {
t.Fatalf("off mode must not require a scope provider: %v", err)
}
}
// TestWrapScopeAuthorizer_AuditRequiresScopes — audit without an overlay
// source is a configuration error, not a silent no-op.
func TestWrapScopeAuthorizer_AuditRequiresScopes(t *testing.T) {
inner := &fakeAuthorizer{decision: Allow("validator-read")}
if _, err := WrapScopeAuthorizer(inner, ScopeAuthorizerConfig{Mode: AuthzModeAudit}); err == nil {
t.Fatal("audit mode without a scope provider must error")
}
}
// --- (b) audit denies nothing ------------------------------------------
// TestAudit_DeniesNothing_ButLogsWouldDenies is the core safety property.
// An identity with NO grant entry — the worst case, the one enforcement
// would reject outright — still gets the inner authorizer's Allow.
func TestAudit_DeniesNothing_ButLogsWouldDenies(t *testing.T) {
node := testNodeID(t, 0x11)
inner := &fakeAuthorizer{decision: Allow("validator-read")}
lg := newCapturingLogger()
az, err := WrapScopeAuthorizer(inner, ScopeAuthorizerConfig{
Mode: AuthzModeAudit,
Scopes: NewStaticScopeProvider(map[ids.NodeID]Grants{}), // nobody granted anything
Logger: lg,
})
if err != nil {
t.Fatalf("WrapScopeAuthorizer: %v", err)
}
for _, path := range []string{"llm-secrets", "bootnode", "anything/at/all"} {
got, err := az.Authorize(context.Background(), Identity{NodeID: node, ServicePath: "hanzo/enso-secrets"}, path, OpAuthGet)
if err != nil {
t.Fatalf("audit mode returned an error: %v", err)
}
if !got.Allow {
t.Fatalf("audit mode DENIED %q — audit must never change a decision: %+v", path, got)
}
if got.Reason != "validator-read" {
t.Fatalf("audit mode rewrote the reason: %q", got.Reason)
}
}
lines := lg.lines()
if len(lines) != 3 {
t.Fatalf("expected 3 would-deny lines, got %d: %v", len(lines), lines)
}
for _, l := range lines {
if !strings.Contains(l, "kms.authz would-deny") {
t.Errorf("unexpected log line: %s", l)
}
if !strings.Contains(l, "enforced=false") {
t.Errorf("audit line must state enforced=false: %s", l)
}
if !strings.Contains(l, "reason=no-scope-entry") {
t.Errorf("missing structured reason: %s", l)
}
if !strings.Contains(l, "dimensions=path-only") {
t.Errorf("audit line must disclose that it compares path only: %s", l)
}
}
}
// TestAudit_DoesNotSecondGuessAnInnerDeny — a request the role model
// already denies carries no scope signal. Logging it would bury the
// signal that matters (requests that pass today and would stop passing).
func TestAudit_DoesNotSecondGuessAnInnerDeny(t *testing.T) {
inner := &fakeAuthorizer{decision: Deny("not-a-validator")}
lg := newCapturingLogger()
az, err := WrapScopeAuthorizer(inner, ScopeAuthorizerConfig{
Mode: AuthzModeAudit,
Scopes: NewStaticScopeProvider(map[ids.NodeID]Grants{}),
Logger: lg,
})
if err != nil {
t.Fatalf("WrapScopeAuthorizer: %v", err)
}
got, err := az.Authorize(context.Background(), Identity{NodeID: testNodeID(t, 0x22)}, "x", OpAuthGet)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Allow {
t.Fatal("audit mode must not upgrade an inner Deny to Allow")
}
if got.Reason != "not-a-validator" {
t.Fatalf("inner deny reason rewritten: %q", got.Reason)
}
if n := len(lg.lines()); n != 0 {
t.Fatalf("audit logged %d lines for an already-denied request: %v", n, lg.lines())
}
}
// TestAudit_PropagatesInnerError — a provider outage must surface
// unchanged; audit must not swallow it into an Allow.
func TestAudit_PropagatesInnerError(t *testing.T) {
inner := &fakeAuthorizer{decision: Deny("validator-authority-unreachable"), err: context.DeadlineExceeded}
lg := newCapturingLogger()
az, _ := WrapScopeAuthorizer(inner, ScopeAuthorizerConfig{
Mode: AuthzModeAudit,
Scopes: NewStaticScopeProvider(map[ids.NodeID]Grants{}),
Logger: lg,
})
got, err := az.Authorize(context.Background(), Identity{NodeID: testNodeID(t, 0x33)}, "x", OpAuthGet)
if err == nil {
t.Fatal("inner error must propagate")
}
if got.Allow {
t.Fatal("audit mode must not turn a fail-closed deny into an allow")
}
}
// --- (c) the grant SET is honoured -------------------------------------
// TestAudit_ManyToManyIdentity_NoWouldDenyOnAnyGrantedPath — the
// hanzo-platform shape. An identity holding many unrelated paths must
// produce ZERO would-denies across all of them. A scalar scope would flag
// all but one, which is exactly the lockout preview this gate exists to
// avoid mispredicting.
func TestAudit_ManyToManyIdentity_NoWouldDenyOnAnyGrantedPath(t *testing.T) {
node := testNodeID(t, 0x44)
paths := []string{
"admin-guard-secrets", "adnexus", "argocd/repo-ssh", "auth",
"bootnode", "bootnode-secrets", "bot", "bot-browser",
"buildx-ghcr-auth", "chat-guest-key", "platform", "storage",
}
grants := make([]Grant, 0, len(paths))
for _, p := range paths {
grants = append(grants, Grant{Org: "hanzo", Env: "prod", Path: p})
}
lg := newCapturingLogger()
az, err := WrapScopeAuthorizer(&fakeAuthorizer{decision: Allow("validator-read")}, ScopeAuthorizerConfig{
Mode: AuthzModeAudit,
Scopes: NewStaticScopeProvider(map[ids.NodeID]Grants{node: {Grants: grants}}),
Logger: lg,
})
if err != nil {
t.Fatalf("WrapScopeAuthorizer: %v", err)
}
for _, p := range paths {
if _, err := az.Authorize(context.Background(), Identity{NodeID: node}, "/"+p+"/", OpAuthGet); err != nil {
t.Fatalf("%s: %v", p, err)
}
}
if lines := lg.lines(); len(lines) != 0 {
t.Fatalf("granted paths produced %d would-denies (a scalar scope would produce %d): %v",
len(lines), len(paths)-1, lines)
}
// A path outside the set IS flagged — the overlay is doing real work.
if _, err := az.Authorize(context.Background(), Identity{NodeID: node}, "iam", OpAuthGet); err != nil {
t.Fatalf("unexpected error: %v", err)
}
lines := lg.lines()
if len(lines) != 1 || !strings.Contains(lines[0], "reason=path-outside-grant-set") {
t.Fatalf("ungranted path was not flagged: %v", lines)
}
}
// TestPathWithinGrant_SegmentBoundary — prefix matching without a
// segment-boundary check silently authorizes a SIBLING. Both of these
// are live production paths, so this is not a synthetic edge case.
func TestPathWithinGrant_SegmentBoundary(t *testing.T) {
for _, c := range []struct {
want, grant string
ok bool
}{
{"bootnode", "bootnode", true},
{"bootnode/sub", "bootnode", true},
{"bootnode-secrets", "bootnode", false}, // sibling, NOT a descendant
{"bootnodex", "bootnode", false},
{"argocd/repo-ssh", "argocd", true},
{"anything", "", true}, // org root grant covers everything beneath it
{"", "", true},
{"", "bootnode", false},
} {
if got := pathWithinGrant(c.want, c.grant); got != c.ok {
t.Errorf("pathWithinGrant(%q, %q) = %v, want %v", c.want, c.grant, got, c.ok)
}
}
}
// TestAudit_UnconfinedIdentityIsNeverFlagged — the kms-operator writes on
// every service's behalf; confining it would confine everything.
func TestAudit_UnconfinedIdentityIsNeverFlagged(t *testing.T) {
node := testNodeID(t, 0x55)
lg := newCapturingLogger()
az, _ := WrapScopeAuthorizer(&fakeAuthorizer{decision: Allow("operator-write")}, ScopeAuthorizerConfig{
Mode: AuthzModeAudit,
Scopes: NewStaticScopeProvider(map[ids.NodeID]Grants{node: {Unconfined: true}}),
Logger: lg,
})
for _, p := range []string{"a", "b/c", ""} {
if _, err := az.Authorize(context.Background(), Identity{NodeID: node}, p, OpAuthPut); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
if lines := lg.lines(); len(lines) != 0 {
t.Fatalf("unconfined identity flagged: %v", lines)
}
}
// TestAudit_ScopedZeroGrantsIsDenyAll — an EXPLICIT empty grant set must
// read as "granted nothing", never as "granted everything". The
// distinction between this and Unconfined is the whole fail-closed
// design.
func TestAudit_ScopedZeroGrantsIsDenyAll(t *testing.T) {
node := testNodeID(t, 0x66)
lg := newCapturingLogger()
az, _ := WrapScopeAuthorizer(&fakeAuthorizer{decision: Allow("validator-read")}, ScopeAuthorizerConfig{
Mode: AuthzModeAudit,
Scopes: NewStaticScopeProvider(map[ids.NodeID]Grants{node: {Grants: []Grant{}}}),
Logger: lg,
})
got, _ := az.Authorize(context.Background(), Identity{NodeID: node}, "anything", OpAuthGet)
if !got.Allow {
t.Fatal("audit must still allow")
}
lines := lg.lines()
if len(lines) != 1 || !strings.Contains(lines[0], "reason=scoped-zero-grants") {
t.Fatalf("empty grant set not reported as deny-all: %v", lines)
}
}
// TestGrants_WireShapeMatchesOperator pins the cross-repo contract. These
// bytes are exactly what hanzoai/kms-operator's bootstrap.Snapshot
// marshals (see its scopes_test.go golden assertions). If either side
// drifts, this fails rather than silently producing an overlay the other
// side reads as empty — which under enforcement is a lockout.
func TestGrants_WireShapeMatchesOperator(t *testing.T) {
const golden = `{"unconfined":true,"grants":[]}`
var unconfined Grants
if err := json.Unmarshal([]byte(golden), &unconfined); err != nil {
t.Fatalf("unmarshal operator wire form: %v", err)
}
if !unconfined.Unconfined || len(unconfined.Grants) != 0 {
t.Fatalf("operator unconfined form misread: %+v", unconfined)
}
const goldenScoped = `{"grants":[{"org":"hanzo","env":"prod","path":"bootnode"},{"org":"hanzo","env":"prod","path":"bootnode-secrets"}]}`
var scoped Grants
if err := json.Unmarshal([]byte(goldenScoped), &scoped); err != nil {
t.Fatalf("unmarshal operator scoped form: %v", err)
}
if scoped.Unconfined {
t.Fatal("scoped form must not decode as unconfined")
}
if len(scoped.Grants) != 2 {
t.Fatalf("grant set lost entries: %+v", scoped.Grants)
}
if scoped.Grants[0] != (Grant{Org: "hanzo", Env: "prod", Path: "bootnode"}) {
t.Fatalf("grant fields misaligned: %+v", scoped.Grants[0])
}
// The deny-all form must NOT decode as unconfined.
const goldenEmpty = `{"grants":[]}`
var empty Grants
if err := json.Unmarshal([]byte(goldenEmpty), &empty); err != nil {
t.Fatalf("unmarshal empty: %v", err)
}
if empty.Unconfined {
t.Fatal("deny-all form decoded as unconfined — fail-open")
}
}