test(auth_v2): pin the role allowlist on the OIDC-login and SAML-SSO paths

The veria review-response fix gates IdP-asserted roles through the same
per-provider allowlist on every role-bearing path, not just JWT bearer tokens.

- rbac: filter_claim_roles (the shared gate) denies a self-asserted role by
  default, filters to the allowlist, and only admits platform roles behind the
  explicit allow_platform_roles flag
- saml: a signed SSO assertion asserting platform_admin yields a session whose
  Principal has no roles by default, and is filtered to the allowlist when set
- oidc: the login callback's identity build (map userinfo -> gate roles ->
  session) denies platform_admin by default and filters to the allowlist

Full auth_v2 suite: 173 passing.
This commit is contained in:
Yassin Kortam 2026-06-10 20:06:04 -07:00
parent 44ac50493e
commit ac971d7e8b
3 changed files with 102 additions and 0 deletions

View file

@ -48,3 +48,48 @@ async def test_callback_seam_upserts_userinfo_into_store():
assert fetched is not None
assert fetched.external_id == "idp-subject-123"
assert fetched.user_name == "dana"
async def _oidc_login_session_roles(userinfo, provider):
# mirror the callback's identity build: map userinfo, gate roles, store a session,
# then authenticate + resolve through the same seam a request would
from litellm.proxy.auth_v2.authenticators import _apply_role_policy
from litellm.proxy.auth_v2.oidc.router import _mapped_claims
from litellm.proxy.auth_v2.session import SessionAuthenticator, SessionStore
from auth_v2_helpers import make_request
claims = _mapped_claims(userinfo)
_apply_role_policy(claims, provider)
store = SessionStore()
sid = store.create_session(
{"method": "oidc", "subject": userinfo["sub"], "claims": claims}
)
authenticator = SessionAuthenticator("litellm_session", store)
credential = await authenticator.authenticate(
make_request(cookies={"litellm_session": sid})
)
principal = await InMemoryIdentityStore().resolve(credential)
return [role.value for role in principal.roles]
async def test_oidc_login_platform_role_denied_by_default():
provider = OIDCProviderConfig(issuer="https://idp.example.com", audience=["x"])
userinfo = {
"sub": "u",
"email": "e@x.com",
"roles": ["platform_admin", "org_admin"],
}
assert await _oidc_login_session_roles(userinfo, provider) == []
async def test_oidc_login_roles_filtered_to_allowlist():
provider = OIDCProviderConfig(
issuer="https://idp.example.com", audience=["x"], allowed_roles=["org_admin"]
)
userinfo = {
"sub": "u",
"email": "e@x.com",
"roles": ["platform_admin", "org_admin"],
}
assert await _oidc_login_session_roles(userinfo, provider) == ["org_admin"]

View file

@ -136,3 +136,26 @@ def test_act_matcher_is_anchored(tmp_path):
viewer = _principal(roles=[Role.PLATFORM_VIEWER])
assert engine.enforce(viewer, "/x", "GET")
assert not engine.enforce(viewer, "/x", "GETX")
# --------------------------------------------------------------------------- #
# filter_claim_roles: the shared allowlist gate for JWT, OIDC-login and SAML
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize(
"roles,allowed,allow_platform,expected",
[
# default deny: a self-asserted role grants nothing
(["platform_admin", "org_admin"], [], False, []),
# allowlist filters; platform role excluded even if listed without the gate
(["platform_admin", "org_admin"], ["org_admin"], False, ["org_admin"]),
(["platform_admin"], ["platform_admin"], False, []),
# platform role only survives with the explicit gate
(["platform_admin"], ["platform_admin"], True, ["platform_admin"]),
],
)
def test_filter_claim_roles(roles, allowed, allow_platform, expected):
from litellm.proxy.auth_v2.rbac import filter_claim_roles
assert filter_claim_roles(roles, allowed, allow_platform) == expected

View file

@ -177,11 +177,45 @@ def _build_app(saml_env: SamlEnv):
"subject": principal.subject,
"auth_method": principal.auth_method.value,
"email": principal.user.email if principal.user else None,
"roles": [role.value for role in principal.roles],
}
return app, store
def _saml_session_roles(env, *, asserted_roles):
app, _ = _build_app(env)
client = TestClient(app)
acs = client.post(
"/auth/saml/acs",
data={
"SAMLResponse": env.mint_response(
identity={"email": ["alice@example.com"], "roles": asserted_roles}
)
},
follow_redirects=False,
)
client.cookies.set("litellm_session", acs.cookies["litellm_session"])
return client.get("/whoami").json()["roles"]
def test_saml_sso_platform_role_denied_by_default(saml_env):
# H1 on the SSO path: an IdP-asserted platform_admin grants nothing by default
roles = _saml_session_roles(
saml_env, asserted_roles=["platform_admin", "org_admin"]
)
assert roles == []
def test_saml_sso_roles_filtered_to_allowlist(saml_env):
env = SamlEnv(
config=saml_env.config.model_copy(update={"allowed_roles": ["org_admin"]}),
idp=saml_env.idp,
)
roles = _saml_session_roles(env, asserted_roles=["platform_admin", "org_admin"])
assert roles == ["org_admin"]
# --------------------------------------------------------------------------- #
# Metadata + login redirect
# --------------------------------------------------------------------------- #