ArchiveBox/archivebox/api/auth.py

140 lines
5.2 KiB
Python
Raw Permalink Normal View History

__package__ = 'archivebox.api'
2024-09-03 17:16:44 -04:00
from typing import Any, Optional, cast
from datetime import timedelta
from django.http import HttpRequest
2024-09-03 17:16:44 -04:00
from django.utils import timezone
from django.contrib.auth import login
2024-04-09 19:29:24 -04:00
from django.contrib.auth import authenticate
from django.contrib.auth.models import AbstractBaseUser
2024-04-09 19:29:24 -04:00
from ninja.security import HttpBearer, APIKeyQuery, APIKeyHeader, HttpBasicAuth, django_auth_superuser
2024-09-03 17:16:44 -04:00
from ninja.errors import HttpError
def get_or_create_api_token(user):
from api.models import APIToken
if user and user.is_superuser:
api_tokens = APIToken.objects.filter(created_by_id=user.pk, expires__gt=timezone.now())
if api_tokens.exists():
# unexpired token exists, use it
api_token = api_tokens.last()
else:
# does not exist, create a new one
api_token = APIToken.objects.create(created_by_id=user.pk, expires=timezone.now() + timedelta(days=30))
assert api_token.is_valid(), f"API token is not valid {api_token}"
return api_token
return None
2024-04-09 19:29:24 -04:00
def auth_using_token(token, request: Optional[HttpRequest]=None) -> Optional[AbstractBaseUser]:
"""Given an API token string, check if a corresponding non-expired APIToken exists, and return its user"""
from api.models import APIToken # lazy import model to avoid loading it at urls.py import time
user = None
2024-04-09 19:29:24 -04:00
2024-09-03 17:16:44 -04:00
submitted_empty_form = str(token).strip() in ('string', '', 'None', 'null')
if not submitted_empty_form:
2024-04-09 19:29:24 -04:00
try:
token = APIToken.objects.get(token=token)
if token.is_valid():
2024-08-20 21:31:21 -04:00
user = token.created_by
2024-09-03 17:16:44 -04:00
request._api_token = token
except APIToken.DoesNotExist:
2024-04-09 19:29:24 -04:00
pass
if not user:
2024-09-03 17:16:44 -04:00
# print('[❌] Failed to authenticate API user using API Key:', request)
2024-09-03 04:21:13 -04:00
return None
2024-09-03 17:16:44 -04:00
2024-09-03 04:21:13 -04:00
return cast(AbstractBaseUser, user)
2024-04-09 19:29:24 -04:00
def auth_using_password(username, password, request: Optional[HttpRequest]=None) -> Optional[AbstractBaseUser]:
"""Given a username and password, check if they are valid and return the corresponding user"""
user = None
submitted_empty_form = (username, password) in (('string', 'string'), ('', ''), (None, None))
2024-09-03 17:16:44 -04:00
if not submitted_empty_form:
user = authenticate(
username=username,
password=password,
)
if not user:
2024-09-03 17:16:44 -04:00
# print('[❌] Failed to authenticate API user using API Key:', request)
2024-08-20 21:31:21 -04:00
user = None
2024-08-20 21:31:21 -04:00
return cast(AbstractBaseUser | None, user)
### Base Auth Types
2024-09-03 17:16:44 -04:00
class APITokenAuthCheck:
"""The base class for authentication methods that use an api.models.APIToken"""
def authenticate(self, request: HttpRequest, key: Optional[str]=None) -> Optional[AbstractBaseUser]:
2024-09-03 17:16:44 -04:00
request.user = auth_using_token(
token=key,
request=request,
)
2024-09-03 17:16:44 -04:00
if request.user and request.user.pk:
# Don't set cookie/persist login ouside this erquest, user may be accessing the API from another domain (CSRF/CORS):
# login(request, request.user, backend='django.contrib.auth.backends.ModelBackend')
request._api_auth_method = self.__class__.__name__
if not request.user.is_superuser:
raise HttpError(403, 'Valid API token but User does not have permission (make sure user.is_superuser=True)')
return request.user
class UserPassAuthCheck:
"""The base class for authentication methods that use a username & password"""
def authenticate(self, request: HttpRequest, username: Optional[str]=None, password: Optional[str]=None) -> Optional[AbstractBaseUser]:
2024-09-03 17:16:44 -04:00
request.user = auth_using_password(
username=username,
password=password,
request=request,
)
2024-09-03 17:16:44 -04:00
if request.user and request.user.pk:
# Don't set cookie/persist login ouside this erquest, user may be accessing the API from another domain (CSRF/CORS):
# login(request, request.user, backend='django.contrib.auth.backends.ModelBackend')
request._api_auth_method = self.__class__.__name__
if not request.user.is_superuser:
raise HttpError(403, 'Valid API token but User does not have permission (make sure user.is_superuser=True)')
return request.user
### Django-Ninja-Provided Auth Methods
class HeaderTokenAuth(APITokenAuthCheck, APIKeyHeader):
"""Allow authenticating by passing X-API-Key=xyz as a request header"""
param_name = "X-ArchiveBox-API-Key"
class BearerTokenAuth(APITokenAuthCheck, HttpBearer):
"""Allow authenticating by passing Bearer=xyz as a request header"""
pass
class QueryParamTokenAuth(APITokenAuthCheck, APIKeyQuery):
"""Allow authenticating by passing api_key=xyz as a GET/POST query parameter"""
param_name = "api_key"
class UsernameAndPasswordAuth(UserPassAuthCheck, HttpBasicAuth):
"""Allow authenticating by passing username & password via HTTP Basic Authentication (not recommended)"""
pass
2024-04-09 19:29:24 -04:00
### Enabled Auth Methods
2024-04-09 19:29:24 -04:00
API_AUTH_METHODS = [
HeaderTokenAuth(),
BearerTokenAuth(),
QueryParamTokenAuth(),
# django_auth_superuser, # django admin cookie auth, not secure to use with csrf=False
UsernameAndPasswordAuth(),
]