auth.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. __package__ = 'archivebox.api'
  2. from typing import Optional, cast
  3. from datetime import timedelta
  4. from django.http import HttpRequest
  5. from django.utils import timezone
  6. from django.contrib.auth import authenticate
  7. from django.contrib.auth.models import AbstractBaseUser
  8. from ninja.security import HttpBearer, APIKeyQuery, APIKeyHeader, HttpBasicAuth
  9. from ninja.errors import HttpError
  10. def get_or_create_api_token(user):
  11. from api.models import APIToken
  12. if user and user.is_superuser:
  13. api_tokens = APIToken.objects.filter(created_by_id=user.pk, expires__gt=timezone.now())
  14. if api_tokens.exists():
  15. # unexpired token exists, use it
  16. api_token = api_tokens.last()
  17. else:
  18. # does not exist, create a new one
  19. api_token = APIToken.objects.create(created_by_id=user.pk, expires=timezone.now() + timedelta(days=30))
  20. assert api_token.is_valid(), f"API token is not valid {api_token}"
  21. return api_token
  22. return None
  23. def auth_using_token(token, request: Optional[HttpRequest]=None) -> Optional[AbstractBaseUser]:
  24. """Given an API token string, check if a corresponding non-expired APIToken exists, and return its user"""
  25. from api.models import APIToken # lazy import model to avoid loading it at urls.py import time
  26. user = None
  27. submitted_empty_form = str(token).strip() in ('string', '', 'None', 'null')
  28. if not submitted_empty_form:
  29. try:
  30. token = APIToken.objects.get(token=token)
  31. if token.is_valid():
  32. user = token.created_by
  33. request._api_token = token
  34. except APIToken.DoesNotExist:
  35. pass
  36. if not user:
  37. # print('[❌] Failed to authenticate API user using API Key:', request)
  38. return None
  39. return cast(AbstractBaseUser, user)
  40. def auth_using_password(username, password, request: Optional[HttpRequest]=None) -> Optional[AbstractBaseUser]:
  41. """Given a username and password, check if they are valid and return the corresponding user"""
  42. user = None
  43. submitted_empty_form = (username, password) in (('string', 'string'), ('', ''), (None, None))
  44. if not submitted_empty_form:
  45. user = authenticate(
  46. username=username,
  47. password=password,
  48. )
  49. if not user:
  50. # print('[❌] Failed to authenticate API user using API Key:', request)
  51. user = None
  52. return cast(AbstractBaseUser | None, user)
  53. ### Base Auth Types
  54. class APITokenAuthCheck:
  55. """The base class for authentication methods that use an api.models.APIToken"""
  56. def authenticate(self, request: HttpRequest, key: Optional[str]=None) -> Optional[AbstractBaseUser]:
  57. request.user = auth_using_token(
  58. token=key,
  59. request=request,
  60. )
  61. if request.user and request.user.pk:
  62. # Don't set cookie/persist login ouside this erquest, user may be accessing the API from another domain (CSRF/CORS):
  63. # login(request, request.user, backend='django.contrib.auth.backends.ModelBackend')
  64. request._api_auth_method = self.__class__.__name__
  65. if not request.user.is_superuser:
  66. raise HttpError(403, 'Valid API token but User does not have permission (make sure user.is_superuser=True)')
  67. return request.user
  68. class UserPassAuthCheck:
  69. """The base class for authentication methods that use a username & password"""
  70. def authenticate(self, request: HttpRequest, username: Optional[str]=None, password: Optional[str]=None) -> Optional[AbstractBaseUser]:
  71. request.user = auth_using_password(
  72. username=username,
  73. password=password,
  74. request=request,
  75. )
  76. if request.user and request.user.pk:
  77. # Don't set cookie/persist login ouside this erquest, user may be accessing the API from another domain (CSRF/CORS):
  78. # login(request, request.user, backend='django.contrib.auth.backends.ModelBackend')
  79. request._api_auth_method = self.__class__.__name__
  80. if not request.user.is_superuser:
  81. raise HttpError(403, 'Valid API token but User does not have permission (make sure user.is_superuser=True)')
  82. return request.user
  83. ### Django-Ninja-Provided Auth Methods
  84. class HeaderTokenAuth(APITokenAuthCheck, APIKeyHeader):
  85. """Allow authenticating by passing X-API-Key=xyz as a request header"""
  86. param_name = "X-ArchiveBox-API-Key"
  87. class BearerTokenAuth(APITokenAuthCheck, HttpBearer):
  88. """Allow authenticating by passing Bearer=xyz as a request header"""
  89. pass
  90. class QueryParamTokenAuth(APITokenAuthCheck, APIKeyQuery):
  91. """Allow authenticating by passing api_key=xyz as a GET/POST query parameter"""
  92. param_name = "api_key"
  93. class UsernameAndPasswordAuth(UserPassAuthCheck, HttpBasicAuth):
  94. """Allow authenticating by passing username & password via HTTP Basic Authentication (not recommended)"""
  95. pass
  96. ### Enabled Auth Methods
  97. API_AUTH_METHODS = [
  98. HeaderTokenAuth(),
  99. BearerTokenAuth(),
  100. QueryParamTokenAuth(),
  101. # django_auth_superuser, # django admin cookie auth, not secure to use with csrf=False
  102. UsernameAndPasswordAuth(),
  103. ]