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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user