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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user