mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-04-06 07:47:53 +10:00
144 lines
5.1 KiB
Python
144 lines
5.1 KiB
Python
__package__ = "archivebox.api"
|
|
|
|
from datetime import timedelta
|
|
|
|
from django.utils import timezone
|
|
from django.http import HttpRequest
|
|
from django.contrib.auth import authenticate
|
|
from django.contrib.auth.models import User
|
|
|
|
from ninja.security import HttpBearer, APIKeyQuery, APIKeyHeader, HttpBasicAuth
|
|
from ninja.errors import HttpError
|
|
|
|
|
|
def get_or_create_api_token(user: User | None):
|
|
from archivebox.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))
|
|
|
|
if api_token is None:
|
|
return None
|
|
assert api_token.is_valid(), f"API token is not valid {api_token}"
|
|
|
|
return api_token
|
|
return None
|
|
|
|
|
|
def auth_using_token(token: str | None, request: HttpRequest | None = None) -> User | None:
|
|
"""Given an API token string, check if a corresponding non-expired APIToken exists, and return its user"""
|
|
from archivebox.api.models import APIToken # lazy import model to avoid loading it at urls.py import time
|
|
|
|
user: User | None = None
|
|
|
|
submitted_empty_form = str(token).strip() in ("string", "", "None", "null")
|
|
if not submitted_empty_form:
|
|
try:
|
|
api_token = APIToken.objects.get(token=token)
|
|
if api_token.is_valid() and isinstance(api_token.created_by, User):
|
|
user = api_token.created_by
|
|
if request is not None:
|
|
setattr(request, "_api_token", api_token)
|
|
except APIToken.DoesNotExist:
|
|
pass
|
|
|
|
return user
|
|
|
|
|
|
def auth_using_password(username: str | None, password: str | None, request: HttpRequest | None = None) -> User | None:
|
|
"""Given a username and password, check if they are valid and return the corresponding user"""
|
|
user: User | None = None
|
|
|
|
submitted_empty_form = (username, password) in (("string", "string"), ("", ""), (None, None))
|
|
if not submitted_empty_form:
|
|
authenticated_user = authenticate(
|
|
username=username,
|
|
password=password,
|
|
)
|
|
if isinstance(authenticated_user, User):
|
|
user = authenticated_user
|
|
return user
|
|
|
|
|
|
### Base Auth Types
|
|
|
|
|
|
def _require_superuser(user: User | None, request: HttpRequest, auth_method: str) -> User | None:
|
|
if user and user.pk:
|
|
request.user = user
|
|
setattr(request, "_api_auth_method", auth_method)
|
|
if not user.is_superuser:
|
|
raise HttpError(403, "Valid credentials but User does not have permission (make sure user.is_superuser=True)")
|
|
return user
|
|
|
|
|
|
### Django-Ninja-Provided Auth Methods
|
|
|
|
|
|
class HeaderTokenAuth(APIKeyHeader):
|
|
"""Allow authenticating by passing X-API-Key=xyz as a request header"""
|
|
|
|
param_name = "X-ArchiveBox-API-Key"
|
|
|
|
def authenticate(self, request: HttpRequest, key: str | None) -> User | None:
|
|
return _require_superuser(auth_using_token(token=key, request=request), request, self.__class__.__name__)
|
|
|
|
|
|
class BearerTokenAuth(HttpBearer):
|
|
"""Allow authenticating by passing Bearer=xyz as a request header"""
|
|
|
|
def authenticate(self, request: HttpRequest, token: str) -> User | None:
|
|
return _require_superuser(auth_using_token(token=token, request=request), request, self.__class__.__name__)
|
|
|
|
|
|
class QueryParamTokenAuth(APIKeyQuery):
|
|
"""Allow authenticating by passing api_key=xyz as a GET/POST query parameter"""
|
|
|
|
param_name = "api_key"
|
|
|
|
def authenticate(self, request: HttpRequest, key: str | None) -> User | None:
|
|
return _require_superuser(auth_using_token(token=key, request=request), request, self.__class__.__name__)
|
|
|
|
|
|
class UsernameAndPasswordAuth(HttpBasicAuth):
|
|
"""Allow authenticating by passing username & password via HTTP Basic Authentication (not recommended)"""
|
|
|
|
def authenticate(self, request: HttpRequest, username: str, password: str) -> User | None:
|
|
return _require_superuser(
|
|
auth_using_password(username=username, password=password, request=request),
|
|
request,
|
|
self.__class__.__name__,
|
|
)
|
|
|
|
|
|
class DjangoSessionAuth:
|
|
"""Allow authenticating with existing Django session cookies (same-origin only)."""
|
|
|
|
def __call__(self, request: HttpRequest) -> User | None:
|
|
return self.authenticate(request)
|
|
|
|
def authenticate(self, request: HttpRequest, **kwargs) -> User | None:
|
|
user = getattr(request, "user", None)
|
|
if isinstance(user, User) and user.is_authenticated:
|
|
setattr(request, "_api_auth_method", self.__class__.__name__)
|
|
if not user.is_superuser:
|
|
raise HttpError(403, "Valid session but User does not have permission (make sure user.is_superuser=True)")
|
|
return user
|
|
return None
|
|
|
|
|
|
### Enabled Auth Methods
|
|
|
|
API_AUTH_METHODS = [
|
|
HeaderTokenAuth(),
|
|
BearerTokenAuth(),
|
|
QueryParamTokenAuth(),
|
|
# django_auth_superuser, # django admin cookie auth, not secure to use with csrf=False
|
|
]
|