litellm - 💡(How to fix) Fix [Bug]: role_permissions.models in JWT auth does not honor wildcards (e.g. bedrock-claude-*, *) [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

@staticmethod def can_rbac_role_call_model( rbac_role: RBAC_ROLES, general_settings: dict, model: Optional[str], ) -> Literal[True]: """ Checks if user is allowed to access the model, based on their role. """ role_based_models = get_role_based_models( rbac_role=rbac_role, general_settings=general_settings ) if role_based_models is None or model is None: return True

if model not in role_based_models:
    raise HTTPException(
        status_code=403,
        detail=f"Role={rbac_role} not allowed to call model={model}. Allowed models={role_based_models}",
    )

return True

Root Cause

  1. Observe the 403 above. The model name clearly matches the bedrock-claude-* pattern, but the request is rejected because the check is exact-string only.

Fix Action

Fixed

Code Example

403: Role=internal_user not allowed to call model=bedrock-claude-draft-rep-sonnet.
     Allowed models=['bedrock-claude-*']

---

@staticmethod
def can_rbac_role_call_model(
    rbac_role: RBAC_ROLES,
    general_settings: dict,
    model: Optional[str],
) -> Literal[True]:
    """
    Checks if user is allowed to access the model, based on their role.
    """
    role_based_models = get_role_based_models(
        rbac_role=rbac_role, general_settings=general_settings
    )
    if role_based_models is None or model is None:
        return True

    if model not in role_based_models:
        raise HTTPException(
            status_code=403,
            detail=f"Role={rbac_role} not allowed to call model={model}. Allowed models={role_based_models}",
        )

    return True

---

general_settings:
     enable_jwt_auth: true
     litellm_jwtauth:
       user_id_jwt_field: "preferred_username"
       user_id_upsert: true
       user_roles_jwt_field: "resource_access.litellm.roles"
       user_allowed_roles: ["proxy_admin", "internal_user", "internal_user_viewer"]
       enforce_rbac: true
     role_permissions:
       - role: "internal_user"
         models: ["bedrock-claude-*"]
       - role: "proxy_admin"
         models: ["*"]

---

def test_can_rbac_role_call_model_wildcard():
    from litellm.proxy.auth.handle_jwt import JWTAuthManager
    from litellm.proxy._types import RoleBasedPermissions, LitellmUserRoles

    perms = [
        RoleBasedPermissions(
            role=LitellmUserRoles.INTERNAL_USER,
            models=["bedrock-claude-*"],
        ),
        RoleBasedPermissions(
            role=LitellmUserRoles.PROXY_ADMIN,
            models=["*"],
        ),
    ]

    assert JWTAuthManager.can_rbac_role_call_model(
        rbac_role=LitellmUserRoles.INTERNAL_USER,
        general_settings={"role_permissions": perms},
        model="bedrock-claude-draft-rep-sonnet",
    )
    assert JWTAuthManager.can_rbac_role_call_model(
        rbac_role=LitellmUserRoles.PROXY_ADMIN,
        general_settings={"role_permissions": perms},
        model="any-model-name",
    )

---

HTTP/1.1 403 Forbidden
{"error":{"message":"Role=internal_user not allowed to call model=bedrock-claude-draft-rep-sonnet. Allowed models=['bedrock-claude-*']","type":"auth_error","param":"None","code":"403"}}

---

if is_admin:
    return LitellmUserRoles.PROXY_ADMIN
elif self.get_team_id(token=token, default_value=None) is not None:
    return LitellmUserRoles.TEAM
elif self.get_user_id(token=token, default_value=None) is not None:
    return LitellmUserRoles.INTERNAL_USER
elif user_roles is not None and self.is_allowed_user_role(
    user_roles=user_roles
):
    return LitellmUserRoles.INTERNAL_USER
elif rbac_role := self._rbac_role_from_role_mapping(token=token):
    return rbac_role
RAW_BUFFERClick to expand / collapse

What happened?

When general_settings.enable_jwt_auth: true and enforce_rbac: true, the model gate in JWTAuthManager.can_rbac_role_call_model performs an exact-string membership check against role_permissions[].models. Wildcard patterns that work everywhere else in LiteLLM (team models, key models, model access groups, provider/* patterns) are not expanded here. As a result:

  • models: ["bedrock-claude-*"] does not allow bedrock-claude-draft-rep-sonnet.
  • models: ["*"] does not allow any concrete model name — it only matches the literal string "*".

A request that should be allowed by the role's pattern is rejected with:

403: Role=internal_user not allowed to call model=bedrock-claude-draft-rep-sonnet.
     Allowed models=['bedrock-claude-*']

Expected: role_permissions[].models should support the same wildcard / access-group / provider/* semantics as team and key model gating, so bedrock-claude-* matches bedrock-claude-draft-rep-sonnet and * matches anything.

Where the bug lives (litellm/proxy/auth/handle_jwt.py, can_rbac_role_call_model):

@staticmethod
def can_rbac_role_call_model(
    rbac_role: RBAC_ROLES,
    general_settings: dict,
    model: Optional[str],
) -> Literal[True]:
    """
    Checks if user is allowed to access the model, based on their role.
    """
    role_based_models = get_role_based_models(
        rbac_role=rbac_role, general_settings=general_settings
    )
    if role_based_models is None or model is None:
        return True

    if model not in role_based_models:
        raise HTTPException(
            status_code=403,
            detail=f"Role={rbac_role} not allowed to call model={model}. Allowed models={role_based_models}",
        )

    return True

The model not in role_based_models line is a literal Python list-membership check. No fnmatch, no _check_wildcard_routing, no model_in_access_group. The existing test tests/proxy_unit_tests/test_user_api_key_auth.py::test_can_rbac_role_call_model only asserts exact-match behavior, so this bug is not currently covered.

The same pattern appears in can_rbac_role_call_route if route patterns are intended to be wildcardable — flagging as a likely sibling concern.

Steps to Reproduce

  1. config.yaml:

    general_settings:
      enable_jwt_auth: true
      litellm_jwtauth:
        user_id_jwt_field: "preferred_username"
        user_id_upsert: true
        user_roles_jwt_field: "resource_access.litellm.roles"
        user_allowed_roles: ["proxy_admin", "internal_user", "internal_user_viewer"]
        enforce_rbac: true
      role_permissions:
        - role: "internal_user"
          models: ["bedrock-claude-*"]
        - role: "proxy_admin"
          models: ["*"]
  2. Send a POST /v1/chat/completions with {"model": "bedrock-claude-draft-rep-sonnet", ...} and a JWT whose RBAC role resolves to internal_user.

  3. Observe the 403 above. The model name clearly matches the bedrock-claude-* pattern, but the request is rejected because the check is exact-string only.

A unit-test repro (both assertions fail today; both should pass):

def test_can_rbac_role_call_model_wildcard():
    from litellm.proxy.auth.handle_jwt import JWTAuthManager
    from litellm.proxy._types import RoleBasedPermissions, LitellmUserRoles

    perms = [
        RoleBasedPermissions(
            role=LitellmUserRoles.INTERNAL_USER,
            models=["bedrock-claude-*"],
        ),
        RoleBasedPermissions(
            role=LitellmUserRoles.PROXY_ADMIN,
            models=["*"],
        ),
    ]

    assert JWTAuthManager.can_rbac_role_call_model(
        rbac_role=LitellmUserRoles.INTERNAL_USER,
        general_settings={"role_permissions": perms},
        model="bedrock-claude-draft-rep-sonnet",
    )
    assert JWTAuthManager.can_rbac_role_call_model(
        rbac_role=LitellmUserRoles.PROXY_ADMIN,
        general_settings={"role_permissions": perms},
        model="any-model-name",
    )

Relevant log output

HTTP/1.1 403 Forbidden
{"error":{"message":"Role=internal_user not allowed to call model=bedrock-claude-draft-rep-sonnet. Allowed models=['bedrock-claude-*']","type":"auth_error","param":"None","code":"403"}}

What part of LiteLLM is this about?

Proxy

Suggested fix

Reuse the wildcard-aware matcher used elsewhere in the proxy. Concretely, replace the membership check with a helper that:

  1. Returns True if "*" is in role_based_models.
  2. Otherwise, for each pattern in role_based_models, returns True if fnmatch.fnmatch(model, pattern) — handles both bedrock-claude-* and provider/* cases.
  3. Optionally, also consults access groups via model_in_access_group for parity with team-level gating.

Happy to put up a PR.

Additional context (separate but related observation)

While debugging, we also noticed that JWTHandler.get_rbac_role() short-circuits to INTERNAL_USER whenever user_id_jwt_field resolves to a non-None value — before it ever consults user_roles_jwt_field / user_allowed_roles:

if is_admin:
    return LitellmUserRoles.PROXY_ADMIN
elif self.get_team_id(token=token, default_value=None) is not None:
    return LitellmUserRoles.TEAM
elif self.get_user_id(token=token, default_value=None) is not None:
    return LitellmUserRoles.INTERNAL_USER
elif user_roles is not None and self.is_allowed_user_role(
    user_roles=user_roles
):
    return LitellmUserRoles.INTERNAL_USER
elif rbac_role := self._rbac_role_from_role_mapping(token=token):
    return rbac_role

This means user_roles_jwt_field is effectively dead config for any deployment that also sets user_id_jwt_field (e.g. Keycloak deployments using preferred_username). A proxy_admin value carried in the roles claim never elevates the RBAC role — only admin_jwt_scope matching the scope claim can. If this is intentional, it should be documented; if not, the resolver should consult user_roles before falling through to INTERNAL_USER. Filing as a separate concern but happy to fold it in if preferred.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

litellm - 💡(How to fix) Fix [Bug]: role_permissions.models in JWT auth does not honor wildcards (e.g. bedrock-claude-*, *) [1 pull requests]