Files
homelab-frontend/internal/proxy/machine_identity_test.go
T
Admin Bot f8788335b8
CI / CI (pull_request) Successful in 2m16s
CI / CI (push) Successful in 3m29s
feat(auth): authorise an allow-listed machine identity without a roles claim
ZITADEL does not put roles into a machine-user access token on this instance. The
grant exists, the scope asks for urn:zitadel:iam:org:project:roles, and the claim
still never arrives; a colon-free role key makes no difference either. A service
account therefore cannot satisfy requiredCapability by any configuration.

auth.allowListedMachineIdentities admits such a caller when its client_id appears in
zitadelClientIds. The caller is still authenticated: it signed an assertion with a
private key whose public half ZITADEL holds. The allow-list entry is a reviewed line
of configuration, not a wildcard.

The new path is off by default, and a token that does state its roles is judged by the
role check as before, so the weaker rule never applies twice.
2026-10-27 23:38:48 -07:00

66 lines
2.2 KiB
Go

package proxy
import (
"testing"
"github.com/golang-jwt/jwt/v5"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
)
func handlerWith(allow bool, ids ...string) *Handler {
return &Handler{config: &config.Config{Auth: config.AuthConfig{
AllowListedMachineIdentities: allow,
ZitadelClientIDs: ids,
}}}
}
func TestAllowListedMachineIdentityAdmitsANamedClientWithoutRoles(t *testing.T) {
h := handlerWith(true, "poimen-llm")
claims := jwt.MapClaims{"client_id": "poimen-llm", "sub": "397071051344839054"}
if !h.allowListedMachineIdentity(claims) {
t.Fatalf("an allow-listed machine client must be admitted when the token has no roles")
}
}
func TestAllowListedMachineIdentityRefusesAnUnknownClient(t *testing.T) {
h := handlerWith(true, "poimen-llm")
claims := jwt.MapClaims{"client_id": "someone-else"}
if h.allowListedMachineIdentity(claims) {
t.Fatalf("a client that is not on the list must not be admitted")
}
}
func TestAllowListedMachineIdentityRefusesWhenTheTokenStatesRoles(t *testing.T) {
h := handlerWith(true, "poimen-llm")
claims := jwt.MapClaims{
"client_id": "poimen-llm",
"urn:zitadel:iam:org:project:roles": map[string]interface{}{"memory:read": nil},
}
if h.allowListedMachineIdentity(claims) {
t.Fatalf("a token with roles must be judged by the role check, not by the allow-list")
}
}
func TestAllowListedMachineIdentityIsOffByDefault(t *testing.T) {
h := handlerWith(false, "poimen-llm")
claims := jwt.MapClaims{"client_id": "poimen-llm"}
if h.allowListedMachineIdentity(claims) {
t.Fatalf("without the setting the gateway must fall back to the role check only")
}
}
func TestRolesClaimReadsBothShapes(t *testing.T) {
asMap := jwt.MapClaims{"urn:zitadel:iam:org:project:roles": map[string]interface{}{"llm:inference": nil}}
if got := rolesClaim(asMap); len(got) != 1 || got[0] != "llm:inference" {
t.Fatalf("map shape must yield the role names, got %v", got)
}
asList := jwt.MapClaims{"roles": []interface{}{"llm:inference", "memory:read"}}
if got := rolesClaim(asList); len(got) != 2 {
t.Fatalf("list shape must yield every role, got %v", got)
}
if got := rolesClaim(jwt.MapClaims{}); len(got) != 0 {
t.Fatalf("a token with no roles must yield nothing, got %v", got)
}
}