M-1: a licence's key form, and an adoption whose key is taken is refused

A key of the bus takes fewer characters than a licence's name; the key is the store's
alone, and two names that write the same key would overwrite each other.

Builds: design 39 bus-and-switch §10, B6

Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
2026-11-11 22:58:52 +01:00
parent 9dfb2eae0e
commit e995e40519
3 changed files with 141 additions and 0 deletions
@@ -0,0 +1,41 @@
package main
// A licence's key form (design 39 bus-and-switch §10, B6): the name every verb and tool takes and answers, written
// in the characters a key of the bus takes. The key is the store's alone.
import (
"context"
"strings"
)
// KeyFor is a licence's key: its name in lower case, each `@` written as `_at_`, and every other character
// outside letters, digits, `-` and `.` written as `_`.
func KeyFor(name string) string {
var b strings.Builder
for _, r := range strings.ToLower(name) {
switch {
case r == '@':
b.WriteString("_at_")
case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '-', r == '.':
b.WriteRune(r)
default:
b.WriteByte('_')
}
}
return b.String()
}
// keyTaken names the licence other than name whose key is name's, or "" when none is.
func (m *Manager) keyTaken(ctx context.Context, name string) (string, error) {
all, err := m.Store.Licences(ctx)
if err != nil {
return "", err
}
key := KeyFor(name)
for _, l := range all {
if l.Name != name && KeyFor(l.Name) == key {
return l.Name, nil
}
}
return "", nil
}
@@ -0,0 +1,72 @@
package main
import (
"context"
"strings"
"testing"
"time"
)
// design 39 bus-and-switch §10, B6.
func TestALicencesKeyForm(t *testing.T) {
for name, key := range map[string]string{
"jane@example.org": "jane_at_example.org",
"Jane.Doe@Example.ORG": "jane.doe_at_example.org",
"0c1d2e3f-4a5b-6c7d-8e9f-a0b1c2d3e4f5": "0c1d2e3f-4a5b-6c7d-8e9f-a0b1c2d3e4f5",
"team key_1": "team_key_1",
"a+b/c:d": "a_b_c_d",
"ops-key": "ops-key",
} {
if got := KeyFor(name); got != key {
t.Errorf("KeyFor(%q) = %q, want %q", name, got, key)
}
}
}
// design 39 bus-and-switch §10: an adoption whose key another licence has is refused, in the audit and as an event.
func TestAnAdoptionWhoseKeyIsTakenIsRefused(t *testing.T) {
ctx := context.Background()
t.Run("an API key", func(t *testing.T) {
mm := newMesh(t)
_ = mm.store.SaveLicence(ctx, Licence{Name: "Ops-Key", Kind: "api-key"})
_, err := mm.m.adoptKey(ctx, "ops-key", "sk-ant-"+strings.Repeat("a", 40), "a test")
if err == nil || !strings.Contains(err.Error(), "Ops-Key") {
t.Fatalf("adopted a key whose key form is taken: %v", err)
}
if l, _ := mm.store.Licence(ctx, "ops-key"); l != nil {
t.Fatal("the clashing licence was kept")
}
assertRefused(t, mm)
})
t.Run("a login", func(t *testing.T) {
mm := newMesh(t)
_ = mm.store.SaveLicence(ctx, Licence{Name: "ACCT-1@example.org", Kind: "api-key"})
r := mm.login("laptop", "rt-laptop", true, t0.Add(-time.Minute))
adopted, err := mm.m.Consider(ctx, []Holdings{r})
if err != nil || len(adopted) != 0 {
t.Fatalf("adopted %v (%v), want none", adopted, err)
}
if len(mm.vendor.exchanged) != 0 {
t.Fatalf("a refused login was exchanged: %v", mm.vendor.exchanged)
}
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-laptop")); o != Refused {
t.Fatalf("outcome %q, want refused", o)
}
assertRefused(t, mm)
})
}
func assertRefused(t *testing.T, mm *miniMesh) {
t.Helper()
said := false
for _, e := range mm.events {
said = said || strings.HasPrefix(e, "licence.refused ")
}
audited := false
for _, a := range mm.store.Audits {
audited = audited || a["what"] == "refused"
}
if !said || !audited {
t.Fatalf("refusal said %v, audited %v: %v", said, audited, mm.events)
}
}
@@ -348,6 +348,24 @@ func (m *Manager) adoptOne(ctx context.Context, c Candidate, reports []Holdings)
return "", nil
}
// A new licence whose key another licence already has is refused before its grant is exchanged
// (design 39 bus-and-switch §10).
if held, err := m.Store.LicenceForAccount(ctx, c.Identity.AccountUUID); err != nil {
return "", err
} else if held == nil {
taken, err := m.keyTaken(ctx, NameFor(c.Identity))
if err != nil {
return "", err
}
if taken != "" {
why := "its key " + KeyFor(NameFor(c.Identity)) + " is the key of the licence " + taken
_ = m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Refused, why)
_ = m.Store.Audit(ctx, "refused", map[string]any{"node": c.Node, "licence": NameFor(c.Identity), "reason": why})
_ = m.Emit("licence.refused", map[string]any{"node": c.Node, "account": NameFor(c.Identity), "reason": why})
return "", nil
}
}
// Adopting is refreshing (ADR 0206 §4): the exchange is the check, and from here this module is the
// only holder of a live refresh token for the account.
r := m.Vendor.Refresh(ctx, offered)
@@ -751,6 +769,16 @@ func (m *Manager) adoptKey(ctx context.Context, name, key, from string) (map[str
if held != nil && held.Kind != "api-key" {
return nil, fmt.Errorf("%s is a subscription; an API key needs a name of its own", name)
}
taken, err := m.keyTaken(ctx, name)
if err != nil {
return nil, err
}
if taken != "" {
why := "its key " + KeyFor(name) + " is the key of the licence " + taken
_ = m.Store.Audit(ctx, "refused", map[string]any{"licence": name, "kind": "api-key", "from": from, "reason": why})
_ = m.Emit("licence.refused", map[string]any{"licence": name, "from": from, "reason": why})
return nil, fmt.Errorf("%s is refused: %s", name, why)
}
l := Licence{Name: name, Kind: "api-key", Sealed: m.Crypt.Seal(key), AdoptedAt: m.Now().UnixMilli()}
if err := m.Store.SaveLicence(ctx, l); err != nil {
return nil, err