package console // A run service at the run doors (service runs spec ยงยง 1, 2, 5): it starts // and follows its own runs of the commands it was granted, and nothing else. import ( "context" "encoding/json" "fmt" "go/ast" "go/parser" "go/token" "net/http" "os" "slices" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/runs" "github.com/carlosframework/platform/internal/seeds" "github.com/carlosframework/platform/internal/svcrole" ) const runServiceFor = "paul@example.com" // runServiceToken mints a run credential for app in the ops account. The // generic serviceToken cannot: a run credential needs its commands. func runServiceToken(t *testing.T, st *Store, name, app string, commands [][]string) string { t.Helper() raw, err := st.NewServicePrincipalScoped(OpsAccountID, name, app, svcrole.Run, nil, commands, "paul@keymail.dev", time.Now()) if err != nil { t.Fatalf("mint run %s: %v", name, err) } return raw } // serviceMeta is a granted command, said to be for runServiceFor. func serviceMeta() runs.Meta { return runs.Meta{Argv: []string{"import", "classic", "--slug", "x"}, TimeoutS: 60, OnBehalfOf: runServiceFor} } func codeOf(t *testing.T, body string) string { t.Helper() var e struct { Code *string `json:"code"` } if err := json.Unmarshal([]byte(body), &e); err != nil { t.Fatalf("body %q: %v", body, err) } if e.Code == nil { return "" } return *e.Code } // startAs admits one run for bearer and returns its id. func (fx *runFx) startAs(t *testing.T, bearer string, meta runs.Meta) string { t.Helper() status, body := postRun(t, fx.runsURL(runTestHost), bearer, meta, nil) if status != http.StatusAccepted { t.Fatalf("create: %d %s", status, body) } var c runs.Created if err := json.Unmarshal([]byte(body), &c); err != nil { t.Fatal(err) } return c.ID } // followAs sends one follow verb ("" is the GET) on id as bearer. func (fx *runFx) followAs(t *testing.T, bearer, verb, id string) (int, string) { t.Helper() u := fx.runsURL(runTestHost) + "/" + id if verb == "" { status, _, body := rawReq(t, http.MethodGet, u, bearer, "") return status, body } status, _, body := rawReq(t, http.MethodPost, u+"/"+verb, bearer, `{"last_seq":0}`) return status, body } // unknownAnswer is what bearer is told for verb on a run id that never // existed, rewritten to name id: the answer a run bearer may not see must be // indistinguishable from it. func (fx *runFx) unknownAnswer(t *testing.T, bearer, verb, id string) (int, string) { t.Helper() never := "run_zzzzzzzzzzzzzzzzzzzzzzzzzz" status, body := fx.followAs(t, bearer, verb, never) return status, strings.ReplaceAll(body, never, id) } func (fx *runFx) noRunObjects(t *testing.T, what string) { t.Helper() if left, _ := fx.rel.List(context.Background(), runs.RunsPrefix(release.OpsAccount, "shop")); len(left) != 0 { t.Errorf("%s left run objects %q", what, left) } if rec, _ := provision.GetRecord(context.Background(), fx.rel, release.OpsAccount, "shop", runTestHost); rec != nil && rec.ActiveRun != nil { t.Errorf("%s admitted a run: %+v", what, rec.ActiveRun) } } // runDoorURLs is the five run doors, filled for runTestHost and id. func (fx *runFx) runDoorURLs(id string) []struct{ method, u, body string } { base := fx.runsURL(runTestHost) return []struct{ method, u, body string }{ {http.MethodPost, base, ""}, {http.MethodGet, base + "/" + id, ""}, {http.MethodPost, base + "/" + id + "/cancel", `{}`}, {http.MethodPost, base + "/" + id + "/expire", `{}`}, {http.MethodPost, base + "/" + id + "/ack", `{"last_seq":0}`}, } } // TestRunRoleMatrix: the run doors take a run service and refuse every other // role, Admin included, with the role gate's own 403, which names the role // that opens them. func TestRunRoleMatrix(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) id := "run_aaaaaaaaaaaaaaaaaaaaaaaaaa" for _, d := range fx.runDoorURLs(id) { status, _, body := rawReq(t, d.method, d.u, runner, d.body) if status == http.StatusForbidden || status == http.StatusUnauthorized { t.Errorf("run service at %s %s = %d %s, want past the role gate", d.method, d.u, status, body) } } for _, r := range svcrole.All() { if r == svcrole.Run { continue } app, rungs := "shop", []string(nil) if r == svcrole.Admin { app = "" // the widest Admin there is } if r == svcrole.Deploy { rungs = []string{"edge"} } tok, err := fx.st.NewServicePrincipalScoped(OpsAccountID, "role-"+string(r), app, r, rungs, nil, "paul@keymail.dev", time.Now()) if err != nil { t.Fatalf("mint %s: %v", r, err) } raw, _ := json.Marshal(map[string]string{"error": fmt.Sprintf("this credential holds role %q; this door needs one of [run]", r)}) want := string(raw) + "\n" for _, d := range fx.runDoorURLs(id) { status, _, body := rawReq(t, d.method, d.u, tok, d.body) if status != http.StatusForbidden || body != want { t.Errorf("role %s at %s %s = %d %q, want 403 %q", r, d.method, d.u, status, body, want) } } } fx.noRunObjects(t, "the role matrix") } func TestRunServiceStartsAGrantedCommand(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) status, body := postRun(t, fx.runsURL(runTestHost), runner, serviceMeta(), nil) if status != http.StatusAccepted { t.Fatalf("create: %d %s", status, body) } var c runs.Created if err := json.Unmarshal([]byte(body), &c); err != nil || !runs.ValidID(c.ID) || c.State != provision.RunQueued { t.Fatalf("created %+v, %v", c, err) } ctx := context.Background() req, err := runs.ReadRequest(ctx, fx.rel, release.OpsAccount, "shop", c.ID) if err != nil { t.Fatal(err) } p, ok := fx.st.CLIPrincipal(runner, time.Now()) if !ok { t.Fatal("the run credential does not resolve") } if req.ByService != "ci" || req.ByCredential != p.CredentialID || req.ByCredential == "" || req.OnBehalfOf != runServiceFor { t.Fatalf("request by_service=%q by_credential=%q on_behalf_of=%q, want ci, %q, %s", req.ByService, req.ByCredential, req.OnBehalfOf, p.CredentialID, runServiceFor) } if want := "service:" + release.OpsAccount + "/ci"; req.By != want { t.Errorf("request by = %q, want the audit actor %q", req.By, want) } rec, err := provision.GetRecord(ctx, fx.rel, release.OpsAccount, "shop", runTestHost) if err != nil || rec.ActiveRun == nil || rec.ActiveRun.ID != c.ID { t.Fatalf("record run %+v, %v", rec.ActiveRun, err) } // It follows its own run. if p := fx.pageAs(t, runner, c.ID); p.ID != c.ID || p.State != provision.RunQueued { t.Fatalf("own page %+v", p) } // A scheduled run names nobody. fx.transition(t, provision.RunExpire(c.ID)) meta := serviceMeta() meta.OnBehalfOf = "" id := fx.startAs(t, runner, meta) if req, err := runs.ReadRequest(ctx, fx.rel, release.OpsAccount, "shop", id); err != nil || req.OnBehalfOf != "" || req.ByService != "ci" { t.Fatalf("scheduled request %+v, %v", req, err) } } func (fx *runFx) pageAs(t *testing.T, bearer, id string) runs.Page { t.Helper() status, body := fx.followAs(t, bearer, "", id) if status != http.StatusOK { t.Fatalf("GET page: %d %s", status, body) } var p runs.Page if err := json.Unmarshal([]byte(body), &p); err != nil { t.Fatal(err) } return p } func TestRunServiceRefusedAnUngrantedCommand(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) for _, argv := range [][]string{ {"db", "shell"}, {"import"}, // a missing word {"import", "classicx"}, // a substring of a word {"Import", "classic"}, // no case folding {"sync", "import", "classic"}, } { meta := serviceMeta() meta.Argv = argv status, body := postRun(t, fx.runsURL(runTestHost), runner, meta, nil) if status != http.StatusForbidden || codeOf(t, body) != "command_not_granted" { t.Errorf("%q: %d %s, want 403 command_not_granted", argv, status, body) } } fx.noRunObjects(t, "an ungranted command") // A person runs any command. meta := okMeta() meta.Argv = []string{"db", "shell"} if status, body := postRun(t, fx.runsURL(runTestHost), fx.member, meta, nil); status != http.StatusAccepted { t.Fatalf("a person's db shell: %d %s", status, body) } } func TestRunServiceWrongAppRefused(t *testing.T) { fx := runFixture(t) if _, err := fx.st.CreateAppIn(OpsAccountID, "other", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } other := runServiceToken(t, fx.st, "ci", "other", importClassic) want := canonicalNotFound(t) status, h, body := postRunH(t, fx.runsURL(runTestHost), other, serviceMeta(), nil) assertNotFound(t, "another app's run service at create", want, status, h, body) for _, d := range fx.runDoorURLs("run_aaaaaaaaaaaaaaaaaaaaaaaaaa")[1:] { status, h, body := rawReq(t, d.method, d.u, other, d.body) assertNotFound(t, "another app's run service at "+d.method+" "+d.u, want, status, h, body) } fx.noRunObjects(t, "another app's run service") } func TestRunRevokedServiceRefused(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) id := fx.startAs(t, runner, serviceMeta()) if err := fx.st.RevokeServicePrincipal(OpsAccountID, "ci", time.Now()); err != nil { t.Fatal(err) } // The 401 a fabricated token gets, byte for byte. _, _, want := rawReq(t, http.MethodGet, fx.ts.URL+"/api/cli/apps", "not-a-token", "") if status, got := postRun(t, fx.runsURL(runTestHost), runner, serviceMeta(), nil); status != http.StatusUnauthorized || got != want { t.Errorf("revoked at create = %d %q, want 401 %q", status, got, want) } for _, d := range fx.runDoorURLs(id)[1:] { status, _, got := rawReq(t, d.method, d.u, runner, d.body) if status != http.StatusUnauthorized || got != want { t.Errorf("revoked at %s %s = %d %q, want 401 %q", d.method, d.u, status, got, want) } } } func TestRunServiceCannotSeeAPersonsRun(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) id := fx.startRun(t) for _, verb := range []string{"", "cancel", "expire", "ack"} { wantStatus, want := fx.unknownAnswer(t, runner, verb, id) status, body := fx.followAs(t, runner, verb, id) if status != wantStatus || body != want || status != http.StatusNotFound { t.Errorf("service %q on a person's run = %d %q, want the unknown id's %d %q", verb, status, body, wantStatus, want) } } // Nothing the service sent touched the person's run. if rec, _ := provision.GetRecord(context.Background(), fx.rel, release.OpsAccount, "shop", runTestHost); rec.ActiveRun == nil || rec.ActiveRun.ID != id || rec.ActiveRun.State != provision.RunQueued { t.Fatalf("record %+v", rec.ActiveRun) } // The member still sees it, and sees a service's run too. if p := fx.page(t, id, 0); p.ID != id { t.Fatalf("member page %+v", p) } fx.transition(t, provision.RunExpire(id)) sid := fx.startAs(t, runner, serviceMeta()) if p := fx.page(t, sid, 0); p.ID != sid || p.State != provision.RunQueued { t.Fatalf("member's page of the service's run %+v", p) } } func TestRunRemintedNameDoesNotInheritRuns(t *testing.T) { fx := runFixture(t) first := runServiceToken(t, fx.st, "ci", "shop", importClassic) id := fx.startAs(t, first, serviceMeta()) if err := fx.st.RevokeServicePrincipal(OpsAccountID, "ci", time.Now()); err != nil { t.Fatal(err) } second := runServiceToken(t, fx.st, "ci", "shop", importClassic) for _, verb := range []string{"", "cancel", "expire", "ack"} { wantStatus, want := fx.unknownAnswer(t, second, verb, id) status, body := fx.followAs(t, second, verb, id) if status != wantStatus || body != want || status != http.StatusNotFound { t.Errorf("the reminted name's %q = %d %q, want the unknown id's %d %q", verb, status, body, wantStatus, want) } } // Rotation keeps the credential, and so its runs. third := runServiceToken(t, fx.st, "rotated", "shop", importClassic) fx.transition(t, provision.RunExpire(id)) rid := fx.startAs(t, third, serviceMeta()) rotated, err := fx.st.RotateServicePrincipal(OpsAccountID, "rotated", time.Now()) if err != nil { t.Fatal(err) } if p := fx.pageAs(t, rotated, rid); p.ID != rid { t.Fatalf("the rotated credential's own run %+v", p) } } func TestRunOpenRefusalRedactedForServices(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) id := fx.startRun(t) status, body := postRun(t, fx.runsURL(runTestHost), runner, serviceMeta(), nil) if status != http.StatusConflict || codeOf(t, body) != "run_open" { t.Fatalf("service create over a person's run: %d %s", status, body) } for _, leak := range []string{id, "alice@keymail.dev", "alice"} { if strings.Contains(body, leak) { t.Errorf("redacted refusal %s carries %q", body, leak) } } // Its own open run, it is told about. fx.transition(t, provision.RunExpire(id)) own := fx.startAs(t, runner, serviceMeta()) status, body = postRun(t, fx.runsURL(runTestHost), runner, serviceMeta(), nil) if status != http.StatusConflict || codeOf(t, body) != "run_open" || !strings.Contains(errorOf(t, body), own) { t.Fatalf("service create over its own run: %d %s, want run_open naming %s", status, body, own) } // Another service's run is redacted too. other := runServiceToken(t, fx.st, "ci2", "shop", importClassic) status, body = postRun(t, fx.runsURL(runTestHost), other, serviceMeta(), nil) if status != http.StatusConflict || codeOf(t, body) != "run_open" || strings.Contains(body, own) || strings.Contains(body, "service:") { t.Fatalf("service create over another service's run: %d %s", status, body) } // The member is told whose it is, in the house line: the service, and // the person only as the service's statement. status, body = postRun(t, fx.runsURL(runTestHost), fx.member, okMeta(), nil) if want := runTestHost + " already has a run open: " + own + ", started by service ci for " + runServiceFor + " (as stated by the service)"; status != http.StatusConflict || errorOf(t, body) != want { t.Fatalf("member over a service's run: %d %s, want %q", status, body, want) } } // The admission's own refusal is redacted the same way: the rival arrives // after the door's checks passed. func TestRunOpenRefusalAtAdmissionRedactedForServices(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) fx.srv.cfg.Releases = &rivalStore{Store: fx.rel, t: t} status, body := postRun(t, fx.runsURL(runTestHost), runner, serviceMeta(), nil) if status != http.StatusConflict || codeOf(t, body) != "run_open" || strings.Contains(body, "run_aaaaaaaaaaaaaaaaaaaaaaaaaa") || strings.Contains(body, "bob@keymail.dev") { t.Fatalf("%d %s", status, body) } } func TestRunPersonMaySetNoOnBehalfOf(t *testing.T) { fx := runFixture(t) meta := okMeta() meta.OnBehalfOf = "bob@keymail.dev" status, body := postRun(t, fx.runsURL(runTestHost), fx.member, meta, nil) if status != http.StatusBadRequest || codeOf(t, body) != "on_behalf_of_invalid" { t.Fatalf("%d %s, want 400 on_behalf_of_invalid", status, body) } fx.noRunObjects(t, "a person's on_behalf_of") } func TestRunOnBehalfOfShape(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) long := strings.Repeat("a", 255-len("@example.com")) + "@example.com" if len(long) != 255 { t.Fatalf("long is %d bytes", len(long)) } for _, v := range []string{"paul", "paul@", "@example.com", "Paul ", "paul@example.com, bob@example.com", " paul@example.com", "paul @example.com", "paul@example.com\n", long} { meta := serviceMeta() meta.OnBehalfOf = v status, body := postRun(t, fx.runsURL(runTestHost), runner, meta, nil) if status != http.StatusBadRequest || codeOf(t, body) != "on_behalf_of_invalid" { t.Errorf("%q: %d %s, want 400 on_behalf_of_invalid", v, status, body) } } fx.noRunObjects(t, "a malformed on_behalf_of") // 254 bytes is the most there may be. Membership is never asked. meta := serviceMeta() meta.OnBehalfOf = long[1:] if status, body := postRun(t, fx.runsURL(runTestHost), runner, meta, nil); status != http.StatusAccepted { t.Fatalf("254 bytes: %d %s", status, body) } } // TestRunRefusalsCarryCodes: every refusal a run handler writes has a code // in runRefusalCodes; the shared gates before the handler do not, and keep // their bodies as every other door gives them. func TestRunRefusalsCarryCodes(t *testing.T) { fx := runFixture(t) ctx := context.Background() runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) known := map[string]bool{} for _, c := range runRefusalCodes { if known[c] { t.Errorf("runRefusalCodes names %s twice", c) } known[c] = true } seen := map[string]bool{} check := func(what string, status int, body string) { t.Helper() if status < 400 { t.Errorf("%s: %d %s, want a refusal", what, status, body) return } code := codeOf(t, body) if !known[code] || errorOf(t, body) == "" { t.Errorf("%s: %d %s, want an error and a code from runRefusalCodes", what, status, body) } seen[code] = true } checkF := func(what string, f func() (int, string)) { t.Helper() status, body := f() check(what, status, body) } post := func(what, host, bearer string, meta runs.Meta) { t.Helper() status, body := postRun(t, fx.runsURL(host), bearer, meta, nil) check(what, status, body) } post("no record", "nobody.example.com", fx.member, okMeta()) post("bad host", "-bad-", fx.member, okMeta()) putShopRecord(t, fx.consoleFixture, provision.Record{Host: "static.example.com", Kind: "static"}) post("class", "static.example.com", fx.member, okMeta()) putShopRecord(t, fx.consoleFixture, provision.Record{Host: "import.example.com", Seed: &provision.SeedSource{Kind: seeds.KindImport, State: seeds.StatePlaced}}) post("import", "import.example.com", fx.member, okMeta()) post("meta", runTestHost, fx.member, runs.Meta{TimeoutS: 60}) post("files", runTestHost, fx.member, runs.Meta{Argv: []string{"a"}, TimeoutS: 60, Files: []runs.MetaFile{{Name: "a", Size: 65 << 20}}}) malformed := okMeta() malformed.Files = []runs.MetaFile{{Name: "x.csv", Size: 5}} post("malformed", runTestHost, fx.member, malformed) ungranted := serviceMeta() ungranted.Argv = []string{"db", "shell"} post("ungranted", runTestHost, runner, ungranted) person := okMeta() person.OnBehalfOf = "bob@keymail.dev" post("person on_behalf_of", runTestHost, fx.member, person) id := fx.startRun(t) post("open", runTestHost, fx.member, okMeta()) post("open, for a service", runTestHost, runner, serviceMeta()) checkF("unknown id", func() (int, string) { return fx.followAs(t, fx.member, "", "run_zzzzzzzzzzzzzzzzzzzzzzzzzz") }) checkF("not a service's run", func() (int, string) { return fx.followAs(t, runner, "", id) }) checkF("bad after", func() (int, string) { status, _, body := rawReq(t, http.MethodGet, fx.runsURL(runTestHost)+"/"+id+"?after=x", fx.member, "") return status, body }) checkF("ack before the end", func() (int, string) { return fx.followAs(t, fx.member, "ack", id) }) checkF("ack malformed", func() (int, string) { status, _, body := rawReq(t, http.MethodPost, fx.runsURL(runTestHost)+"/"+id+"/ack", fx.member, `not json`) return status, body }) fx.transition(t, provision.RunPick(id, "box-1", "boot-1", time.Now().Add(time.Hour))) checkF("expire a picked run", func() (int, string) { return fx.followAs(t, fx.member, "expire", id) }) if err := runs.WriteResult(ctx, fx.rel, release.OpsAccount, "shop", id, runs.Result{Exit: 0, LastSeq: 3}); err != nil { t.Fatal(err) } fx.transition(t, provision.RunFinish(id)) checkF("cancel an ended run", func() (int, string) { return fx.followAs(t, fx.member, "cancel", id) }) checkF("ack at the wrong seq", func() (int, string) { return fx.followAs(t, fx.member, "ack", id) }) fx.transition(t, provision.RunClear(id)) if _, err := provision.SetRecordMove(ctx, fx.rel, release.OpsAccount, "shop", runTestHost, provision.MoveRequest("uk", time.Now())); err != nil { t.Fatal(err) } post("moving", runTestHost, fx.member, okMeta()) fx.srv.cli.instances = newRateLimiter(1, cliRateWindow) fx.srv.cli.instances.allow(hashToken(fx.member), time.Now()) post("budget", runTestHost, fx.member, okMeta()) fx.srv.cli.runs = newRateLimiter(1, cliRateWindow) fx.srv.cli.runs.allow(hashToken(fx.member), time.Now()) checkF("follow budget", func() (int, string) { return fx.followAs(t, fx.member, "", id) }) fx.srv.cli = newCLILimits() fx.srv.cfg.HostClaim = true post("host claim", runTestHost, fx.member, okMeta()) checkF("host claim follow", func() (int, string) { return fx.followAs(t, fx.member, "", id) }) fx.srv.cfg.HostClaim = false for _, want := range []string{"not_found", "not_runnable", "import_uncommitted", "invalid_request", "files_too_large", "malformed", "command_not_granted", "on_behalf_of_invalid", "run_open", "run_not_finished", "run_started", "run_ended", "last_seq_mismatch", "moving", "rate_limited", "host_claim"} { if !seen[want] { t.Errorf("no refusal here carried %s; seen %v", want, seen) } } // The shared gates: no code, and the bodies every door gives. _, _, unsigned := rawReq(t, http.MethodGet, fx.ts.URL+"/api/cli/apps", "not-a-token", "") if status, body := postRun(t, fx.runsURL(runTestHost), "not-a-token", okMeta(), nil); status != http.StatusUnauthorized || body != unsigned || strings.Contains(body, "code") { t.Errorf("401 at the run door = %d %q, want %q", status, body, unsigned) } publish := serviceToken(t, fx.st, OpsAccountID, "pub", "shop", svcrole.Publish) if status, body := postRun(t, fx.runsURL(runTestHost), publish, okMeta(), nil); status != http.StatusForbidden || strings.Contains(body, "code") { t.Errorf("role 403 at the run door = %d %q, want no code", status, body) } stranger := cliToken(t, fx.consoleFixture, "mallory@keymail.dev", "code-run-codes-stranger") status, h, body := postRunH(t, fx.runsURL(runTestHost), stranger, okMeta(), nil) assertNotFound(t, "stranger", canonicalNotFound(t), status, h, body) status, h, body = rawReq(t, http.MethodGet, fx.runsURL(runTestHost)+"/"+id, stranger, "") assertNotFound(t, "stranger follow", canonicalNotFound(t), status, h, body) } // TestRunHandlersWriteOnlyCodedRefusals reads the run handlers' source: no // refusal there is an uncoded jsonError, and every code is a runCode // constant named in runRefusalCodes. The two http.NotFound calls are the // principal check, which answers as the membership gate does. func TestRunHandlersWriteOnlyCodedRefusals(t *testing.T) { fset := token.NewFileSet() consts := map[string]string{} for _, file := range []string{"apiruns.go", "apirunsfollow.go"} { f, err := parser.ParseFile(fset, file, nil, 0) if err != nil { t.Fatal(err) } ast.Inspect(f, func(n ast.Node) bool { switch n := n.(type) { case *ast.ValueSpec: for i, name := range n.Names { if strings.HasPrefix(name.Name, "runCode") && i < len(n.Values) { if lit, ok := n.Values[i].(*ast.BasicLit); ok { consts[name.Name] = strings.Trim(lit.Value, `"`) } } } case *ast.CallExpr: id, ok := n.Fun.(*ast.Ident) if !ok { return true } switch id.Name { case "jsonError": t.Errorf("%s: an uncoded jsonError in a run handler", fset.Position(n.Pos())) case "jsonErrorCode": if len(n.Args) != 4 { t.Errorf("%s: jsonErrorCode takes (w, status, code, msg)", fset.Position(n.Pos())) return true } switch a := n.Args[2].(type) { case *ast.Ident: if !strings.HasPrefix(a.Name, "runCode") { t.Errorf("%s: code %s is not a runCode constant", fset.Position(n.Pos()), a.Name) } case *ast.CallExpr: if f, ok := a.Fun.(*ast.Ident); !ok || f.Name != "runMetaRefusalCode" { t.Errorf("%s: code is computed by something other than runMetaRefusalCode", fset.Position(n.Pos())) } default: t.Errorf("%s: code is not a runCode constant", fset.Position(n.Pos())) } } } return true }) } if len(consts) != len(runRefusalCodes) { t.Errorf("%d runCode constants, %d in runRefusalCodes", len(consts), len(runRefusalCodes)) } for name, v := range consts { if !slices.Contains(runRefusalCodes, v) { t.Errorf("%s = %q is not in runRefusalCodes", name, v) } } for _, msg := range []string{runFilesTooLargeMsg, "anything else"} { if !slices.Contains(runRefusalCodes, runMetaRefusalCode(msg)) { t.Errorf("runMetaRefusalCode(%q) is not in runRefusalCodes", msg) } } } func TestServiceRunRendersAsTheServices(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) id := fx.startAs(t, runner, serviceMeta()) fx.transition(t, provision.RunPick(id, "box-1", "boot-1", time.Now().Add(time.Hour))) if err := runs.WriteResult(context.Background(), fx.rel, release.OpsAccount, "shop", id, runs.Result{Exit: 0}); err != nil { t.Fatal(err) } fx.pageAs(t, runner, id) // writes the run-end row line := "run by service ci for " + runServiceFor + " (as stated by the service)" events, _ := fx.st.Events(OpsAccountID, "shop", 20) rows := map[string]bool{} for _, e := range events { if !strings.HasPrefix(e.Action, "run-") { continue } rows[e.Action] = true if e.Address != "service:"+release.OpsAccount+"/ci" { t.Errorf("%s row's actor is %q, want the service", e.Action, e.Address) } if !strings.Contains(e.Detail, line) { t.Errorf("%s row %q does not say %q", e.Action, e.Detail, line) } if strings.Count(e.Detail, runServiceFor) != 1 { t.Errorf("%s row %q names %s outside the service's statement", e.Action, e.Detail, runServiceFor) } } if !rows["run-start"] || !rows["run-end"] { t.Fatalf("run rows %v in %+v", rows, events) } // The console's history. c := signInPaul(t, fx.consoleFixture) status, _, page := get(t, c, fx.u("/apps/shop/activity")) if status != http.StatusOK { t.Fatalf("activity: %d", status) } if !strings.Contains(page, line) { t.Errorf("the activity page does not say %q", line) } if strings.Contains(page, ""+runServiceFor+"") { t.Errorf("the activity page shows %s as the actor", runServiceFor) } // A scheduled run. fx.transition(t, provision.RunFinish(id)) fx.transition(t, provision.RunClear(id)) meta := serviceMeta() meta.OnBehalfOf = "" sid := fx.startAs(t, runner, meta) req, err := runs.ReadRequest(context.Background(), fx.rel, release.OpsAccount, "shop", sid) if err != nil { t.Fatal(err) } if got := req.ServiceLine(); got != "run by service ci (scheduled)" { t.Errorf("scheduled line %q", got) } // A person's run has no service line, and its rows are as before. if got := (runs.Request{By: "alice@keymail.dev"}).ServiceLine(); got != "" { t.Errorf("a person's run has a service line %q", got) } } // The run's incarnation is the record's when the service created it, so a // delete and create again leaves it naming the old account (the box refuses // it, RefusedIncarnation). func TestRunServiceRunIsBoundToItsIncarnation(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) id := fx.startAs(t, runner, serviceMeta()) ctx := context.Background() rec, err := provision.GetRecord(ctx, fx.rel, release.OpsAccount, "shop", runTestHost) if err != nil { t.Fatal(err) } req, err := runs.ReadRequest(ctx, fx.rel, release.OpsAccount, "shop", id) if err != nil { t.Fatal(err) } inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt) if err != nil || req.Incarnation != inc { t.Fatalf("request incarnation %q, record's %q, %v", req.Incarnation, inc, err) } // Deleted, and created again under the same name. if err := fx.rel.Delete(ctx, provision.RecordKey(release.OpsAccount, "shop", runTestHost)); err != nil { t.Fatal(err) } putShopRecord(t, fx.consoleFixture, provision.Record{Host: runTestHost, Channel: "stable", CreatedAt: rec.CreatedAt.Add(time.Hour)}) again, _ := provision.GetRecord(ctx, fx.rel, release.OpsAccount, "shop", runTestHost) if inc2, _ := hostclaim.Incarnation(again.Account, again.App, again.Host, again.CreatedAt); inc2 == req.Incarnation { t.Fatal("the recreated instance has the run's incarnation") } } func TestRunServiceRefusedWhileMoving(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) if _, err := provision.SetRecordMove(context.Background(), fx.rel, release.OpsAccount, "shop", runTestHost, provision.MoveRequest("uk", time.Now())); err != nil { t.Fatal(err) } status, body := postRun(t, fx.runsURL(runTestHost), runner, serviceMeta(), nil) if status != http.StatusConflict || codeOf(t, body) != "moving" || errorOf(t, body) != runTestHost+" is moving. run the command once the move has finished" { t.Fatalf("%d %s", status, body) } fx.noRunObjectsBut(t, "a moving instance") } // noRunObjectsBut is noRunObjects without the record check, for a record // that holds a move. func (fx *runFx) noRunObjectsBut(t *testing.T, what string) { t.Helper() if left, _ := fx.rel.List(context.Background(), runs.RunsPrefix(release.OpsAccount, "shop")); len(left) != 0 { t.Errorf("%s left run objects %q", what, left) } } // A service's open run holds the instance against a move, as a person's does. func TestMoveDoorRefusesWhileAServicesRunIsOpen(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) id := fx.startAs(t, runner, serviceMeta()) if _, err := fx.rel.Put(context.Background(), "control/edges/uk-box.json", []byte(`{"v":1,"box":"uk-box","eip":"1.2.3.4","placement":"uk","data_bucket":"data-uk","data_bucket_region":"eu-west-2","hosts":{}}`), ""); err != nil { t.Fatal(err) } u := fx.ts.URL + "/api/cli/apps/bab/shop/instances/" + runTestHost + "/move" status, _, body := rawReq(t, http.MethodPost, u, fx.member, `{"region":"uk"}`) if status != http.StatusConflict || !strings.Contains(body, "a run is open on this instance: "+id) { t.Fatalf("move over a service's run: %d %s", status, body) } } // One run per instance holds across a person and a service: at once, one is // admitted and the other refused, and the loser's objects are gone. func TestRunPersonAndServiceRaceAdmitsOne(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) var wg sync.WaitGroup statuses := make([]int, 2) bodies := make([]string, 2) for i, c := range []struct { bearer string meta runs.Meta }{{fx.member, okMeta()}, {runner, serviceMeta()}} { wg.Add(1) go func() { defer wg.Done() statuses[i], bodies[i] = postRun(t, fx.runsURL(runTestHost), c.bearer, c.meta, nil) }() } wg.Wait() won, lost := 0, 0 for i := range 2 { switch statuses[i] { case http.StatusAccepted: won++ case http.StatusConflict: lost++ if codeOf(t, bodies[i]) != "run_open" { t.Errorf("loser said %s", bodies[i]) } } } if won != 1 || lost != 1 { t.Fatalf("statuses %v %v", statuses, bodies) } prefix := runs.RunsPrefix(release.OpsAccount, "shop") keys, _ := fx.rel.List(context.Background(), prefix) owners := map[string]bool{} for _, k := range keys { owners[strings.SplitN(strings.TrimPrefix(k, prefix), "/", 2)[0]] = true } if len(owners) != 1 { t.Fatalf("runs with objects %v, want only the winner's", owners) } } func TestCallerScopeNamesTheKind(t *testing.T) { person := Principal{Kind: PrincipalUser, Address: "abc"} svc := Principal{Kind: PrincipalService, CredentialID: "abc"} if callerScope(person) == callerScope(svc) { t.Fatalf("a person and a credential with the same string share a scope: %q", callerScope(person)) } if callerScope(person) != "user:abc" || callerScope(svc) != "svc:abc" { t.Fatalf("scopes %q %q", callerScope(person), callerScope(svc)) } if callerScope(Principal{Kind: PrincipalService}) != "" || callerScope(Principal{Kind: PrincipalUser}) != "" { t.Fatal("a principal with no identity has a scope") } s := &Server{} if s.runVisibleTo(Principal{Kind: PrincipalService}, runs.Request{}) { t.Fatal("a service with no id sees a run with no credential") } if !s.runVisibleTo(svc, runs.Request{ByCredential: "abc"}) || s.runVisibleTo(svc, runs.Request{ByCredential: "abd"}) { t.Fatal("runVisibleTo does not key on the credential id") } if !s.runVisibleTo(person, runs.Request{ByCredential: "zzz"}) { t.Fatal("a member does not see a service's run") } } // A run credential minted in another account, for an app of the same name, // is a stranger at this account's run doors: the membership 404, and nothing // written. func TestRunServiceOfAnotherAccountRefused(t *testing.T) { fx := runFixture(t) const otherID = 4242 now := time.Now() if err := fx.st.CreateAccount(otherID, "cdef", "Other", "paul@keymail.dev", now); err != nil { t.Fatal(err) } if _, err := fx.st.CreateAppIn(otherID, "shop", "paul@keymail.dev", now); err != nil { t.Fatal(err) } other, err := fx.st.NewServicePrincipalScoped(otherID, "ci", "shop", svcrole.Run, nil, importClassic, "paul@keymail.dev", now) if err != nil { t.Fatal(err) } runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) id := fx.startAs(t, runner, serviceMeta()) want := canonicalNotFound(t) for _, d := range fx.runDoorURLs(id) { status, h, body := rawReq(t, d.method, d.u, other, d.body) assertNotFound(t, "another account's run service at "+d.method+" "+d.u, want, status, h, body) } status, h, body := postRunH(t, fx.runsURL(runTestHost), other, serviceMeta(), nil) assertNotFound(t, "another account's run service at create", want, status, h, body) if rec, _ := provision.GetRecord(context.Background(), fx.rel, release.OpsAccount, "shop", runTestHost); rec == nil || rec.ActiveRun == nil || rec.ActiveRun.ID != id || rec.ActiveRun.State != provision.RunQueued { t.Fatalf("another account's run service changed the run: %+v", rec) } } // A granted run service on a host-claim console gets the refusal a person // gets, at every run door. func TestRunGrantedServiceOnAHostClaimConsoleRefused(t *testing.T) { fx := runFixture(t) runner := runServiceToken(t, fx.st, "ci", "shop", importClassic) id := fx.startAs(t, runner, serviceMeta()) fx.srv.cfg.HostClaim = true for _, d := range fx.runDoorURLs(id) { pStatus, _, pBody := rawReq(t, d.method, d.u, fx.member, d.body) sStatus, _, sBody := rawReq(t, d.method, d.u, runner, d.body) if pStatus != http.StatusConflict || codeOf(t, pBody) != runCodeHostClaim || errorOf(t, pBody) != claimRunUnsupportedMsg { t.Errorf("a person at %s %s = %d %s, want the host-claim refusal", d.method, d.u, pStatus, pBody) } if sStatus != pStatus || sBody != pBody { t.Errorf("the run service at %s %s = %d %q, want what a person gets, %d %q", d.method, d.u, sStatus, sBody, pStatus, pBody) } } status, body := postRun(t, fx.runsURL(runTestHost), runner, serviceMeta(), nil) if status != http.StatusConflict || errorOf(t, body) != claimRunUnsupportedMsg { t.Errorf("a granted create on a host-claim console = %d %s", status, body) } } // Every refusal code a run handler writes is listed in the run API section // of the CLI page: a caller with no person to read the sentence acts on the // code, so a code the docs do not name is a code nobody can act on. func TestRunRefusalCodesAreDocumented(t *testing.T) { raw, err := os.ReadFile("../../docs/site/cli.md") if err != nil { t.Fatal(err) } _, section, ok := strings.Cut(string(raw), "\n#### The run API\n") if !ok { t.Fatal("docs/site/cli.md has no '#### The run API' section") } if i := strings.Index(section, "\n#"); i >= 0 { section = section[:i] } for _, code := range runRefusalCodes { if !strings.Contains(section, "- `"+code+"`:") { t.Errorf("the run API docs do not list the code %s", code) } } }