mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
refactor(auth_v2): rename resolver classes, drop dead protocol boilerplate
Rename IdentityResolver -> Resolver and DbIdentityStore -> DbResolver so the names stop colliding with authentication; the package these move to is resolvers/, not identity/. Drop the IdentityStore union protocol, which was only used as DbResolver's base; DbResolver now inherits Resolver and ProvisioningStore directly. Drop the @runtime_checkable decorators on these protocols, which had no isinstance callers. ProvisioningStore keeps its name.
This commit is contained in:
parent
03f1c11e9b
commit
770ae5902c
6 changed files with 24 additions and 40 deletions
|
|
@ -46,7 +46,7 @@ always run first.
|
|||
`401` with a combined `WWW-Authenticate` challenge built from each scheme.
|
||||
|
||||
2. Resolve identity. The verified `Credential` is handed to the configured
|
||||
`IdentityResolver`, which builds the `Principal`. The DB resolver looks the subject up in
|
||||
`Resolver`, which builds the `Principal`. The DB resolver looks the subject up in
|
||||
the proxy's Prisma tables (key object, user, teams, org). A blocked key or unknown subject
|
||||
raises `401`/`403` here, before any route logic runs.
|
||||
|
||||
|
|
@ -87,8 +87,8 @@ built once at the composition root.
|
|||
dispatch to it by where its credential lives). `build_authenticators` constructs and orders
|
||||
them from `AuthConfig`. JWT verification for OIDC/OAuth2 is shared via `JWTVerifier`.
|
||||
|
||||
`resolvers.py` holds the `IdentityResolver` / `IdentityStore` protocols and the single
|
||||
`DbIdentityStore` implementation against Prisma. The store also handles SCIM user/group
|
||||
`resolvers.py` holds the `Resolver` and `ProvisioningStore` protocols and the single
|
||||
`DbResolver` implementation against Prisma. The store also handles SCIM user/group
|
||||
provisioning so a provisioned user is immediately resolvable. `utils.py` holds the pure
|
||||
SCIM/role-mapping helpers the store uses.
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ the right status and challenge header.
|
|||
## Adding things
|
||||
|
||||
A new credential scheme is a new `Authenticator` plus a branch in `build_authenticators`.
|
||||
A new identity backend is a new `IdentityStore`. A new authorization method (ReBAC, an
|
||||
A new identity backend is a new `Resolver` (also a `ProvisioningStore` if it provisions). A new authorization method (ReBAC, an
|
||||
external PDP) is a new `Authorizer` passed as `AuthSecurity(..., authorizer=...)`.
|
||||
Dependencies are injected at construction, so each of these is unit-testable with a fake
|
||||
in place of the real backend.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from litellm.proxy.auth_v2.config import (
|
|||
)
|
||||
from litellm.proxy.auth_v2.models import Principal
|
||||
from litellm.proxy.auth_v2.authorization import Role
|
||||
from litellm.proxy.auth_v2.resolvers import IdentityResolver, ProvisioningStore
|
||||
from litellm.proxy.auth_v2.resolvers import ProvisioningStore, Resolver
|
||||
from litellm.proxy.auth_v2.security import AuthSecurity
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -19,7 +19,7 @@ __all__ = [
|
|||
"AuthConfig",
|
||||
"Principal",
|
||||
"Role",
|
||||
"IdentityResolver",
|
||||
"Resolver",
|
||||
"ProvisioningStore",
|
||||
"ApiKeySchemeConfig",
|
||||
"HttpBasicConfig",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, List, Optional, Protocol, runtime_checkable
|
||||
from typing import TYPE_CHECKING, List, Optional, Protocol
|
||||
|
||||
from scim2_models import Group as ScimGroup
|
||||
from scim2_models import User as ScimUser
|
||||
|
|
@ -44,8 +44,7 @@ if TYPE_CHECKING:
|
|||
from litellm.proxy.utils import PrismaClient
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class IdentityResolver(Protocol):
|
||||
class Resolver(Protocol):
|
||||
async def resolve(self, credential: Credential) -> Principal:
|
||||
"""Resolve a verified credential to a Principal.
|
||||
|
||||
|
|
@ -59,7 +58,6 @@ class IdentityResolver(Protocol):
|
|||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ProvisioningStore(Protocol):
|
||||
async def upsert_user(self, user: ScimUser) -> ScimUser: ...
|
||||
async def get_user(self, resource_id: str) -> Optional[ScimUser]: ...
|
||||
|
|
@ -71,31 +69,20 @@ class ProvisioningStore(Protocol):
|
|||
async def list_groups(self, filter_expr: Optional[str]) -> List[ScimGroup]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class IdentityStore(IdentityResolver, ProvisioningStore, Protocol):
|
||||
"""An identity backend: resolves credentials and provisions SCIM users/groups.
|
||||
|
||||
This is the single interface every implementation satisfies (in-memory,
|
||||
database, ...). Resolution and provisioning live behind one store so a
|
||||
provisioned user is immediately resolvable.
|
||||
"""
|
||||
|
||||
|
||||
class DbIdentityStore(IdentityStore):
|
||||
class DbResolver(Resolver, ProvisioningStore):
|
||||
"""Resolves credentials against the proxy's Prisma tables and provisions
|
||||
SCIM users/groups into ``LiteLLM_UserTable`` / ``LiteLLM_TeamTable``.
|
||||
|
||||
The Prisma client and key cache are injected so this stays a plain object
|
||||
the composition root can build once the proxy DB is connected.
|
||||
Resolution and provisioning share one object so a provisioned user is
|
||||
immediately resolvable. The Prisma client and key cache are injected so this
|
||||
stays a plain object the composition root can build once the proxy DB is
|
||||
connected.
|
||||
"""
|
||||
|
||||
def __init__(self, prisma_client: "PrismaClient", cache: "DualCache") -> None:
|
||||
self._prisma = prisma_client
|
||||
self._cache = cache
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# IdentityResolver
|
||||
# ------------------------------------------------------------------ #
|
||||
async def resolve(self, credential: Credential) -> Principal:
|
||||
if credential.method == AuthMethod.API_KEY:
|
||||
return await self._resolve_api_key(credential)
|
||||
|
|
@ -238,9 +225,6 @@ class DbIdentityStore(IdentityStore):
|
|||
name = org.organization_alias if org is not None else None
|
||||
return OrganizationIdentity(id=user.organization_id, name=name)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# ProvisioningStore
|
||||
# ------------------------------------------------------------------ #
|
||||
async def upsert_user(self, user: ScimUser) -> ScimUser:
|
||||
repo = UserRepository(self._prisma)
|
||||
data = scim_user_to_db(user)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from litellm.proxy.auth_v2.authorization import (
|
|||
Role,
|
||||
)
|
||||
from litellm.proxy.auth_v2.authenticators.session import SessionAuthenticator
|
||||
from litellm.proxy.auth_v2.resolvers import IdentityResolver
|
||||
from litellm.proxy.auth_v2.resolvers import Resolver
|
||||
from litellm.proxy.auth_v2.sessions import (
|
||||
InMemorySessionStore,
|
||||
RedisSessionStore,
|
||||
|
|
@ -83,7 +83,7 @@ class AuthSecurity:
|
|||
def __init__(
|
||||
self,
|
||||
config: AuthConfig,
|
||||
resolver: IdentityResolver,
|
||||
resolver: Resolver,
|
||||
authorizer: Optional[Authorizer] = None,
|
||||
authenticators: Optional[List[Authenticator]] = None,
|
||||
basic_verifier: Optional[BasicAuthVerifier] = None,
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ from litellm.proxy.auth_v2.models import (
|
|||
PrincipalType,
|
||||
SecuritySchemeType,
|
||||
)
|
||||
from litellm.proxy.auth_v2.resolvers import DbIdentityStore
|
||||
from litellm.proxy.auth_v2.resolvers import DbResolver
|
||||
|
||||
|
||||
class _FakeCache:
|
||||
"""Stands in for the DualCache that get_key_object / get_user_object read.
|
||||
|
||||
Both helpers return a cache hit before touching the DB, so seeding this and
|
||||
injecting it into DbIdentityStore exercises the real resolver mapping without
|
||||
injecting it into DbResolver exercises the real resolver mapping without
|
||||
a database. A non-None prisma client is still required (the helpers guard on
|
||||
it); it is never reached on a hit.
|
||||
"""
|
||||
|
|
@ -39,8 +39,8 @@ class _FakeCache:
|
|||
_PRISMA_STUB = object()
|
||||
|
||||
|
||||
def _store(entries: Optional[Dict[str, object]] = None) -> DbIdentityStore:
|
||||
return DbIdentityStore(_PRISMA_STUB, _FakeCache(entries))
|
||||
def _store(entries: Optional[Dict[str, object]] = None) -> DbResolver:
|
||||
return DbResolver(_PRISMA_STUB, _FakeCache(entries))
|
||||
|
||||
|
||||
def _api_key_credential(raw: str) -> Credential:
|
||||
|
|
@ -112,7 +112,7 @@ async def test_api_key_lookup_is_keyed_on_hashed_token():
|
|||
raw = "sk-live-abc"
|
||||
key = UserAPIKeyAuth(token=hash_token(raw), user_id="u-1")
|
||||
# cache seeded under the RAW key, not its hash -> resolver hashes first -> miss
|
||||
store = DbIdentityStore(None, _FakeCache({raw: key}))
|
||||
store = DbResolver(None, _FakeCache({raw: key}))
|
||||
with pytest.raises(AuthError) as exc:
|
||||
await store.resolve(_api_key_credential(raw))
|
||||
assert exc.value.status_code == 401
|
||||
|
|
@ -130,7 +130,7 @@ async def test_blocked_key_is_rejected_403():
|
|||
|
||||
async def test_unknown_key_is_rejected_401():
|
||||
# cache miss + no prisma -> get_key_object raises -> resolver maps to 401
|
||||
store = DbIdentityStore(None, _FakeCache())
|
||||
store = DbResolver(None, _FakeCache())
|
||||
with pytest.raises(AuthError) as exc:
|
||||
await store.resolve(_api_key_credential("sk-live-unknown"))
|
||||
assert exc.value.status_code == 401
|
||||
|
|
@ -166,7 +166,7 @@ async def test_mtls_credential_resolves_to_service_account():
|
|||
client_certificate=ClientCertificate(subject_dn="CN=svc-a,O=Co"),
|
||||
)
|
||||
# service-account path does no identity lookup, so no cache/prisma needed
|
||||
principal = await DbIdentityStore(None, _FakeCache()).resolve(credential)
|
||||
principal = await DbResolver(None, _FakeCache()).resolve(credential)
|
||||
|
||||
assert principal.principal_type == PrincipalType.SERVICE_ACCOUNT
|
||||
assert principal.user is None
|
||||
|
|
|
|||
|
|
@ -55,10 +55,10 @@ class _FakeResolver:
|
|||
"""Resolver double for the security-layer tests.
|
||||
|
||||
These tests inject fully-formed Principals (arbitrary scopes/roles) keyed by
|
||||
API key, which the production DbIdentityStore cannot express; DbIdentityStore
|
||||
API key, which the production DbResolver cannot express; DbResolver
|
||||
has its own coverage in test_resolver.py. An API-key credential is looked up
|
||||
by its raw-key claim; anything else echoes the credential's subject. Returns a
|
||||
fresh Principal per the IdentityResolver contract.
|
||||
fresh Principal per the Resolver contract.
|
||||
"""
|
||||
|
||||
def __init__(self, by_key: Dict[str, Principal]) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue