package main import ( "crypto/rand" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" "regexp" "slices" "strings" "sync" "testing" "golang.org/x/crypto/nacl/box" "gitea/internal/forgehttp" ) const adminPassword = "admin-password-never-said" // fakeForge is gitea's API as far as this bundle uses it, as the administrator's login reaches it. type fakeForge struct { mu sync.Mutex users map[string]*forgeUser tokens map[string][]forgeToken next int64 calls []string made map[string]map[string]any minted map[string][]string // account -> scopes of its last mint failMint bool } func newFakeForge() *fakeForge { return &fakeForge{users: map[string]*forgeUser{}, tokens: map[string][]forgeToken{}, made: map[string]map[string]any{}, minted: map[string][]string{}, next: 1} } var tokenPath = regexp.MustCompile(`^/api/v1/users/([^/]+)/tokens(?:/(\d+))?$`) func (f *fakeForge) ServeHTTP(w http.ResponseWriter, r *http.Request) { f.mu.Lock() defer f.mu.Unlock() user, pass, ok := r.BasicAuth() if !ok || user != "mesh-admin" || pass != adminPassword { http.Error(w, "unauthorized", http.StatusUnauthorized) return } f.calls = append(f.calls, r.Method+" "+r.URL.Path) body, _ := io.ReadAll(r.Body) var in map[string]any _ = json.Unmarshal(body, &in) reply := func(status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if v != nil { _ = json.NewEncoder(w).Encode(v) } } switch { case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v1/users/") && strings.Count(r.URL.Path, "/") == 4: u, ok := f.users[strings.TrimPrefix(r.URL.Path, "/api/v1/users/")] if !ok { reply(404, map[string]any{"message": "user does not exist"}) return } reply(200, map[string]any{"is_admin": u.IsAdmin, "active": u.Active, "prohibit_login": u.ProhibitLogin}) case r.Method == http.MethodPost && r.URL.Path == "/api/v1/admin/users": name, _ := in["username"].(string) f.users[name] = &forgeUser{Active: true} f.made[name] = in reply(201, map[string]any{"login": name}) case r.Method == http.MethodPatch && strings.HasPrefix(r.URL.Path, "/api/v1/admin/users/"): u := f.users[strings.TrimPrefix(r.URL.Path, "/api/v1/admin/users/")] u.IsAdmin, _ = in["admin"].(bool) u.Active, _ = in["active"].(bool) u.ProhibitLogin, _ = in["prohibit_login"].(bool) reply(200, map[string]any{}) case tokenPath.MatchString(r.URL.Path): m := tokenPath.FindStringSubmatch(r.URL.Path) account := m[1] switch r.Method { case http.MethodGet: reply(200, f.tokens[account]) case http.MethodDelete: kept := f.tokens[account][:0] for _, t := range f.tokens[account] { if fmt.Sprint(t.ID) != m[2] { kept = append(kept, t) } } f.tokens[account] = kept reply(204, nil) case http.MethodPost: if f.failMint { reply(500, map[string]any{"message": "the forge is unwell"}) return } name, _ := in["name"].(string) for _, t := range f.tokens[account] { if t.Name == name { reply(400, map[string]any{"message": "access token name has been used already"}) return } } var scopes []string for _, s := range in["scopes"].([]any) { scopes = append(scopes, s.(string)) } f.minted[account] = scopes b := make([]byte, 20) _, _ = rand.Read(b) t := forgeToken{ID: f.next, Name: name, SHA1: fmt.Sprintf("%x", b)} f.next++ f.tokens[account] = append(f.tokens[account], forgeToken{ID: t.ID, Name: t.Name}) reply(201, t) } default: reply(404, map[string]any{"message": "not here: " + r.URL.Path}) } } type harness struct { forge *fakeForge server *httptest.Server minter *Minter public *[32]byte private *[32]byte logged *[]string } func newHarness(t *testing.T) harness { t.Helper() f := newFakeForge() srv := httptest.NewServer(f) t.Cleanup(srv.Close) dir := t.TempDir() pw := filepath.Join(dir, "admin.secret") origins := filepath.Join(dir, "origins") if err := os.WriteFile(pw, []byte(adminPassword+"\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(origins, []byte("https://git.example.test\n"), 0o644); err != nil { t.Fatal(err) } pub, priv, err := box.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } var logged []string m := &Minter{Forge: forgehttp.Client{URL: srv.URL, User: "mesh-admin", PasswordFile: pw}, Node: "anchor", OriginsFile: origins, Log: func(format string, a ...any) { logged = append(logged, fmt.Sprintf(format, a...)) }} return harness{forge: f, server: srv, minter: m, public: pub, private: priv, logged: &logged} } func (h harness) open(t *testing.T, sealed string) string { t.Helper() raw, err := base64.StdEncoding.DecodeString(sealed) if err != nil { t.Fatal(err) } out, ok := box.OpenAnonymous(nil, raw, h.public, h.private) if !ok { t.Fatal("a holder's value is not sealed to the call's key") } return string(out) } func (h harness) args(holders ...HolderAsk) map[string]any { list := []any{} for _, x := range holders { list = append(list, map[string]any{"node": x.Node, "module": x.Module}) } return map[string]any{"provision": Provision, "seal_to": base64.StdEncoding.EncodeToString(h.public[:]), "holders": list} } func TestOnlyTheVaultIsAnsweredAndNothingIsMintedForAnyoneElse(t *testing.T) { h := newHarness(t) for _, caller := range []string{"controller", "novox.node-tools", "provisioner.anchor", "anchor.claude-code", "mesh-vault", "anchor.mesh-vault.x", "", "unproven"} { if err := fromTheVault(caller); err == nil { t.Errorf("%q was taken for the vault", caller) } } if err := fromTheVault("novox.mesh-vault"); err != nil { t.Errorf("the vault was refused: %v", err) } // Through the tool as the runtime serves it: a call no runtime named a caller for is the vault's to nobody. tools := Tools(func() (*Minter, error) { return h.minter, nil }) if _, err := tools[0].Run(h.args(HolderAsk{"anchor", "claude-code"})); err == nil || !strings.Contains(err.Error(), "vault alone") { t.Fatalf("an unproven caller was answered: %v", err) } if len(h.forge.calls) != 0 { t.Fatalf("the forge was reached for a caller that is not the vault: %v", h.forge.calls) } } func TestEachAgentGetsAnAccountOfItsOwnAndAReadTokenSealedForItAlone(t *testing.T) { h := newHarness(t) ask, err := readAsk(h.args(HolderAsk{"anchor", "claude-code"}, HolderAsk{"laptop", "claude-code"}, HolderAsk{"laptop", "lab"})) if err != nil { t.Fatal(err) } got, err := h.minter.Answer(ask) if err != nil { t.Fatal(err) } if len(got.Holders) != 2 { t.Fatalf("answered %d holders, want the two agents: %+v", len(got.Holders), got) } if !strings.Contains(got.Skipped["lab on laptop"], Holder) { t.Errorf("a holder that is not an agent's module was not said: %v", got.Skipped) } lines := map[string]string{} for _, s := range got.Holders { lines[s.Node] = h.open(t, s.Sealed) } anchorTok := tokenIn(t, lines["anchor"], "agent-anchor") laptopTok := tokenIn(t, lines["laptop"], "agent-laptop") if anchorTok == laptopTok { t.Fatal("two agents were handed one token") } // The forge's own machine reaches it at its own address too; another machine only at its public origin. if !strings.Contains(lines["anchor"], "https://agent-anchor:"+anchorTok+"@git.example.test\n") || !strings.Contains(lines["anchor"], strings.Replace(h.server.URL, "http://", "http://agent-anchor:"+anchorTok+"@", 1)) { t.Errorf("anchor's credential lines: %q", strings.ReplaceAll(lines["anchor"], anchorTok, "")) } if lines["laptop"] != "https://agent-laptop:"+laptopTok+"@git.example.test\n" { t.Errorf("laptop's credential lines: %q", strings.ReplaceAll(lines["laptop"], laptopTok, "")) } for _, account := range []string{"agent-anchor", "agent-laptop"} { u := h.forge.users[account] if u == nil || u.IsAdmin { t.Errorf("%s: %+v, want a plain account", account, u) } if made := h.forge.made[account]; made["must_change_password"] != false || made["password"] == "" { t.Errorf("%s was made with %v", account, made) } if !slices.Equal(h.forge.minted[account], []string{"read:repository"}) { t.Errorf("%s's token carries %v, want read:repository alone", account, h.forge.minted[account]) } } for _, c := range h.forge.calls { if strings.Contains(c, "/teams") || strings.Contains(c, "/orgs/") || strings.Contains(c, "collaborators") { t.Errorf("an agent's account was given access (%s): it starts in no team, the operator grants (ADR 0365 ยง3)", c) } } if _, made := h.forge.users["agent-laptop-lab"]; made || len(h.forge.users) != 2 { t.Errorf("accounts made: %v", h.forge.users) } said := strings.Join(*h.logged, "\n") if strings.Contains(said, anchorTok) || strings.Contains(said, laptopTok) { t.Fatal("a token was said in the log") } } func TestAskedAgainTheTokenIsMintedAnewAndTheOneFromBeforeDeleted(t *testing.T) { h := newHarness(t) ask, _ := readAsk(h.args(HolderAsk{"laptop", "claude-code"})) first, err := h.minter.Answer(ask) if err != nil { t.Fatal(err) } h.forge.tokens["agent-laptop"] = append(h.forge.tokens["agent-laptop"], forgeToken{ID: 99, Name: "a-persons-own"}) again, err := h.minter.Answer(ask) if err != nil { t.Fatal(err) } if h.open(t, first.Holders[0].Sealed) == h.open(t, again.Holders[0].Sealed) { t.Fatal("asked again, the same token was answered: it was not rotated") } var names []string for _, tok := range h.forge.tokens["agent-laptop"] { names = append(names, tok.Name) } slices.Sort(names) if !slices.Equal(names, []string{"a-persons-own", TokenName}) { t.Fatalf("the account holds %v: the token from before is not deleted, or another was", names) } } func TestAnAccountMadeAnAdministratorByHandIsMadePlainAgain(t *testing.T) { h := newHarness(t) h.forge.users["agent-laptop"] = &forgeUser{IsAdmin: true, Active: true} ask, _ := readAsk(h.args(HolderAsk{"laptop", "claude-code"})) if _, err := h.minter.Answer(ask); err != nil { t.Fatal(err) } if h.forge.users["agent-laptop"].IsAdmin { t.Fatal("the agent's account is still an administrator") } } func TestAFailureSaysNeitherTheAdminsPasswordNorATokenAndAnswersNothing(t *testing.T) { h := newHarness(t) h.forge.failMint = true ask, _ := readAsk(h.args(HolderAsk{"anchor", "claude-code"}, HolderAsk{"laptop", "claude-code"})) got, err := h.minter.Answer(ask) if err == nil || len(got.Holders) != 0 { t.Fatalf("a failed mint answered %+v, %v", got, err) } if strings.Contains(err.Error(), adminPassword) { t.Fatal("the admin's password was said in an error") } // A wrong password is a refusal, said without it. if err := os.WriteFile(h.minter.Forge.PasswordFile, []byte("wrong"), 0o600); err != nil { t.Fatal(err) } h.forge.failMint = false if _, err := h.minter.Answer(ask); err == nil || strings.Contains(err.Error(), "wrong") { t.Fatalf("a refused login: %v", err) } } func TestTheAskIsReadStrictly(t *testing.T) { h := newHarness(t) for name, args := range map[string]map[string]any{ "another provision": {"provision": "git", "seal_to": base64.StdEncoding.EncodeToString(h.public[:]), "holders": []any{map[string]any{"node": "a", "module": "claude-code"}}}, "no key": {"provision": Provision, "holders": []any{map[string]any{"node": "a", "module": "claude-code"}}}, "short key": {"provision": Provision, "seal_to": "AAAA", "holders": []any{map[string]any{"node": "a", "module": "claude-code"}}}, "no holders": {"provision": Provision, "seal_to": base64.StdEncoding.EncodeToString(h.public[:])}, "a holder twice": {"provision": Provision, "seal_to": base64.StdEncoding.EncodeToString(h.public[:]), "holders": []any{map[string]any{"node": "a", "module": "claude-code"}, map[string]any{"node": "a", "module": "claude-code"}}}, } { if _, err := readAsk(args); err == nil { t.Errorf("%s was taken", name) } } } func TestTheReadmeNamesWhatTheBundleServes(t *testing.T) { var served []string for _, tool := range Tools(MinterFromEnv) { served = append(served, tool.Name) } if !slices.Equal(served, ToolNames) { t.Fatalf("served %v, named %v", served, ToolNames) } readme, err := os.ReadFile("../../README.md") if err != nil { t.Fatal(err) } for _, n := range ToolNames { if !strings.Contains(string(readme), "`"+n+"`") { t.Errorf("the README does not name %s", n) } } manifest, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } if !strings.Contains(string(manifest), `"tool": "`+ToolName+`"`) || !strings.Contains(string(manifest), `"name": "`+Provision+`"`) { t.Error("the manifest does not offer the provision with this tool as its issuer") } } // tokenIn is the token of account's credential lines, all one. func tokenIn(t *testing.T, lines, account string) string { t.Helper() re := regexp.MustCompile(regexp.QuoteMeta(account) + `:([0-9a-f]{40})@`) all := re.FindAllStringSubmatch(lines, -1) if len(all) == 0 { t.Fatalf("no credential for %s in the lines", account) } for _, m := range all { if m[1] != all[0][1] { t.Fatalf("%s's lines carry two tokens", account) } } return all[0][1] }