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.
This commit was merged in pull request #71.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user