Files
2021-02-07 09:45:03 +01:00

197 lines
7.0 KiB
Python

import re
from inspect import Parameter, Signature, signature
from typing import Optional, Sequence
from deprecated import deprecated
from fastapi import Depends, HTTPException, status
from makefun import with_signature
from fastapi_users.authentication.base import BaseAuthentication # noqa: F401
from fastapi_users.authentication.cookie import CookieAuthentication # noqa: F401
from fastapi_users.authentication.jwt import JWTAuthentication # noqa: F401
from fastapi_users.db import BaseUserDatabase
from fastapi_users.models import BaseUserDB
INVALID_CHARS_PATTERN = re.compile(r"[^0-9a-zA-Z_]")
INVALID_LEADING_CHARS_PATTERN = re.compile(r"^[^a-zA-Z_]+")
def name_to_variable_name(name: str) -> str:
"""Transform a backend name string into a string safe to use as variable name."""
name = re.sub(INVALID_CHARS_PATTERN, "", name)
name = re.sub(INVALID_LEADING_CHARS_PATTERN, "", name)
return name
class DuplicateBackendNamesError(Exception):
pass
class Authenticator:
"""
Provides dependency callables to retrieve authenticated user.
It performs the authentication against a list of backends
defined by the end-developer. The first backend yielding a user wins.
If no backend yields a user, an HTTPException is raised.
:param backends: List of authentication backends.
:param user_db: Database adapter instance.
"""
backends: Sequence[BaseAuthentication]
user_db: BaseUserDatabase
def __init__(
self, backends: Sequence[BaseAuthentication], user_db: BaseUserDatabase
):
self.backends = backends
self.user_db = user_db
self.get_current_user = self._deprecated_current_user("get_current_user")
self.get_current_active_user = self._deprecated_current_user(
"get_current_active_user", active=True
)
self.get_current_verified_user = self._deprecated_current_user(
"get_current_verified_user", active=True, verified=True
)
self.get_current_superuser = self._deprecated_current_user(
"get_current_superuser", active=True, superuser=True
)
self.get_current_verified_superuser = self._deprecated_current_user(
"get_current_verified_superuser",
active=True,
verified=True,
superuser=True,
)
self.get_optional_current_user = self._deprecated_current_user(
"get_optional_current_user", optional=True
)
self.get_optional_current_active_user = self._deprecated_current_user(
"get_optional_current_active_user", optional=True, active=True
)
self.get_optional_current_verified_user = self._deprecated_current_user(
"get_optional_current_verified_user",
optional=True,
active=True,
verified=True,
)
self.get_optional_current_superuser = self._deprecated_current_user(
"get_optional_current_superuser", optional=True, active=True, superuser=True
)
self.get_optional_current_verified_superuser = self._deprecated_current_user(
"get_optional_current_verified_superuser",
optional=True,
active=True,
verified=True,
superuser=True,
)
def current_user(
self,
optional: bool = False,
active: bool = False,
verified: bool = False,
superuser: bool = False,
):
"""
Return a dependency callable to retrieve currently authenticated user.
:param optional: If `True`, `None` is returned if there is no authenticated user
or if it doesn't pass the other requirements.
Otherwise, throw `401 Unauthorized`. Defaults to `False`.
Otherwise, an exception is raised. Defaults to `False`.
:param active: If `True`, throw `401 Unauthorized` if
the authenticated user is inactive. Defaults to `False`.
:param verified: If `True`, throw `401 Unauthorized` if
the authenticated user is not verified. Defaults to `False`.
:param superuser: If `True`, throw `403 Forbidden` if
the authenticated user is not a superuser. Defaults to `False`.
"""
# Here comes some blood magic 🧙‍♂️
# Thank to "makefun", we are able to generate callable
# with a dynamic number of dependencies at runtime.
# This way, each security schemes are detected by the OpenAPI generator.
try:
parameters = [
Parameter(
name=name_to_variable_name(backend.name),
kind=Parameter.POSITIONAL_OR_KEYWORD,
default=Depends(backend.scheme), # type: ignore
)
for backend in self.backends
]
signature = Signature(parameters)
except ValueError:
raise DuplicateBackendNamesError()
@with_signature(signature)
async def current_user_dependency(*args, **kwargs):
return await self._authenticate(
*args,
optional=optional,
active=active,
verified=verified,
superuser=superuser,
**kwargs
)
return current_user_dependency
def _deprecated_current_user(
self,
func_name: str,
optional: bool = False,
active: bool = False,
verified: bool = False,
superuser: bool = False,
):
current_user_dependency = self.current_user(
optional, active, verified, superuser
)
@deprecated(
version="5.1.0",
reason=(
"You should call `current_user` with your own set of parameters. "
"See: "
"https://frankie567.github.io/fastapi-users/usage/dependency-callables/"
),
)
@with_signature(signature(current_user_dependency), func_name=func_name)
async def deprecated_current_user_dependency(*args, **kwargs):
return await current_user_dependency(*args, **kwargs)
return deprecated_current_user_dependency
async def _authenticate(
self,
*args,
optional: bool = False,
active: bool = False,
verified: bool = False,
superuser: bool = False,
**kwargs
) -> Optional[BaseUserDB]:
user: Optional[BaseUserDB] = None
for backend in self.backends:
token: str = kwargs[name_to_variable_name(backend.name)]
if token:
user = await backend(token, self.user_db)
if user:
break
status_code = status.HTTP_401_UNAUTHORIZED
if user:
if active and not user.is_active:
user = None
elif verified and not user.is_verified:
user = None
elif superuser and not user.is_superuser:
status_code = status.HTTP_403_FORBIDDEN
user = None
if not user and not optional:
raise HTTPException(status_code=status_code)
return user