carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "mime/multipart"
8 "net/http"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/release"
14)
15
16// uploadBuild parks a local build through the upload door and returns the
17// status and, on a 200, the handle.
18func uploadBuild(t *testing.T, fx *consoleFixture, token string) (int, string) {
19 t.Helper()
20 var buf bytes.Buffer
21 mw := multipart.NewWriter(&buf)
22 meta, _ := mw.CreateFormField("meta")
23 meta.Write([]byte(`{"version":"66587fa","platform":"linux-arm64"}`))
24 file, _ := mw.CreateFormFile("file", "carlos-linux-arm64")
25 file.Write([]byte("ELF local"))
26 mw.Close()
27 req, _ := http.NewRequest(http.MethodPut, fx.ts.URL+"/api/cli/system/upload", &buf)
28 req.Header.Set("Content-Type", mw.FormDataContentType())
29 req.Header.Set("Authorization", "Bearer "+token)
30 res, err := http.DefaultClient.Do(req)
31 if err != nil {
32 t.Fatal(err)
33 }
34 defer res.Body.Close()
35 if res.StatusCode != http.StatusOK {
36 return res.StatusCode, ""
37 }
38 var up map[string]any
39 if err := json.NewDecoder(res.Body).Decode(&up); err != nil {
40 t.Fatal(err)
41 }
42 return res.StatusCode, str(t, up, "upload")
43}
44
45// TestSystemOperatorRollsSignedReleases: an address on SystemOperators who
46// owns nothing in the ops account can do the whole of a roll — status, an
47// update from the signed feed, watching its job, and a rollback — and every
48// move is recorded against that address, not against an owner.
49func TestSystemOperatorRollsSignedReleases(t *testing.T) {
50 fx := newConsoleFixture(t)
51 fx.srv.cfg.SystemOperators = []string{"bill@keymail.dev"}
52 systemFeed(t, fx, map[string][]byte{"carlos-linux-arm64": []byte("ELF arm64")})
53 seedBox(t, fx, "ie", "linux-arm64", "canary", "v0.17.0")
54 sg := release.RootSigner(fx.priv)
55 release.PromoteSystem(context.Background(), fx.rel, sg, "router", "canary",
56 release.SystemRelease{Version: "v0.17.0", Artifacts: map[string]release.SystemArtifact{"linux-arm64": {Key: release.SystemArtifactKey("v0.17.0", "linux-arm64"), SHA256: strings.Repeat("a", 64)}}},
57 release.SystemPromoteOptions{})
58 time.Sleep(1100 * time.Millisecond)
59 bill := cliToken(t, fx, "bill@keymail.dev", "code-sysop-bill")
60
61 if st, out := getJSON(t, fx.ts.URL+"/api/cli/system/status", bill); st != http.StatusOK {
62 t.Fatalf("status as an operator = %d %v, want 200", st, out)
63 }
64 st, out := postJSON(t, fx.ts.URL+"/api/cli/system/update", map[string]any{"channel": "canary"}, bill)
65 if st != http.StatusOK {
66 t.Fatalf("update as an operator = %d %v, want 200", st, out)
67 }
68 if job := waitJob(t, fx, bill, str(t, out, "job")); job["error"] != "" {
69 t.Fatalf("operator's job: %v", job)
70 }
71 p, _, err := release.GetSystemPointer(context.Background(), fx.rel, fx.pub, "router", "canary")
72 if err != nil || p.Current.Version != "v0.18.0" {
73 t.Fatalf("pointer after the operator's update = %+v %v", p, err)
74 }
75 if len(p.Audit) == 0 || p.Audit[0].Actor != "bill@keymail.dev" {
76 t.Fatalf("pointer audit = %+v, want the operator as the actor", p.Audit)
77 }
78 time.Sleep(1100 * time.Millisecond)
79 st, out = postJSON(t, fx.ts.URL+"/api/cli/system/rollback", map[string]string{"channel": "canary"}, bill)
80 if st != http.StatusOK || str(t, out, "version") != "v0.17.0" {
81 t.Fatalf("rollback as an operator = %d %v, want 200 onto v0.17.0", st, out)
82 }
83 events, err := fx.st.Events(OpsAccountID, "platform", 10)
84 if err != nil {
85 t.Fatal(err)
86 }
87 actions := map[string]string{}
88 for _, e := range events {
89 actions[e.Action] = e.Address
90 }
91 for _, a := range []string{"system-update", "system-rollback"} {
92 if actions[a] != "bill@keymail.dev" {
93 t.Errorf("%s audit row by %q, want the operator", a, actions[a])
94 }
95 }
96}
97
98// TestSystemOperatorCannotShipALocalBuild: the console signs whatever it is
99// uploaded with the deployment key and every box runs it, so a local build
100// stays the ops owner's. An operator is refused at the upload door, and
101// cannot spend an owner's parked upload by naming its handle either: the
102// refusal comes before the handle is consumed, so the owner's promote still
103// finds it.
104func TestSystemOperatorCannotShipALocalBuild(t *testing.T) {
105 fx := newConsoleFixture(t)
106 fx.srv.cfg.SystemOperators = []string{"bill@keymail.dev"}
107 owner := cliToken(t, fx, "paul@keymail.dev", "code-sysop-owner")
108 bill := cliToken(t, fx, "bill@keymail.dev", "code-sysop-nofile")
109
110 if st, _ := uploadBuild(t, fx, bill); st != http.StatusForbidden {
111 t.Fatalf("upload as an operator = %d, want 403", st)
112 }
113 st, handle := uploadBuild(t, fx, owner)
114 if st != http.StatusOK {
115 t.Fatalf("upload as the owner = %d", st)
116 }
117 st, out := postJSON(t, fx.ts.URL+"/api/cli/system/update", map[string]any{"channel": "canary", "upload": handle}, bill)
118 if st != http.StatusForbidden {
119 t.Fatalf("update naming an upload as an operator = %d %v, want 403", st, out)
120 }
121 if str(t, out, "error") == "" {
122 t.Fatalf("operator's refusal = %v, want a sentence", out)
123 }
124 st, out = postJSON(t, fx.ts.URL+"/api/cli/system/update", map[string]any{"channel": "canary", "upload": handle}, owner)
125 if st != http.StatusOK {
126 t.Fatalf("owner's update = %d %v", st, out)
127 }
128 if job := waitJob(t, fx, owner, str(t, out, "job")); job["error"] != "" {
129 t.Fatalf("owner's upload was consumed by the operator's refused request: %v", job)
130 }
131}
132
133// TestSystemOperatorListGatesWhoGetsIn: the list is the whole grant. Off it,
134// a person gets the same bare 404 a nonexistent path gives; an empty list is
135// exactly the owner-only console that existed before it. Entries are
136// trimmed and lowercased the way the other operator lists are, because the
137// list is typed by a person into an env file.
138func TestSystemOperatorListGatesWhoGetsIn(t *testing.T) {
139 fx := newConsoleFixture(t)
140 bill := cliToken(t, fx, "bill@keymail.dev", "code-sysop-list")
141 _, want := getRaw(t, fx.ts.URL+"/api/cli/system/no-such-door", bill)
142 for _, list := range [][]string{nil, {"alice@keymail.dev"}, {"bill@keymail.dev.evil"}} {
143 fx.srv.cfg.SystemOperators = list
144 if st, body := getRaw(t, fx.ts.URL+"/api/cli/system/status", bill); st != http.StatusNotFound || body != want {
145 t.Errorf("status with SystemOperators=%q = %d %q, want 404 %q", list, st, body, want)
146 }
147 if st, body := postRaw(t, fx.ts.URL+"/api/cli/system/rollback", map[string]string{"channel": "canary"}, bill); st != http.StatusNotFound || body != want {
148 t.Errorf("rollback with SystemOperators=%q = %d %q, want 404 %q", list, st, body, want)
149 }
150 // The owner-only 403 is for callers the gate has ADMITTED. Off the
151 // list, the two upload paths must answer the same 404 as every other
152 // door, or the 403's sentence confirms the surface exists.
153 if st, _ := uploadBuild(t, fx, bill); st != http.StatusNotFound {
154 t.Errorf("upload with SystemOperators=%q = %d, want 404", list, st)
155 }
156 if st, body := postRaw(t, fx.ts.URL+"/api/cli/system/update", map[string]string{"channel": "canary", "upload": "deadbeef"}, bill); st != http.StatusNotFound || body != want {
157 t.Errorf("update naming an upload with SystemOperators=%q = %d %q, want 404 %q", list, st, body, want)
158 }
159 }
160 fx.srv.cfg.SystemOperators = []string{" Bill@Keymail.dev "}
161 if st, out := getJSON(t, fx.ts.URL+"/api/cli/system/status", bill); st != http.StatusOK {
162 t.Fatalf("status with a padded, mixed-case entry = %d %v, want 200", st, out)
163 }
164}
165
166// TestSystemRollerStoreErrorAdmitsNobody: a failed ownership lookup is
167// logged and the list is still consulted, so a listed operator keeps working
168// through a store fault. The fault itself must never admit anyone: an owner
169// the console cannot see is refused like a stranger, and the listed operator
170// gets in as an operator, never as an owner.
171func TestSystemRollerStoreErrorAdmitsNobody(t *testing.T) {
172 fx := newConsoleFixture(t)
173 fx.srv.cfg.SystemOperators = []string{"bill@keymail.dev"}
174 owner := cliToken(t, fx, "paul@keymail.dev", "code-sysop-err-owner")
175 bill := cliToken(t, fx, "bill@keymail.dev", "code-sysop-err-bill")
176 _, want := getRaw(t, fx.ts.URL+"/api/cli/system/no-such-door", owner)
177 if _, err := fx.st.db.Exec(`ALTER TABLE memberships RENAME TO memberships_gone`); err != nil {
178 t.Fatal(err)
179 }
180 if _, err := fx.st.Role(OpsAccountID, "paul@keymail.dev"); err == nil {
181 t.Fatal("Role still answers; the fault this test needs did not happen")
182 }
183 if st, body := getRaw(t, fx.ts.URL+"/api/cli/system/status", owner); st != http.StatusNotFound || body != want {
184 t.Errorf("status as an unlisted owner during a store fault = %d %q, want 404 %q", st, body, want)
185 }
186 if st, out := getJSON(t, fx.ts.URL+"/api/cli/system/status", bill); st != http.StatusOK {
187 t.Errorf("status as a listed operator during a store fault = %d %v, want 200", st, out)
188 }
189 if st, _ := uploadBuild(t, fx, bill); st != http.StatusForbidden {
190 t.Errorf("upload as a listed operator during a store fault = %d, want 403 (an operator, not an owner)", st)
191 }
192}
193