Commit ed682bb8e7

ed682bb8e7a6d851484e7e3fb41c3a86ecf70165

parent: dd36248950

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 07:12 UTC

e2e: expiring tokens refused on minting; revoke --created

Ref #257

Layout: unified · split

e2e/tokenorigin_test.go added +74
@@ -0,0 +1,74 @@
1package e2e
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "strings"
8 "testing"
9)
10
11// An expiring token cannot mint a credential that outlives it, and
12// revoking a token can take what it created with it (#257).
13func TestTokenDelegation(t *testing.T) {
14 t.Parallel()
15 inst := startInstanceWith(t, "[api]\nenabled = true\n")
16 aliceKey := inst.newKey(t, "alice")
17 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
18
19 mint := func(args ...string) (token, scope string) {
20 t.Helper()
21 out, errOut, code := inst.ssh(t, aliceKey, "", append([]string{"token", "create", "--json"}, args...)...)
22 if code != 0 {
23 t.Fatalf("token create %v: %s", args, errOut)
24 }
25 var env struct {
26 Data struct {
27 Token string `json:"token"`
28 Scope string `json:"scope"`
29 } `json:"data"`
30 }
31 if err := json.Unmarshal([]byte(out), &env); err != nil {
32 t.Fatalf("token create output: %v %s", err, out)
33 }
34 return env.Data.Token, env.Data.Scope
35 }
36 if _, scope := mint("--name", "plain"); scope != "read" {
37 t.Fatalf("default scope %q, want read", scope)
38 }
39 brief, _ := mint("--name", "brief", "--scope", "full", "--ttl", "1h")
40 lasting, _ := mint("--name", "lasting", "--scope", "full")
41
42 spare := inst.newKey(t, "spare")
43 pub, err := os.ReadFile(spare + ".pub")
44 if err != nil {
45 t.Fatal(err)
46 }
47 status, body := inst.apiCall(t, brief, []string{"keys", "add"}, string(pub))
48 if status != 403 || !strings.Contains(fmt.Sprint(body["error"]), "expires") {
49 t.Fatalf("expiring token added a key: %d %v", status, body)
50 }
51 if status, _ := inst.apiCall(t, brief, []string{"whoami"}, ""); status != 200 {
52 t.Fatalf("expiring token refused a read: %d", status)
53 }
54 if status, body := inst.apiCall(t, lasting, []string{"keys", "add"}, string(pub)); status != 200 {
55 t.Fatalf("keys add: %d %v", status, body)
56 }
57 if status, body := inst.apiCall(t, lasting, []string{"token", "create", "--name", "child"}, ""); status != 200 {
58 t.Fatalf("token create: %d %v", status, body)
59 }
60 if _, errOut, code := inst.ssh(t, spare, "", "whoami"); code != 0 {
61 t.Fatalf("the added key does not work: %s", errOut)
62 }
63
64 out, errOut, code := inst.ssh(t, aliceKey, "", "token", "revoke", "lasting", "--created")
65 if code != 0 || !strings.Contains(out, "token child") || !strings.Contains(out, fingerprint(t, spare+".pub")) {
66 t.Fatalf("revoke --created: exit %d\n%s%s", code, out, errOut)
67 }
68 if _, _, code := inst.ssh(t, spare, "", "whoami"); code == 0 {
69 t.Fatal("a key the revoked token created still works")
70 }
71 if out, _, _ := inst.ssh(t, aliceKey, "", "token", "list"); strings.Contains(out, "child") {
72 t.Fatalf("the child token survived:\n%s", out)
73 }
74}