mesh/merge-gate head 48f0662fdea2 merged into trunk 5ce2d07c7f47 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head 48f0662fdea2 merged into trunk 5ce2d07c7f47 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The tools' token reached everything the administrator may do, so their boundary rested on their own guards. The gitea module now makes a plain account, mesh-tools, with the admin login, gives it write on code, issues and pull requests of every novox repository through one team, and mints the tools' token for it; the admin's old token goes only after the new one is kept. The Go forge bundle sends that token, never the admin login. The forge shows protection rules only to a repository administrator, so every check reads a branch's protection from the branch. mesh-admin stays a ready author, so marks made before the move keep counting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
4.7 KiB
Go
121 lines
4.7 KiB
Go
package main
|
|
|
|
// The commit and the reads act as the tools' own account (novox/hq ADR 0365 §2, issue 469): the token the module
|
|
// keeps for `mesh-tools`, never the admin login. That account is no repository's administrator, so the forge
|
|
// refuses it the protection rules (403, as gitea's reqAdmin does); the commit then judges an existing branch by the
|
|
// protection the forge says it applies, and leaves a branch not made yet to the forge's own judgement.
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea/internal/forgehttp"
|
|
)
|
|
|
|
const toolsToken = "tools-token-never-shown"
|
|
|
|
// asTools serves the fake forge to a bundle holding the tools' token: only that token is taken, the rules are
|
|
// refused as to a non-administrator, and the branches named protected say so as the forge does.
|
|
func asTools(t *testing.T, f *fakeForge, protected map[string]string, scopes string) (*Forge, *[]string) {
|
|
dir := t.TempDir()
|
|
file := filepath.Join(dir, "token")
|
|
if err := os.WriteFile(file, []byte(toolsToken+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(file+".scopes", []byte(scopes+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var seen []string
|
|
const repo = "/api/v1/repos/acme/app"
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
seen = append(seen, r.Method+" "+r.URL.Path)
|
|
if _, _, basic := r.BasicAuth(); basic || r.Header.Get("Authorization") != "token "+toolsToken {
|
|
http.Error(w, "unauthorized", 401)
|
|
return
|
|
}
|
|
if r.URL.Path == repo+"/branch_protections" {
|
|
http.Error(w, `{"message":"token does not have the administration of this repository"}`, 403)
|
|
return
|
|
}
|
|
if name := strings.TrimPrefix(r.URL.Path, repo+"/branches/"); r.Method == "GET" && name != r.URL.Path {
|
|
if rule, ok := protected[name]; ok {
|
|
_, _ = w.Write([]byte(`{"name":"` + name + `","commit":{"id":"c0"},"protected":true,"effective_branch_protection_name":"` + rule + `"}`))
|
|
return
|
|
}
|
|
}
|
|
r.SetBasicAuth("mesh-admin", f.password) // the fake's own door, past the token checked above
|
|
f.handle(w, r)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return &Forge{Client: forgehttp.Client{URL: srv.URL, TokenFile: file, Account: "mesh-tools"}}, &seen
|
|
}
|
|
|
|
func TestTheToolsAccountCommitsOnANewBranchWithItsTokenThoughTheRulesAreNotItsToRead(t *testing.T) {
|
|
f := newFake(t)
|
|
forge, _ := asTools(t, f, nil, "read:user,write:issue,write:repository for mesh-tools")
|
|
got, err := commit(context.Background(), forge, ask(t, map[string]any{"branch": "fix/365",
|
|
"files": filesArg(map[string]any{"path": "x", "content": "x"})}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(f.writes) != 1 || !got.Created {
|
|
t.Fatalf("%d writes, %+v", len(f.writes), got)
|
|
}
|
|
}
|
|
|
|
func TestTheToolsAccountIsRefusedAProtectedBranchTheForgeSaysItGuardsAndTheDefaultBranch(t *testing.T) {
|
|
for branch, rule := range map[string]string{"release/1.0": "release/*", "fix/1-old": "fix/1-old", "main": ""} {
|
|
f := newFake(t)
|
|
protected := map[string]string{}
|
|
if rule != "" {
|
|
protected[branch] = rule
|
|
}
|
|
forge, _ := asTools(t, f, protected, "read:user,write:issue,write:repository for mesh-tools")
|
|
_, err := commit(context.Background(), forge, ask(t, map[string]any{"branch": branch,
|
|
"files": filesArg(map[string]any{"path": "x", "content": "x"})}))
|
|
if err == nil || !strings.Contains(err.Error(), "refused") || !strings.Contains(err.Error(), "ADR 0266") {
|
|
t.Errorf("%s: %v", branch, err)
|
|
}
|
|
if rule != "" && !strings.Contains(err.Error(), rule) {
|
|
t.Errorf("%s: the rule %s is not named: %v", branch, rule, err)
|
|
}
|
|
if len(f.writes)+len(f.patches) != 0 {
|
|
t.Errorf("%s: something was written", branch)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestATokenKeptForTheAdministratorIsNeverSent(t *testing.T) {
|
|
f := newFake(t)
|
|
// What the module kept before ADR 0365: the admin's token, its scopes line naming no account.
|
|
forge, seen := asTools(t, f, nil, "read:user,write:issue,write:repository")
|
|
_, err := commit(context.Background(), forge, ask(t, map[string]any{"branch": "fix/365",
|
|
"files": filesArg(map[string]any{"path": "x", "content": "x"})}))
|
|
if err == nil || !strings.Contains(err.Error(), "not mesh-tools's") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
if len(*seen) != 0 {
|
|
t.Fatalf("the forge was asked %v", *seen)
|
|
}
|
|
}
|
|
|
|
func TestTheBundleHandedATokenFileNeedsNoAdminLogin(t *testing.T) {
|
|
t.Setenv("MESH_GITEA_URL", "http://127.0.0.1:3000")
|
|
t.Setenv("MESH_GITEA_TOKEN_FILE", "/state/token")
|
|
t.Setenv("MESH_GITEA_TOOLS_USER", "")
|
|
t.Setenv("MESH_GITEA_ADMIN_USER", "")
|
|
t.Setenv("MESH_GITEA_ADMIN_PASSWORD_FILE", "")
|
|
f, err := ForgeFromEnv()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f.TokenFile != "/state/token" || f.Account != "mesh-tools" || f.User != "" || f.PasswordFile != "" {
|
|
t.Fatalf("%+v", f.Client)
|
|
}
|
|
}
|