Source code for smarter.lib.drf.views.token_authentication_helpers

"""Django template and view helper functions for knox token authentication."""

from typing import Any, Union

from django.http import HttpResponseBase, HttpResponseForbidden
from rest_framework.authentication import SessionAuthentication
from rest_framework.generics import ListAPIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from smarter.apps.api.signals import api_request_completed, api_request_initiated
from smarter.common.utils import is_authenticated_request, smarter_build_absolute_uri
from smarter.lib import logging
from smarter.lib.django.request import SmarterRequestMixin
from smarter.lib.django.waffle import SmarterWaffleSwitches
from smarter.lib.drf.views.helpers import SmarterAuthenticatedPermissionClass

from ..token_authentication import SmarterTokenAuthentication

logger = logging.getSmarterLogger(__name__, any_switches=[SmarterWaffleSwitches.API_LOGGING])


# ------------------------------------------------------------------------------
# API Authenticated Views
# ------------------------------------------------------------------------------


[docs] class SmarterAuthenticatedAPIView(APIView, SmarterRequestMixin): """Smarter base class for DRF API detail views that require authentication. Does the following: - Adds SmarterRequestMixin to the view, so that base Smarter functionality is available to all subclasses. - Adds SmarterTokenAuthentication to the default SessionAuthentication for authentication. - Overrides Django's logic for initializing the request object to ensure that SmarterRequestMixin is fully initialized before any other logic runs. """ permission_classes = [SmarterAuthenticatedPermissionClass] authentication_classes = [SmarterTokenAuthentication, SessionAuthentication]
[docs] def __init__(self, *args, **kwargs): """Initialize the SmarterAuthenticatedAPIView.""" super().__init__(*args, **kwargs) self.request = kwargs.pop("request", None) user = kwargs.pop("user", None) account = kwargs.pop("account", None) user_profile = kwargs.pop("user_profile", None) SmarterRequestMixin.__init__( self, request=self.request, user=user, account=account, user_profile=user_profile, *args, **kwargs )
@property def formatted_class_name(self): """Helper method to get the formatted class name for logging.""" class_name = f"{__name__}.{SmarterAuthenticatedAPIView.__name__}" return self.formatted_text(class_name)
[docs] def setup(self, request: Request, *args, **kwargs): """Extend setup() DRF view method. Setup the view. This is called by Django before dispatch() and is used to set up the view for the request. Args: request (HttpRequest): The incoming HTTP request. """ # drf setup logic super().setup(request, *args, **kwargs) # go through our own request and account mixin setup logic self.smarter_request = request # overwrite the request object with our smarter_request object if not self.smarter_request: logger.warning( "%s.setup() - smarter_request is None, overwriting with request: %s", self.formatted_class_name, smarter_build_absolute_uri(request), ) self.smarter_request = request self.request = self.smarter_request api_request_initiated.send(sender=self.__class__, instance=self, request=request) logger.debug( "%s.setup() - finished for request: %s, user: %s, self.user: %s is_authenticated: %s", self.formatted_class_name, smarter_build_absolute_uri(request), request.user.username if request.user else "Anonymous", # type: ignore[assignment] self.user_profile, is_authenticated_request(request), )
[docs] def initial(self, request: Request, *args, **kwargs): """Extend initial() DRF view method. Initialize the view with the request and any additional arguments. This is the earliest point in the DRF view lifecycle where the request object is available. Up to this point our SmarterRequestMixin, and AccountMixin classes are only partially initialized. This method takes care of the rest of the initialization. Args: request (HttpRequest): The incoming HTTP request. """ if not self.is_requestmixin_ready: logger.debug( "%s.initial() - completing initialization of SmarterRequestMixin with request: %s", self.formatted_class_name, request.build_absolute_uri(), ) self.smarter_request = request logger.debug( "%s.initial() - request: %s, args: %s, kwargs: %s", self.formatted_class_name, self.request, args, kwargs, ) super().initial(self.request, *args, **kwargs)
[docs] class SmarterAuthenticatedListAPIView(ListAPIView, SmarterRequestMixin): """Smarter base class for DRF API list views that require authentication. Does the following: - Adds SmarterRequestMixin to the view, so that base Smarter functionality is available to all subclasses. - Adds SmarterTokenAuthentication to the default SessionAuthentication for authentication. - Overrides Django's logic for initializing the request object to ensure that SmarterRequestMixin is fully initialized before any other logic runs. """ permission_classes = [SmarterAuthenticatedPermissionClass] authentication_classes = [SmarterTokenAuthentication, SessionAuthentication] @property def formatted_class_name(self): """Helper method to get the formatted class name for logging.""" class_name = f"{__name__}.{SmarterAuthenticatedListAPIView.__name__}" return self.formatted_text(class_name)
[docs] def initial(self, request: Request, *args, **kwargs): """Extend DRF initial() to add SmarterRequestMixin. Args: request (Request): The incoming HTTP request. """ logger.debug( "%s.initial() - request: %s, args: %s, kwargs: %s", self.formatted_class_name, request, args, kwargs, ) self.smarter_request = request super().initial(self.request, *args, **kwargs)
# ------------------------------------------------------------------------------ # Admin API Views # ------------------------------------------------------------------------------ class SmarterAdminAPIMixin(SmarterRequestMixin): request: Any def is_superuser(self) -> bool: """Check if the authenticated user is a superuser. Returns: bool: True if the user is not a superuser, False otherwise. """ logger.debug( "%s.is_superuser() - checking if user_profile %s is superuser: %s", self.formatted_class_name, self.user_profile, self.user_profile.user.is_superuser if self.user_profile and self.user_profile.user else False, ) if not is_authenticated_request(self.request): logger.warning( "%s.is_superuser() - request user is not authenticated: %s", self.formatted_class_name, self.request.user, ) return False if not self.user_profile or not self.user_profile.user.is_superuser: return False return True def is_staff(self) -> bool: """Check if the authenticated user is a staff member. Returns: bool: True if the user is a staff member, False otherwise. """ logger.debug( "%s.is_staff() - checking if user_profile %s is staff or superuser: %s", self.formatted_class_name, self.user_profile, ( (self.user_profile.user.is_staff or self.user_profile.user.is_superuser) if self.user_profile and self.user_profile.user else False ), ) if not is_authenticated_request(self.request): logger.warning( "%s.is_staff() - request user is not authenticated: %s", self.formatted_class_name, self.request.user, ) return False if not self.user_profile or (not self.user_profile.user.is_staff and not self.user_profile.user.is_superuser): return False return True class SmarterAdminAPIView(APIView, SmarterAdminAPIMixin): """Smarter base class for DRF API views that require admin authentication. Does the following: - Adds SmarterRequestMixin to the view, so that base Smarter functionality is available to all subclasses. - Adds SmarterTokenAuthentication to the default SessionAuthentication for authentication. - Overrides Django's logic for initializing the request object to ensure that SmarterRequestMixin is fully initialized before any other logic runs. - Limits access to admin users only (staff/superusers). """ permission_classes = [SmarterAuthenticatedPermissionClass] authentication_classes = [SmarterTokenAuthentication, SessionAuthentication] @property def formatted_class_name(self): """Helper method to get the formatted class name for logging.""" class_name = f"{__name__}.{SmarterAdminAPIView.__name__}" return self.formatted_text(class_name) def __init__(self, *args, **kwargs): """Initialize the SmarterAdminAPIView.""" super().__init__(*args, **kwargs) request = kwargs.pop("request", None) user = kwargs.pop("user", None) account = kwargs.pop("account", None) user_profile = kwargs.pop("user_profile", None) SmarterRequestMixin.__init__( self, request=request, user=user, account=account, user_profile=user_profile, *args, **kwargs ) def setup(self, request: Request, *args, **kwargs) -> None: """Extend DRF setup() the view. This is called by Django before dispatch() and is used to set up the view for the request. Args: request (Request): The incoming HTTP request. """ user = kwargs.pop("user", None) account = kwargs.pop("account", None) user_profile = kwargs.pop("user_profile", None) super().setup(request, *args, **kwargs) SmarterRequestMixin.__init__( self, request=request, user=user, account=account, user_profile=user_profile, *args, **kwargs ) logger.debug( "%s.setup() - called for request: %s user: %s", self.formatted_class_name, smarter_build_absolute_uri(request), request.user if hasattr(request, "user") and hasattr(request.user, "is_authenticated") else "N/A", ) # note: setup() is the earliest point in the request lifecycle where we can # send signals. api_request_initiated.send(sender=self.__class__, instance=self, request=request) logger.debug( "%s.setup() - request: %s, user_profile: %s", self.formatted_class_name, smarter_build_absolute_uri(request), self.user_profile, ) def dispatch(self, request: Request, *args, **kwargs) -> Union[HttpResponseBase, HttpResponseForbidden, Response]: """Extend DRF dispatch() to add authentication check. Args: request (Request): The incoming HTTP request. Raises: AuthenticationFailed: Raised when authentication fails. SmarterTokenAuthenticationError: Raised for errors specific to SmarterTokenAuthentication. """ if not self.is_superuser(): return HttpResponseForbidden("Forbidden: User %s does not have superuser privileges.", self.user_profile) logger.debug( "%s.dispatch() - called by user_profile: %s and ready to process request: %s", self.formatted_class_name, self.user_profile, smarter_build_absolute_uri(request), ) try: response = super().dispatch(request, *args, **kwargs) # pylint: disable=broad-except except AttributeError: # catches an error raised by a decorator elsewhere in the stack that # barfs when the user object is None # File "/home/smarter_user/venv/lib/python3.12/site-packages/django/contrib/admin/views/decorators.py", line 13, in <lambda> return HttpResponseForbidden("Forbidden: Invalid or missing authentication credentials.") logger.debug( "%s.dispatch() - request: %s, user: %s", self.formatted_class_name, smarter_build_absolute_uri(self.request), self.request.user.username if self.request.user else "Anonymous", # type: ignore[assignment] ) api_request_completed.send(sender=self.__class__, instance=self, request=self.request, response=response) return response def finalize_response(self, request: Request, response: Response, *args, **kwargs) -> Response: """Extend DRF finalize_response() to add logging and signals. Args: request (Request): The incoming HTTP request. response (HttpResponse): The outgoing HTTP response. """ logger.debug( "%s.finalize_response() - request: %s, response status: %s", self.formatted_class_name, smarter_build_absolute_uri(request), response.status_code, ) api_request_completed.send(sender=self.__class__, instance=self, request=request, response=response) return super().finalize_response(request, response, *args, **kwargs) class SmarterAdminListAPIView(ListAPIView, SmarterAdminAPIMixin): """Smarter base class for DRF list views that require admin access. Does the following: - Adds SmarterRequestMixin to the view, so that base Smarter functionality is available to all subclasses. - Adds SmarterTokenAuthentication to the default SessionAuthentication for authentication. - Overrides Django's logic for initializing the request object to ensure that SmarterRequestMixin is fully initialized before any other logic runs. - Limits access to admin users only (staff/superusers). """ permission_classes = [SmarterAuthenticatedPermissionClass] authentication_classes = [SmarterTokenAuthentication, SessionAuthentication] def __init__(self, *args, **kwargs): """Initialize the SmarterAdminListAPIView.""" super().__init__(*args, **kwargs) request = kwargs.pop("request", None) user = kwargs.pop("user", None) account = kwargs.pop("account", None) user_profile = kwargs.pop("user_profile", None) SmarterRequestMixin.__init__( self, request=request, user=user, account=account, user_profile=user_profile, *args, **kwargs ) @property def formatted_class_name(self): """Helper method to get the formatted class name for logging.""" class_name = f"{__name__}.{SmarterAdminListAPIView.__name__}" return self.formatted_text(class_name) def setup(self, request: Request, *args, **kwargs): """Extend DRF setup() to add Django signals. Args: request (Request): The incoming HTTP request. """ super().setup(request, *args, **kwargs) user = kwargs.pop("user", None) account = kwargs.pop("account", None) user_profile = kwargs.pop("user_profile", None) SmarterRequestMixin.__init__( self, request=request, user=user, account=account, user_profile=user_profile, *args, **kwargs ) if not self.is_superuser(): logger.warning( "%s.setup() - request user %s is not superuser", self.formatted_class_name, self.user, ) return HttpResponseForbidden(f"Forbidden: User {self.user} does not have admin privileges.") # note: setup() is the earliest point in the request lifecycle where we can # send signals. api_request_initiated.send(sender=self.__class__, instance=self, request=request) logger.debug( "%s.setup() - request: %s, user_profile: %s", self.formatted_class_name, smarter_build_absolute_uri(request), self.user_profile, ) def initial(self, request: Request, *args, **kwargs): """Extend DRF initial() to add app logging. Args: request (Request): The incoming HTTP request. """ super().initial(request, *args, **kwargs) logger.debug( "%s.initial() - running for request: %s, user: %s, args: %s, kwargs: %s", self.formatted_class_name, request, request.user.username if request.user else "Anonymous", # type: ignore[assignment] args, kwargs, ) def dispatch(self, request: Request, *args, **kwargs): """Extend DRF dispatch() to add authentication checks and logging. Args: request (Request): The incoming HTTP request. Returns: HttpResponse: The HTTP response generated by the view. Raises: AuthenticationFailed: Raised when authentication fails. SmarterTokenAuthenticationError: Raised for errors specific to SmarterTokenAuthentication. """ if not self.is_superuser(): return HttpResponseForbidden("Forbidden: %s does not have superuser privileges.", self.user_profile) logger.debug( "%s.dispatch() - called for request: %s user: %s", self.formatted_class_name, smarter_build_absolute_uri(request), request.user if hasattr(request, "user") and hasattr(request.user, "is_authenticated") else "N/A", ) try: response = super().dispatch(request, *args, **kwargs) except AttributeError as e: logger.error( "%s.dispatch() - encountered AttributeError: %s.", self.formatted_class_name, str(e), exc_info=True, ) # catches an error raised by a decorator elsewhere in the stack that # barfs when the user object is None # File "/home/smarter_user/venv/lib/python3.12/site-packages/django/contrib/admin/views/decorators.py", line 13, in <lambda> return HttpResponseForbidden("Forbidden: Invalid or missing authentication credentials.") return response def finalize_response(self, request: Request, response: Response, *args, **kwargs): """Extend DRF finalize_response() to add logging and signals. Args: request (Request): The incoming HTTP request. response (HttpResponse): The outgoing HTTP response. """ api_request_completed.send(sender=self.__class__, instance=self, request=request, response=response) return super().finalize_response(request, response, *args, **kwargs)