mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
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:
parent
44ac50493e
commit
ac971d7e8b
3 changed files with 102 additions and 0 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue