mirror of
https://github.com/hanzoai/namespace.git
synced 2026-09-30 22:57:59 +00:00
Add Of: the namespace a billing subject owns
An account's subject (hanzoai/account Account.Subject) already names "org or user" — a bare slug for an org, "<org>/<name>" for a person or project — and it is what the ledger keys on. Deriving the namespace from that one string means the wallet and the database cannot disagree: a request that bills hanzo/alice reads and writes hanzo/alice's data by construction. The alternative is a table mapping each account kind to a namespace kind, which is one more place for the two vocabularies to drift. The org half is kept via the existing group, so acme/alice and globex/alice are different namespaces — and therefore different databases — and a person's namespace differs from their org's, which is what stops every personal account in the signup org from sharing one. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2026, Hanzo AI. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package namespace
|
||||
|
||||
import "strings"
|
||||
|
||||
// Of returns the namespace naming the data that belongs to subject — the key an
|
||||
// account holds within its ledger (github.com/hanzoai/account Account.Subject):
|
||||
// a bare slug for an org, "<org>/<name>" for a person or a project.
|
||||
//
|
||||
// It is a total function of that one string, deliberately. The alternative is a
|
||||
// table mapping each account kind to a namespace kind, and a table is somewhere
|
||||
// the two vocabularies can drift; deriving both the wallet and the database from
|
||||
// the same value means they cannot. If a request bills "hanzo/alice" it reads and
|
||||
// writes hanzo/alice's data, by construction rather than by agreement.
|
||||
//
|
||||
// The org half is kept rather than collapsing a person to a bare name, so
|
||||
// "acme/alice" and "globex/alice" stay different namespaces — and therefore
|
||||
// different databases — even where two orgs use the same username.
|
||||
//
|
||||
// Validation is inherited: the org must be a legal id and the name a legal
|
||||
// group, so a subject with a path separator, a traversal, or an oversized
|
||||
// segment is rejected here rather than reaching a filesystem or a key.
|
||||
func Of(subject string) (Namespace, error) {
|
||||
org, name, hasName := strings.Cut(subject, "/")
|
||||
|
||||
ns, err := Org(org)
|
||||
if err != nil {
|
||||
return Namespace{}, err
|
||||
}
|
||||
if !hasName {
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
g, err := NewGroup(name)
|
||||
if err != nil {
|
||||
return Namespace{}, err
|
||||
}
|
||||
return ns.WithGroup(g), nil
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Copyright (C) 2026, Hanzo AI. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package namespace
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestOfOrgSubject(t *testing.T) {
|
||||
ns, err := Of("acme")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ns.Kind() != KindOrg || ns.ID() != "acme" || ns.Group().String() != "" {
|
||||
t.Fatalf("got %q", ns)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfPersonSubjectKeepsBothHalves(t *testing.T) {
|
||||
ns, err := Of("hanzo/alice")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ns.ID() != "hanzo" || ns.Group().String() != "alice" {
|
||||
t.Fatalf("got id=%q group=%q", ns.ID(), ns.Group())
|
||||
}
|
||||
}
|
||||
|
||||
// Two orgs may use the same username; collapsing to a bare name would put both
|
||||
// in one database.
|
||||
func TestOfKeepsUsersInDifferentOrgsApart(t *testing.T) {
|
||||
a, err := Of("acme/alice")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := Of("globex/alice")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.String() == b.String() {
|
||||
t.Fatalf("acme/alice and globex/alice share a namespace: %q", a)
|
||||
}
|
||||
}
|
||||
|
||||
// A person's namespace must differ from their org's, or every personal account
|
||||
// in the signup org shares one database.
|
||||
func TestOfPersonDiffersFromItsOrg(t *testing.T) {
|
||||
person, _ := Of("hanzo/alice")
|
||||
org, _ := Of("hanzo")
|
||||
if person.String() == org.String() {
|
||||
t.Fatalf("person and org share a namespace: %q", org)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfRejectsBadSubjects(t *testing.T) {
|
||||
for _, s := range []string{"", "/", "acme/", "/alice", "acme/alice/extra", "../etc", "acme/../etc", "a b"} {
|
||||
if ns, err := Of(s); err == nil {
|
||||
t.Fatalf("Of(%q) = %q, want an error", s, ns)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user