Files
summercms/modules/pier/fingerprint_test.go
Jakub Zych f604e6de64 fix(19.7): RA-WR-02 stop a shared stateless server flapping its tool set
The identity fingerprint no longer includes token ids: guards decide on
scopes, subjects and the ceiling, and an OAuth access token's id changes on
every refresh, so a rotation alone no longer re-filters a cached server.

The stateless path's refresh only adds tools and prompts for a fingerprint
new to the entry (remembered until a fan-out recompute) and never removes:
one server serves every live token of a binding, and removing on each
alternation between a wider and a narrower token pulled a tool from under
the wider token's call. The receiving middleware still filters every list
and call per request; fan-out recomputes still narrow. The stateful path
keeps its exact widen, a session serves one client.

TestFingerprintIgnoresTokenID pins the fingerprint; TestStatelessSharedBinding
blocks the wide token's call after its server was chosen, sends a request
of the narrow token and asserts the call still succeeds (it failed with
"unknown tool" before).
2026-10-23 20:05:08 +02:00

35 lines
1.3 KiB
Go

package pier
import (
"testing"
"git.golem15.com/golem15/summercms/modules/pact"
)
// TestFingerprintIgnoresTokenID pins RA-WR-02: an OAuth refresh changes the
// access-token id only, which must not re-filter a cached server, while a
// change of scopes, subjects or ceiling still does.
func TestFingerprintIgnoresTokenID(t *testing.T) {
id := func(token uint, scopes ...string) Identity {
return Identity{
Scopes: scopes,
Binding: "oauth:c1:7",
TokenKind: TokenKindOAuth,
ceiling: true,
Frontend: &pact.MCPSubject{Guard: "user", UserID: 7, ClientID: "c1", TokenID: token, Scopes: scopes},
Backend: &pact.MCPSubject{Guard: BackendGuard, UserID: 42, ClientID: "c1", TokenID: token, Scopes: scopes},
}
}
if a, b := id(1, "read", "backend").fingerprint(), id(2, "read", "backend").fingerprint(); a != b {
t.Fatalf("a token rotation changed the fingerprint:\n%s\n%s", a, b)
}
if a, b := id(1, "read", "backend").fingerprint(), id(1, "read", "write", "backend").fingerprint(); a == b {
t.Fatal("a scope change kept the fingerprint")
}
noBackend := id(1, "read", "backend")
noBackend.Backend = nil
if noBackend.fingerprint() == id(1, "read", "backend").fingerprint() {
t.Fatal("dropping the backend subject kept the fingerprint")
}
}