fix(cabana): scope the token last_used_at throttle to the guard instance

- The process-global debounce map was keyed by token ID, so a stamp from one test database suppressed a later write in another and turned TestBackendTokenPostgres red depending on order
- Each compositeGuard now owns a stampThrottle; the backend guard and the ResolveBackend guard of one activation share it, so a single-app binary still writes last_used_at at most once a minute per token
- TestTokenLastUsedThrottlePerGuard pins the per-guard isolation
This commit is contained in:
2026-10-11 15:19:55 +02:00
parent 21fdcfcba6
commit b45af8ebbf
5 changed files with 74 additions and 15 deletions

View File

@@ -160,7 +160,6 @@ func TestBackendTokenGuard(t *testing.T) {
ID: 99, BackendUserID: 1, Name: "t", TokenHash: hash2, TokenHint: hint2,
Scopes: lagoon.Jsonable[[]string]{Data: []string{"demo"}, Valid: true},
})
lastStamp.Delete(uint(99))
g2 := newCompositeGuard(jwt, errStore, users, reg)
r = httptest.NewRequest(http.MethodGet, "/x", nil)
r.Header.Set("Authorization", "Bearer "+plain2)
@@ -183,7 +182,6 @@ func TestTokenLastUsedDebounce(t *testing.T) {
ID: 42, BackendUserID: 1, Name: "t", TokenHash: hash, TokenHint: hint,
Scopes: lagoon.Jsonable[[]string]{Data: []string{"demo"}, Valid: true},
})
lastStamp.Delete(uint(42))
store.stampN = 0
r := httptest.NewRequest(http.MethodGet, "/x", nil)
r.Header.Set("Authorization", "Bearer "+plain)
@@ -196,7 +194,7 @@ func TestTokenLastUsedDebounce(t *testing.T) {
if store.stampN != 1 {
t.Fatalf("within minute stamps = %d", store.stampN)
}
lastStamp.Store(uint(42), time.Now().UTC().Add(-2*time.Minute))
guard.stamps.last.Store(uint(42), time.Now().UTC().Add(-2*time.Minute))
if _, _, err := guard.AuthenticateCredential(r); err != nil {
t.Fatal(err)
}
@@ -205,6 +203,47 @@ func TestTokenLastUsedDebounce(t *testing.T) {
}
}
// TestTokenLastUsedThrottlePerGuard pins that the last_used_at debounce
// belongs to the guard: two guards over separate stores that both hold token
// ID 7 (fresh databases restart IDs) each stamp once.
func TestTokenLastUsedThrottlePerGuard(t *testing.T) {
reg := &Registry{tokenScopes: tracerCatalog()}
dev := &bouncer.Principal{ID: 1, Backend: true, PermissionGrants: map[string]bool{"acme.demo.access": true}}
users := stubUsers{byID: map[uint]*bouncer.Principal{1: dev}}
newSide := func() (*memTokenStore, *compositeGuard, *http.Request) {
store := &memTokenStore{byHash: map[string]*BackendAPIToken{}}
plain, hash, hint, err := mintAPIToken()
if err != nil {
t.Fatal(err)
}
store.put(&BackendAPIToken{
ID: 7, BackendUserID: 1, Name: "t", TokenHash: hash, TokenHint: hint,
Scopes: lagoon.Jsonable[[]string]{Data: []string{"demo"}, Valid: true},
})
r := httptest.NewRequest(http.MethodGet, "/x", nil)
r.Header.Set("Authorization", "Bearer "+plain)
return store, newCompositeGuard(&stubJWTGuard{}, store, users, reg), r
}
storeA, guardA, reqA := newSide()
storeB, guardB, reqB := newSide()
if _, _, err := guardA.AuthenticateCredential(reqA); err != nil {
t.Fatal(err)
}
if _, _, err := guardB.AuthenticateCredential(reqB); err != nil {
t.Fatal(err)
}
if storeA.stampN != 1 || storeB.stampN != 1 {
t.Fatalf("stamps A=%d B=%d, want 1 each", storeA.stampN, storeB.stampN)
}
if _, _, err := guardB.AuthenticateCredential(reqB); err != nil {
t.Fatal(err)
}
if storeB.stampN != 1 {
t.Fatalf("within-guard debounce: B stamps = %d, want 1", storeB.stampN)
}
}
func TestResolveBackend(t *testing.T) {
cat := tracerCatalog()
reg := &Registry{tokenScopes: cat}