mirror of
https://github.com/hanzoai/namespace.git
synced 2026-09-30 22:57:59 +00:00
Carry the namespace on the context
A namespace names the entity a piece of work acts for, so it has to reach the store that opens a database or builds a key. Threading it through every call between the handler and the store means one missed parameter reads another entity's data. Every service needs this, so the carrier belongs here. A service-local copy is how two services end up disagreeing about what an unset namespace means: the zero value is not carried, an unset context reports ok=false rather than standing for everything, and spanning entities is the explicit system kind. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright (C) 2026, Hanzo AI. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package namespace
|
||||
|
||||
import "context"
|
||||
|
||||
// A namespace names the entity a piece of work acts for, so it has to travel
|
||||
// with that work: the request handler knows it, and the store that opens a
|
||||
// database or builds a key is where it has to be applied. Passing it through
|
||||
// every call between them means one missed parameter reads another entity's
|
||||
// data, so it rides the context instead.
|
||||
//
|
||||
// Every service does this, so the carrier lives here rather than being
|
||||
// reinvented per service — a service-local copy is how two services end up
|
||||
// disagreeing about what an unset namespace means.
|
||||
|
||||
type ctxKey struct{}
|
||||
|
||||
// NewContext returns a copy of ctx carrying ns. A zero namespace is not
|
||||
// carried: it names nothing, so storing it would turn "never set" into
|
||||
// "set to nothing" and hide the mistake at the point where it matters.
|
||||
func NewContext(ctx context.Context, ns Namespace) context.Context {
|
||||
if ns.IsZero() {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, ctxKey{}, ns)
|
||||
}
|
||||
|
||||
// FromContext returns the namespace ctx carries. ok is false when nothing set
|
||||
// one, which callers should treat as an error rather than a licence to read
|
||||
// everything — see IsSystem for work that legitimately spans entities.
|
||||
func FromContext(ctx context.Context) (ns Namespace, ok bool) {
|
||||
ns, ok = ctx.Value(ctxKey{}).(Namespace)
|
||||
return ns, ok && !ns.IsZero()
|
||||
}
|
||||
|
||||
// IsSystem reports whether ctx acts for the system namespace, which spans
|
||||
// every entity. Reserve it for work with no requesting user — schedulers,
|
||||
// sweeps, migrations — and never derive it from request input.
|
||||
func IsSystem(ctx context.Context) bool {
|
||||
ns, ok := FromContext(ctx)
|
||||
return ok && ns.Kind() == KindSystem
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (C) 2026, Hanzo AI. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package namespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContextRoundTrip(t *testing.T) {
|
||||
ns := MustOrg("acme")
|
||||
got, ok := FromContext(NewContext(context.Background(), ns))
|
||||
if !ok {
|
||||
t.Fatal("namespace did not survive the context")
|
||||
}
|
||||
if got.String() != ns.String() {
|
||||
t.Fatalf("got %q, want %q", got, ns)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing set means nothing set. Reporting ok here would let a caller treat an
|
||||
// unset namespace as permission to read every entity.
|
||||
func TestFromContextReportsUnset(t *testing.T) {
|
||||
if _, ok := FromContext(context.Background()); ok {
|
||||
t.Fatal("an unscoped context must report ok=false")
|
||||
}
|
||||
}
|
||||
|
||||
// Storing the zero namespace would turn "never set" into "set to nothing".
|
||||
func TestZeroNamespaceIsNotCarried(t *testing.T) {
|
||||
ctx := NewContext(context.Background(), Namespace{})
|
||||
if _, ok := FromContext(ctx); ok {
|
||||
t.Fatal("the zero namespace must not be carried")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReScopingReplaces(t *testing.T) {
|
||||
ctx := NewContext(NewContext(context.Background(), MustOrg("a")), MustOrg("b"))
|
||||
got, _ := FromContext(ctx)
|
||||
if got.ID() != "b" {
|
||||
t.Fatalf("got %q, want the inner scope to win", got.ID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSystem(t *testing.T) {
|
||||
if !IsSystem(NewContext(context.Background(), System())) {
|
||||
t.Fatal("system namespace must report IsSystem")
|
||||
}
|
||||
if IsSystem(NewContext(context.Background(), MustOrg("acme"))) {
|
||||
t.Fatal("an org namespace must not report IsSystem")
|
||||
}
|
||||
if IsSystem(context.Background()) {
|
||||
t.Fatal("an unscoped context must not report IsSystem")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user