~bigbes/sr-ht-spec

ref: bb3d22db80100b831492f6806f09f8bad3e141be sr-ht-spec/service/token_acl_test.go -rw-r--r-- 3.7 KiB
bb3d22db — Eugene Blikh web: git.sr.ht-style dashboard and unified nav brand 11 days 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
package service

import (
	"errors"
	"strings"
	"testing"

	"sourcecraft.dev/bigbes/sr-ht-spec/authn"
)

// The three token calls are owner-only, and these tests run against a database
// that cannot be reached: a refusal that needs a query is a refusal that would
// have leaked the inventory, or minted the row, before deciding.

func TestIssueAgentTokenIsOwnerOnly(t *testing.T) {
	svc, _ := newService(t)

	for name, p := range map[string]authn.Principal{
		"an agent":         {Kind: authn.KindAgent, Owner: "bigbes", Agent: "claude", Session: "s1"},
		"anonymous":        authn.Anonymous(),
		"a zero principal": {},
	} {
		token, row, err := svc.IssueAgentToken(t.Context(), p, "another")
		if !errors.Is(err, ErrForbidden) {
			t.Errorf("%s was not refused with ErrForbidden: %v", name, err)
		}
		if token != "" || row.ID != 0 {
			t.Errorf("%s got a token back: %q %+v", name, token, row)
		}
	}
}

// TestIssueAgentTokenRefusesAnAgentBeforeMinting is the property revocation
// depends on: an agent that could mint would survive having its own credential
// revoked, so "revoke the token" would stop being incident response.
func TestIssueAgentTokenRefusesAnAgentBeforeMinting(t *testing.T) {
	svc, _ := newService(t)
	agent := authn.Principal{Kind: authn.KindAgent, Owner: "bigbes", Agent: "claude", Session: "s1"}

	_, _, err := svc.IssueAgentToken(t.Context(), agent, "self-issued")
	if !errors.Is(err, ErrForbidden) {
		t.Fatalf("an agent minting a token was not refused: %v", err)
	}
	if !strings.Contains(err.Error(), "only the instance owner") {
		t.Errorf("the refusal does not say who may: %v", err)
	}
}

func TestListAndRevokeAreOwnerOnly(t *testing.T) {
	svc, _ := newService(t)
	agent := authn.Principal{Kind: authn.KindAgent, Owner: "bigbes", Agent: "claude", Session: "s1"}

	if _, err := svc.ListAgentTokens(t.Context(), agent); !errors.Is(err, ErrForbidden) {
		t.Errorf("an agent listing tokens was not refused: %v", err)
	}
	if err := svc.RevokeAgentToken(t.Context(), agent, 1); !errors.Is(err, ErrForbidden) {
		t.Errorf("an agent revoking a token was not refused: %v", err)
	}
}

// TestRevokeAgentTokenChecksTheIDBeforeTheDatabase keeps a mistyped id from
// becoming an UPDATE that matches nothing and reports "not found", which reads
// like the token is already gone.
func TestRevokeAgentTokenChecksTheIDBeforeTheDatabase(t *testing.T) {
	svc, _ := newService(t)
	owner := authn.Principal{Kind: authn.KindOwner, Owner: "bigbes"}

	for _, id := range []int{0, -1} {
		if err := svc.RevokeAgentToken(t.Context(), owner, id); !errors.Is(err, ErrInvalid) {
			t.Errorf("revoking id %d was not refused as invalid: %v", id, err)
		}
	}
}

func TestValidateTokenName(t *testing.T) {
	got, err := validateTokenName("  claude-code  ")
	if err != nil {
		t.Fatalf("validateTokenName: %v", err)
	}
	if got != "claude-code" {
		t.Errorf("name = %q want it trimmed", got)
	}

	for name, in := range map[string]string{
		"empty":                "",
		"only whitespace":      "   ",
		"a control character":  "claude\x00code",
		"a newline":            "claude\ncode",
		"longer than the caps": strings.Repeat("x", MaxTokenNameLen+1),
	} {
		if _, err := validateTokenName(in); !errors.Is(err, ErrInvalid) {
			t.Errorf("%s was accepted as a token name", name)
		}
	}
}

// TestIssueAgentTokenValidatesTheNameBeforeMinting keeps a rejected name from
// consuming entropy and, more importantly, from leaving a row whose label the
// owner cannot read in the listing.
func TestIssueAgentTokenValidatesTheNameBeforeMinting(t *testing.T) {
	svc, _ := newService(t)
	owner := authn.Principal{Kind: authn.KindOwner, Owner: "bigbes"}

	if _, _, err := svc.IssueAgentToken(t.Context(), owner, "  "); !errors.Is(err, ErrInvalid) {
		t.Errorf("a blank token name was accepted: %v", err)
	}
}