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 "errors"
5 "net/http"
6 "net/url"
7 "reflect"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/runs"
13 "github.com/carlosframework/platform/internal/svcrole"
14)
15
16var importClassic = [][]string{{"import", "classic"}}
17
18func mintRun(t *testing.T, st *Store, name string, commands [][]string, now time.Time) string {
19 t.Helper()
20 raw, err := st.NewServicePrincipalScoped(1, name, "hello", svcrole.Run, nil, commands, "paul@example.test", now)
21 if err != nil {
22 t.Fatalf("mint run %s: %v", name, err)
23 }
24 return raw
25}
26
27func credentialIDOf(t *testing.T, st *Store, raw string) string {
28 t.Helper()
29 var id string
30 if err := st.db.QueryRow(`SELECT credential_id FROM cli_tokens WHERE token_hash = ?`, hashToken(raw)).Scan(&id); err != nil {
31 t.Fatal(err)
32 }
33 return id
34}
35
36func TestRunCredentialCarriesItsCommands(t *testing.T) {
37 st := newDeployStore(t)
38 now := time.Now()
39 raw := mintRun(t, st, "importer", [][]string{{"import", "classic"}, {"import", "sync"}}, now)
40 p, ok := st.CLIPrincipal(raw, now)
41 if !ok {
42 t.Fatal("a run credential does not resolve")
43 }
44 if p.Role != svcrole.Run || p.App != "hello" || p.Name != "importer" {
45 t.Fatalf("CLIPrincipal = %+v", p)
46 }
47 if want := [][]string{{"import", "classic"}, {"import", "sync"}}; !reflect.DeepEqual(p.Commands, want) {
48 t.Fatalf("Commands = %q, want %q", p.Commands, want)
49 }
50 if p.CredentialID == "" || p.CredentialID != credentialIDOf(t, st, raw) {
51 t.Fatalf("CredentialID = %q, want the row's own non-empty id", p.CredentialID)
52 }
53 // At least 128 bits: 16 random bytes, hex.
54 if len(p.CredentialID) < 32 {
55 t.Fatalf("CredentialID %q is shorter than 128 bits of hex", p.CredentialID)
56 }
57 if strings.Contains(p.CredentialID, "importer") {
58 t.Fatalf("CredentialID %q is derived from the name", p.CredentialID)
59 }
60 // Stored canonically: JSON, in the order given.
61 var stored string
62 if err := st.db.QueryRow(`SELECT commands FROM cli_tokens WHERE token_hash = ?`, hashToken(raw)).Scan(&stored); err != nil {
63 t.Fatal(err)
64 }
65 if want := `[["import","classic"],["import","sync"]]`; stored != want {
66 t.Fatalf("stored commands = %s, want %s", stored, want)
67 }
68
69 // Every other service principal has an id too, and no commands.
70 pub, err := st.NewServicePrincipalScoped(1, "pub", "hello", svcrole.Publish, nil, nil, "paul@example.test", now)
71 if err != nil {
72 t.Fatal(err)
73 }
74 pp, ok := st.CLIPrincipal(pub, now)
75 if !ok || pp.CredentialID == "" || pp.Commands != nil {
76 t.Fatalf("publish principal = %+v, %v; want an id and nil Commands", pp, ok)
77 }
78 if pp.CredentialID == p.CredentialID {
79 t.Fatal("two credentials share one id")
80 }
81 // The listing carries the stored form.
82 rows, err := st.ServicePrincipals(1)
83 if err != nil {
84 t.Fatal(err)
85 }
86 for _, r := range rows {
87 if r.Name == "importer" && r.CommandList() != "import classic, import sync" {
88 t.Fatalf("listing CommandList = %q", r.CommandList())
89 }
90 if r.Name == "pub" && r.Commands != "" {
91 t.Fatalf("publish listing Commands = %q, want empty", r.Commands)
92 }
93 }
94}
95
96func TestRunCredentialNeedsCommands(t *testing.T) {
97 st := newDeployStore(t)
98 now := time.Now()
99 mint := func(app string, role svcrole.Role, commands [][]string) error {
100 _, err := st.NewServicePrincipalScoped(1, "x", app, role, nil, commands, "paul@example.test", now)
101 return err
102 }
103 if err := mint("hello", svcrole.Run, nil); !errors.Is(err, ErrRunNeedsCommands) {
104 t.Errorf("run with nil commands: %v, want ErrRunNeedsCommands", err)
105 }
106 if err := mint("hello", svcrole.Run, [][]string{}); !errors.Is(err, ErrRunNeedsCommands) {
107 t.Errorf("run with no commands: %v, want ErrRunNeedsCommands", err)
108 }
109 if err := mint("hello", svcrole.Instance, importClassic); !errors.Is(err, ErrCommandsNeedRun) {
110 t.Errorf("instance with commands: %v, want ErrCommandsNeedRun", err)
111 }
112 if err := mint("hello", svcrole.Admin, importClassic); !errors.Is(err, ErrCommandsNeedRun) {
113 t.Errorf("admin with commands: %v, want ErrCommandsNeedRun", err)
114 }
115 if err := mint("", svcrole.Run, importClassic); !errors.Is(err, ErrAccountScopedRun) {
116 t.Errorf("run with no app: %v, want ErrAccountScopedRun", err)
117 }
118 if err := mint("hello", svcrole.Run, [][]string{{"import"}, {}}); !errors.Is(err, runs.ErrEmptyCommand) {
119 t.Errorf("run with a zero-word prefix: %v, want runs.ErrEmptyCommand", err)
120 }
121 if err := mint("hello", svcrole.Run, [][]string{{"import", ""}}); !errors.Is(err, runs.ErrEmptyCommandWord) {
122 t.Errorf("run with an empty word: %v, want runs.ErrEmptyCommandWord", err)
123 }
124 // Rungs and commands are separate fields: a run credential takes no rungs.
125 if _, err := st.NewServicePrincipalScoped(1, "x", "hello", svcrole.Run, []string{"edge"}, importClassic, "paul@example.test", now); !errors.Is(err, ErrRungsNeedDeploy) {
126 t.Errorf("run with rungs: %v, want ErrRungsNeedDeploy", err)
127 }
128 // The old entry point carries no commands, so it cannot mint a run credential.
129 if _, err := st.NewServicePrincipal(1, "x", "hello", svcrole.Run, "paul@example.test", now); !errors.Is(err, ErrRunNeedsCommands) {
130 t.Errorf("NewServicePrincipal(run): %v, want ErrRunNeedsCommands", err)
131 }
132 if rows, err := st.ServicePrincipals(1); err != nil || len(rows) != 0 {
133 t.Fatalf("ServicePrincipals = %d rows %v after refused mints, want none", len(rows), err)
134 }
135}
136
137// TestRunCredentialRefusedWithAnEmptyPrefix: a stored run row whose
138// commands are missing, unparseable, empty, non-canonical or hold a
139// zero-word prefix is refused outright, never read as an unscoped principal.
140func TestRunCredentialRefusedWithAnEmptyPrefix(t *testing.T) {
141 st := newDeployStore(t)
142 now := time.Now()
143 for i, stored := range []string{
144 `[[]]`,
145 `[["import"],[]]`,
146 `[null]`,
147 ``,
148 `null`,
149 `[]`,
150 `not json`,
151 `[["import",""]]`,
152 `[["import", "classic"]]`, // parses, but not the canonical form this console writes
153 `{"import":"classic"}`,
154 } {
155 raw := mintRun(t, st, "run"+string(rune('a'+i)), importClassic, now)
156 if _, err := st.db.Exec(`UPDATE cli_tokens SET commands = ? WHERE token_hash = ?`, stored, hashToken(raw)); err != nil {
157 t.Fatal(err)
158 }
159 if p, ok := st.CLIPrincipal(raw, now); ok {
160 t.Errorf("commands %q: resolved to %+v; want refused", stored, p)
161 }
162 }
163 // A run row with no credential id is refused: Task 3 keys runs on it.
164 raw := mintRun(t, st, "noid", importClassic, now)
165 if _, err := st.db.Exec(`UPDATE cli_tokens SET credential_id = '' WHERE token_hash = ?`, hashToken(raw)); err != nil {
166 t.Fatal(err)
167 }
168 if p, ok := st.CLIPrincipal(raw, now); ok {
169 t.Errorf("a run row with no credential id resolved to %+v; want refused", p)
170 }
171 // A run row whose app was emptied is refused: never account-wide.
172 raw = mintRun(t, st, "noapp", importClassic, now)
173 if _, err := st.db.Exec(`UPDATE cli_tokens SET app = '' WHERE token_hash = ?`, hashToken(raw)); err != nil {
174 t.Fatal(err)
175 }
176 if p, ok := st.CLIPrincipal(raw, now); ok {
177 t.Errorf("a run row with app '' resolved to %+v; want refused", p)
178 }
179 // Any other role carrying commands is refused too.
180 pub, err := st.NewServicePrincipalScoped(1, "pub", "hello", svcrole.Publish, nil, nil, "paul@example.test", now)
181 if err != nil {
182 t.Fatal(err)
183 }
184 if _, err := st.db.Exec(`UPDATE cli_tokens SET commands = '[["import"]]' WHERE token_hash = ?`, hashToken(pub)); err != nil {
185 t.Fatal(err)
186 }
187 if p, ok := st.CLIPrincipal(pub, now); ok {
188 t.Errorf("a publish row with commands resolved to %+v; want refused", p)
189 }
190}
191
192func TestRotationKeepsCredentialIDAndCommands(t *testing.T) {
193 st := newDeployStore(t)
194 now := time.Now()
195 raw := mintRun(t, st, "importer", importClassic, now)
196 before, ok := st.CLIPrincipal(raw, now)
197 if !ok {
198 t.Fatal("does not resolve")
199 }
200 fresh, err := st.RotateServicePrincipal(1, "importer", now.Add(time.Minute))
201 if err != nil {
202 t.Fatal(err)
203 }
204 if _, ok := st.CLIPrincipal(raw, now); ok {
205 t.Fatal("the old secret still resolves after rotate")
206 }
207 after, ok := st.CLIPrincipal(fresh, now)
208 if !ok {
209 t.Fatal("the rotated secret does not resolve")
210 }
211 if after.CredentialID != before.CredentialID {
212 t.Fatalf("rotation moved the credential id: %q -> %q", before.CredentialID, after.CredentialID)
213 }
214 if !reflect.DeepEqual(after.Commands, importClassic) {
215 t.Fatalf("rotation changed the commands: %q", after.Commands)
216 }
217}
218
219func TestRemintGetsANewCredentialID(t *testing.T) {
220 st := newDeployStore(t)
221 now := time.Now()
222 old := mintRun(t, st, "importer", importClassic, now)
223 oldID := credentialIDOf(t, st, old)
224 if err := st.RevokeServicePrincipal(1, "importer", now); err != nil {
225 t.Fatal(err)
226 }
227 fresh := mintRun(t, st, "importer", importClassic, now)
228 p, ok := st.CLIPrincipal(fresh, now)
229 if !ok {
230 t.Fatal("the re-minted credential does not resolve")
231 }
232 if p.CredentialID == "" || p.CredentialID == oldID {
233 t.Fatalf("re-minted credential id = %q, old = %q; want a fresh one", p.CredentialID, oldID)
234 }
235}
236
237// TestCredentialIDBackfillIsIdempotent: rows written before the column
238// existed (an empty credential_id) get a fresh, distinct id at Open, and a
239// later Open keeps every id it finds.
240func TestCredentialIDBackfillIsIdempotent(t *testing.T) {
241 path := t.TempDir() + "/console.db"
242 st, err := Open(path)
243 if err != nil {
244 t.Fatal(err)
245 }
246 now := time.Now()
247 svc, err := st.NewServicePrincipalScoped(1, "pub", "hello", svcrole.Publish, nil, nil, "paul@example.test", now)
248 if err != nil {
249 t.Fatal(err)
250 }
251 app, err := st.NewAppToken(1, "hello", "paul@example.test", "router", now)
252 if err != nil {
253 t.Fatal(err)
254 }
255 user := mintUserToken(t, st, "paul@example.test", now)
256 // Pretend these were written by a console before the column existed.
257 if _, err := st.db.Exec(`UPDATE cli_tokens SET credential_id = ''`); err != nil {
258 t.Fatal(err)
259 }
260 st.Close()
261
262 st, err = Open(path)
263 if err != nil {
264 t.Fatal(err)
265 }
266 svcID, appID, userID := credentialIDOf(t, st, svc), credentialIDOf(t, st, app), credentialIDOf(t, st, user)
267 if svcID == "" || appID == "" {
268 t.Fatalf("backfill left a service row without an id: service %q, app %q", svcID, appID)
269 }
270 if svcID == appID {
271 t.Fatal("backfill gave two rows one id")
272 }
273 if userID != "" {
274 t.Fatalf("backfill gave a person's token an id %q", userID)
275 }
276 st.Close()
277
278 st, err = Open(path)
279 if err != nil {
280 t.Fatal(err)
281 }
282 t.Cleanup(func() { st.Close() })
283 if got := credentialIDOf(t, st, svc); got != svcID {
284 t.Fatalf("a second Open moved the service id: %q -> %q", svcID, got)
285 }
286 if got := credentialIDOf(t, st, app); got != appID {
287 t.Fatalf("a second Open moved the app id: %q -> %q", appID, got)
288 }
289 if p, ok := st.CLIPrincipal(app, now); !ok || p.CredentialID != appID {
290 t.Fatalf("legacy app principal = %+v, %v; want CredentialID %q", p, ok, appID)
291 }
292 if p, ok := st.CLIPrincipal(user, now); !ok || p.CredentialID != "" {
293 t.Fatalf("person principal = %+v, %v; want no CredentialID", p, ok)
294 }
295}
296
297func TestAPIMintRun(t *testing.T) {
298 fx := newConsoleFixture(t)
299 if _, err := fx.st.CreateAppIn(OpsAccountID, "hello", "paul@keymail.dev", time.Now()); err != nil {
300 t.Fatal(err)
301 }
302 token := cliToken(t, fx, "paul@keymail.dev", "code-run-mint")
303 u := fx.ts.URL + "/api/cli/services/bab"
304 status, out := postJSON(t, u, map[string]any{"name": "importer", "role": "run", "app": "hello",
305 "commands": [][]string{{"import", "classic"}, {"import", "sync"}}}, token)
306 if status != http.StatusCreated {
307 t.Fatalf("mint run = %d %v", status, out)
308 }
309 if got := out["commands"]; !reflect.DeepEqual(got, []any{[]any{"import", "classic"}, []any{"import", "sync"}}) {
310 t.Fatalf("commands in response = %#v", got)
311 }
312 for _, tc := range []struct {
313 body map[string]any
314 want string
315 }{
316 {map[string]any{"name": "a1", "role": "run", "commands": importClassic}, "must name one app"},
317 {map[string]any{"name": "a2", "role": "run", "app": "hello"}, "must name the commands it may run"},
318 {map[string]any{"name": "a3", "role": "run", "app": "hello", "commands": [][]string{{}}}, "a command prefix has no words"},
319 {map[string]any{"name": "a4", "role": "run", "app": "hello", "commands": [][]string{{"import", ""}}}, "empty word"},
320 {map[string]any{"name": "a5", "role": "publish", "app": "hello", "commands": importClassic}, "only a run credential takes commands"},
321 } {
322 status, out := postJSON(t, u, tc.body, token)
323 if status != http.StatusBadRequest || !strings.Contains(str(t, out, "error"), tc.want) {
324 t.Errorf("%v = %d %v; want 400 containing %q", tc.body, status, out, tc.want)
325 }
326 }
327 if status, out := postJSON(t, u, map[string]any{"name": "pub", "role": "publish", "app": "hello"}, token); status != http.StatusCreated {
328 t.Fatalf("mint publish = %d %v", status, out)
329 } else if c, ok := out["commands"].([]any); !ok || len(c) != 0 {
330 t.Fatalf("publish mint commands = %#v, want []", out["commands"])
331 }
332 status, out = getJSON(t, u, token)
333 if status != http.StatusOK {
334 t.Fatalf("list = %d %v", status, out)
335 }
336 rows, _ := out["services"].([]any)
337 if len(rows) != 2 {
338 t.Fatalf("listing = %v", rows)
339 }
340 for _, r := range rows {
341 row := r.(map[string]any)
342 c, ok := row["commands"].([]any)
343 switch row["name"] {
344 case "importer":
345 if !reflect.DeepEqual(c, []any{[]any{"import", "classic"}, []any{"import", "sync"}}) {
346 t.Errorf("listing commands for importer = %#v", row["commands"])
347 }
348 case "pub":
349 if !ok || len(c) != 0 {
350 t.Errorf("listing commands for pub = %#v, want []", row["commands"])
351 }
352 }
353 }
354}
355
356func TestFormMintRun(t *testing.T) {
357 fx := newConsoleFixture(t)
358 c := signInPaul(t, fx)
359 csrf := sessionToken(t, fx, c)
360
361 before, err := fx.st.ServicePrincipals(OpsAccountID)
362 if err != nil {
363 t.Fatal(err)
364 }
365 for _, bad := range []struct {
366 why string
367 form url.Values
368 want string
369 }{
370 {"no app", url.Values{"csrf": {csrf}, "name": {"imp"}, "role": {"run"}, "commands": {"import classic"}}, "A run credential must name one app. Nothing was minted."},
371 {"no commands", url.Values{"csrf": {csrf}, "name": {"imp"}, "role": {"run"}, "app": {"kass"}}, "A run credential must name the commands it may run"},
372 {"only commas", url.Values{"csrf": {csrf}, "name": {"imp"}, "role": {"run"}, "app": {"kass"}, "commands": {" , ,"}}, "A run credential must name the commands it may run"},
373 {"a word too long", url.Values{"csrf": {csrf}, "name": {"imp"}, "role": {"run"}, "app": {"kass"}, "commands": {"import " + strings.Repeat("x", 129)}}, "128 bytes"},
374 {"commands on publish", url.Values{"csrf": {csrf}, "name": {"imp"}, "role": {"publish"}, "app": {"kass"}, "commands": {"import classic"}}, "Only a run credential takes commands. Nothing was minted."},
375 } {
376 status, loc, body := postForm(t, c, fx.u("/services/new"), bad.form)
377 if strings.HasPrefix(loc, fx.fk.srv.URL) {
378 t.Errorf("%s cost a passkey trip (-> %q)", bad.why, loc)
379 }
380 if status != http.StatusSeeOther || loc != fx.path("/settings") {
381 t.Errorf("%s = %d -> %q, want 303 back to the page:\n%s", bad.why, status, loc, body)
382 continue
383 }
384 _, _, page := get(t, c, fx.u("/settings"))
385 if !strings.Contains(page, bad.want) {
386 t.Errorf("%s: the settings page does not say %q:\n%s", bad.why, bad.want, page)
387 }
388 rows, err := fx.st.ServicePrincipals(OpsAccountID)
389 if err != nil || len(rows) != len(before) {
390 t.Errorf("%s: ServicePrincipals = %d rows %v, want %d (nothing minted)", bad.why, len(rows), err, len(before))
391 }
392 }
393 // The form offers the field.
394 _, _, page := get(t, c, fx.u("/settings"))
395 if !strings.Contains(page, `name="commands"`) {
396 t.Fatalf("the mint form has no commands field:\n%s", page)
397 }
398
399 status, _, body := postGated(t, fx, c, fx.u("/services/new"),
400 url.Values{"csrf": {csrf}, "name": {"imp"}, "role": {"run"}, "app": {"kass"}, "commands": {"import classic, import sync"}})
401 if status != http.StatusOK {
402 t.Fatalf("run mint = %d:\n%s", status, body)
403 }
404 if !strings.Contains(body, "commands import classic, import sync") {
405 t.Fatalf("the secret banner does not name the commands:\n%s", body)
406 }
407 secret := paneSecret(t, body)
408 p, ok := fx.st.CLIPrincipal(secret, time.Now())
409 if !ok || !reflect.DeepEqual(p.Commands, [][]string{{"import", "classic"}, {"import", "sync"}}) {
410 t.Fatalf("minted credential resolves to %+v, %v", p, ok)
411 }
412 _, _, page = get(t, c, fx.u("/settings"))
413 if !strings.Contains(page, "import classic, import sync") {
414 t.Fatalf("the credential listing does not show the commands:\n%s", page)
415 }
416}
417
418// badCommandWords are words neither mint door may store. Through the JSON
419// API each reaches the store as one word; "\xff" arrives as U+FFFD, which
420// is how the decoder replaces invalid UTF-8, and is refused as such.
421var badCommandWords = map[string]string{
422 "a space": "import classic",
423 "a comma": "import,classic",
424 "ESC": "\x1b[8mclassic",
425 "invalid UTF-8": "\xff",
426 "a tab": "import\tclassic",
427}
428
429func TestAPIMintRunRefusesUnprintableWords(t *testing.T) {
430 fx := newConsoleFixture(t)
431 if _, err := fx.st.CreateAppIn(OpsAccountID, "hello", "paul@keymail.dev", time.Now()); err != nil {
432 t.Fatal(err)
433 }
434 token := cliToken(t, fx, "paul@keymail.dev", "code-run-words")
435 u := fx.ts.URL + "/api/cli/services/bab"
436 for why, w := range badCommandWords {
437 status, out := postJSON(t, u, map[string]any{"name": "imp", "role": "run", "app": "hello",
438 "commands": [][]string{{"import", w}}}, token)
439 if status != http.StatusBadRequest || !strings.Contains(str(t, out, "error"), "cannot be printed") {
440 t.Errorf("a word with %s = %d %v; want 400 naming the character rule", why, status, out)
441 }
442 }
443 if rows, err := fx.st.ServicePrincipals(OpsAccountID); err != nil || len(rows) != 0 {
444 t.Fatalf("ServicePrincipals = %d rows %v, want nothing minted", len(rows), err)
445 }
446}
447
448// The form's field is text, where a space, a comma or a tab separates
449// words and prefixes, so those three can never reach a word through it:
450// they split instead (pinned below). ESC, invalid UTF-8 and a zero-width
451// space do reach a word, and are refused before the passkey.
452func TestFormMintRunRefusesUnprintableWords(t *testing.T) {
453 fx := newConsoleFixture(t)
454 c := signInPaul(t, fx)
455 csrf := sessionToken(t, fx, c)
456 for why, field := range map[string]string{
457 "ESC": "import \x1b[8mclassic",
458 "invalid UTF-8": "import \xff",
459 "a zero-width space": "import\u200bclassic",
460 } {
461 status, loc, body := postForm(t, c, fx.u("/services/new"),
462 url.Values{"csrf": {csrf}, "name": {"imp"}, "role": {"run"}, "app": {"kass"}, "commands": {field}})
463 if strings.HasPrefix(loc, fx.fk.srv.URL) {
464 t.Errorf("%s cost a passkey trip (-> %q)", why, loc)
465 }
466 if status != http.StatusSeeOther || loc != fx.path("/settings") {
467 t.Errorf("%s = %d -> %q, want 303 back to the page:\n%s", why, status, loc, body)
468 continue
469 }
470 _, _, page := get(t, c, fx.u("/settings"))
471 if !strings.Contains(page, "A command word holds a space, a comma or a character that cannot be printed. Nothing was minted.") {
472 t.Errorf("%s: the settings page does not give the character rule:\n%s", why, page)
473 }
474 }
475 if rows, err := fx.st.ServicePrincipals(OpsAccountID); err != nil || len(rows) != 0 {
476 t.Fatalf("ServicePrincipals = %d rows %v, want nothing minted", len(rows), err)
477 }
478 // A space, comma or tab in the field separates; it never lands in a word.
479 status, _, body := postGated(t, fx, c, fx.u("/services/new"),
480 url.Values{"csrf": {csrf}, "name": {"imp"}, "role": {"run"}, "app": {"kass"}, "commands": {"import\tclassic,import sync"}})
481 if status != http.StatusOK {
482 t.Fatalf("run mint = %d:\n%s", status, body)
483 }
484 p, ok := fx.st.CLIPrincipal(paneSecret(t, body), time.Now())
485 if !ok || !reflect.DeepEqual(p.Commands, [][]string{{"import", "classic"}, {"import", "sync"}}) {
486 t.Fatalf("minted credential resolves to %+v, %v", p, ok)
487 }
488}
489
490// TestStoredUnprintableWordIsRefusedAtSignIn: the read side runs the same
491// word check, so a row holding such a word never resolves.
492func TestStoredUnprintableWordIsRefusedAtSignIn(t *testing.T) {
493 st := newDeployStore(t)
494 now := time.Now()
495 raw := mintRun(t, st, "imp", importClassic, now)
496 if _, err := st.db.Exec(`UPDATE cli_tokens SET commands = ? WHERE token_hash = ?`, `[["import","a\u001bb"]]`, hashToken(raw)); err != nil {
497 t.Fatal(err)
498 }
499 if p, ok := st.CLIPrincipal(raw, now); ok {
500 t.Fatalf("a stored word with ESC resolved to %+v; want refused", p)
501 }
502}
503
504func TestCredentialIDIsUnique(t *testing.T) {
505 st := newDeployStore(t)
506 now := time.Now()
507 a := mintRun(t, st, "a", importClassic, now)
508 b, err := st.NewServicePrincipalScoped(1, "b", "hello", svcrole.Publish, nil, nil, "paul@example.test", now)
509 if err != nil {
510 t.Fatal(err)
511 }
512 if _, err := st.db.Exec(`UPDATE cli_tokens SET credential_id = ? WHERE token_hash = ?`,
513 credentialIDOf(t, st, a), hashToken(b)); err == nil {
514 t.Fatal("two rows were given one credential id; want the unique index to refuse it")
515 }
516 // Any number of people's tokens keep the empty id side by side.
517 for i := 0; i < 3; i++ {
518 mintUserToken(t, st, "paul@example.test", now)
519 }
520 var empty int
521 if err := st.db.QueryRow(`SELECT COUNT(*) FROM cli_tokens WHERE credential_id = ''`).Scan(&empty); err != nil || empty != 3 {
522 t.Fatalf("rows with an empty id = %d, %v; want 3", empty, err)
523 }
524}
525
526// TestUnreadableRunCommandsAreSaidSo: a run row whose stored list cannot
527// be read is refused at sign-in, so the listing says that rather than
528// showing it as a credential with no commands.
529func TestUnreadableRunCommandsAreSaidSo(t *testing.T) {
530 fx := newConsoleFixture(t)
531 if _, err := fx.st.CreateAppIn(OpsAccountID, "hello", "paul@keymail.dev", time.Now()); err != nil {
532 t.Fatal(err)
533 }
534 if _, err := fx.st.NewServicePrincipalScoped(OpsAccountID, "mangled", "hello", svcrole.Run, nil, importClassic, "paul@keymail.dev", time.Now()); err != nil {
535 t.Fatal(err)
536 }
537 if _, err := fx.st.NewServicePrincipalScoped(OpsAccountID, "pub", "hello", svcrole.Publish, nil, nil, "paul@keymail.dev", time.Now()); err != nil {
538 t.Fatal(err)
539 }
540 if _, err := fx.st.db.Exec(`UPDATE cli_tokens SET commands = '[[]]' WHERE name = 'mangled'`); err != nil {
541 t.Fatal(err)
542 }
543 token := cliToken(t, fx, "paul@keymail.dev", "code-run-unreadable")
544 status, out := getJSON(t, fx.ts.URL+"/api/cli/services/bab", token)
545 if status != http.StatusOK {
546 t.Fatalf("list = %d %v", status, out)
547 }
548 for _, r := range out["services"].([]any) {
549 row := r.(map[string]any)
550 switch row["name"] {
551 case "mangled":
552 if row["commands_unreadable"] != true {
553 t.Errorf("mangled row = %v; want commands_unreadable true", row)
554 }
555 if c, ok := row["commands"].([]any); !ok || len(c) != 0 {
556 t.Errorf("mangled commands = %#v; want []", row["commands"])
557 }
558 case "pub":
559 if _, ok := row["commands_unreadable"]; ok {
560 t.Errorf("pub row carries commands_unreadable: %v", row)
561 }
562 }
563 }
564 c := signInPaul(t, fx)
565 _, _, page := get(t, c, fx.u("/settings"))
566 if !strings.Contains(page, "(unreadable — refused at sign-in)") {
567 t.Fatalf("the settings listing does not say the commands are unreadable:\n%s", page)
568 }
569 if n := strings.Count(page, "unreadable — refused"); n != 1 {
570 t.Fatalf("the unreadable note appears %d times, want once (only the run row)", n)
571 }
572}
573