~james/core

core/spindle/nixcache/token_test.go -rw-r--r-- 3.2 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package nixcache

import (
	"crypto/rand"
	"crypto/rsa"
	"strings"
	"testing"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

func testKey(t *testing.T) *rsa.PrivateKey {
	t.Helper()
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatal(err)
	}
	return key
}

func parseClaims(t *testing.T, key *rsa.PrivateKey, token string) jwt.MapClaims {
	t.Helper()
	claims := jwt.MapClaims{}
	_, err := jwt.ParseWithClaims(token, claims, func(*jwt.Token) (any, error) { return &key.PublicKey, nil },
		jwt.WithValidMethods([]string{"RS256"}), jwt.WithExpirationRequired())
	if err != nil {
		t.Fatalf("parsing token: %v", err)
	}
	return claims
}

func TestMintTokenClaims(t *testing.T) {
	key := testKey(t)
	exp := time.Now().Add(time.Hour).Truncate(time.Second)
	token, err := MintToken(key, "lease-1", "plc-abc", Grant{Pull: true, Push: true}, exp)
	if err != nil {
		t.Fatal(err)
	}
	claims := parseClaims(t, key, token)

	if claims["sub"] != "lease-1" {
		t.Errorf("sub = %v", claims["sub"])
	}
	if got := int64(claims["exp"].(float64)); got != exp.Unix() {
		t.Errorf("exp = %d, want %d", got, exp.Unix())
	}
	if nbf, ok := claims["nbf"]; ok {
		t.Errorf("nbf = %v, want none: attic checks it with no clock tolerance", nbf)
	}
	attic, ok := claims["https://jwt.attic.rs/v1"].(map[string]any)
	if !ok {
		t.Fatalf("no attic claim in %v", claims)
	}
	caches := attic["caches"].(map[string]any)
	if len(caches) != 1 {
		t.Fatalf("caches = %v, want exactly one", caches)
	}
	perms, ok := caches["plc-abc"].(map[string]any)
	if !ok {
		t.Fatalf("caches = %v, want plc-abc", caches)
	}
	want := map[string]any{"r": 1.0, "w": 1.0}
	if len(perms) != len(want) || perms["r"] != want["r"] || perms["w"] != want["w"] {
		t.Errorf("perms = %v, want %v", perms, want)
	}
}

func TestMintTokenGrantFieldsMapToAtticBits(t *testing.T) {
	key := testKey(t)
	tests := []struct {
		grant Grant
		bit   string
	}{
		{Grant{Pull: true}, "r"},
		{Grant{Push: true}, "w"},
		{Grant{Delete: true}, "d"},
		{Grant{Create: true}, "cc"},
		{Grant{ConfigureCache: true}, "cr"},
		{Grant{ConfigureRetention: true}, "cq"},
	}
	for _, tt := range tests {
		token, err := MintToken(key, "s", "c", tt.grant, time.Now().Add(time.Minute))
		if err != nil {
			t.Fatal(err)
		}
		attic := parseClaims(t, key, token)["https://jwt.attic.rs/v1"].(map[string]any)
		perms := attic["caches"].(map[string]any)["c"].(map[string]any)
		if len(perms) != 1 || perms[tt.bit] != 1.0 {
			t.Errorf("grant %+v = %v, want only %q", tt.grant, perms, tt.bit)
		}
	}
}

func TestMintTokenRejectsPatternsAndBadInput(t *testing.T) {
	key := testKey(t)
	exp := time.Now().Add(time.Minute)
	for _, cache := range []string{"*", "plc-*", "a?b", "", "-lead", strings.Repeat("a", 51), "a b"} {
		if token, err := MintToken(key, "s", cache, Grant{Pull: true}, exp); err == nil {
			t.Errorf("MintToken for cache %q = %q; want an error", cache, token)
		}
	}
	if _, err := MintToken(key, "s", "c", Grant{Pull: true}, time.Now().Add(-time.Minute)); err == nil {
		t.Error("an expired deadline minted a token")
	}
	if _, err := MintToken(key, "s", "c", Grant{}, exp); err == nil {
		t.Error("an empty grant minted a token")
	}
	if _, err := MintToken(nil, "s", "c", Grant{Pull: true}, exp); err == nil {
		t.Error("a nil key minted a token")
	}
}