From cf82f4b2233b3facf4c5b33b291f2903c21d1435 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 21 Jan 2022 16:15:16 +0100 Subject: [PATCH] #31 API basic implementation Token Authentication * adds token checking to AbstractModelAPIView * adds user accessibility filtering for intervention API v1 * extends fetch_and_serialize() method to take a dict for db filtering instead of a single field and value * organizes urlnames into supporting formats like "api:v1:intervention" --- api/models/token.py | 25 +++++++++++++++++++++++++ api/settings.py | 8 ++++++++ api/urls/urls.py | 2 +- api/urls/v1/urls.py | 5 +++-- api/views/v1/intervention.py | 13 +++++++------ api/views/views.py | 27 ++++++++++++++++++++------- 6 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 api/settings.py diff --git a/api/models/token.py b/api/models/token.py index f52cf7c8..81c0373e 100644 --- a/api/models/token.py +++ b/api/models/token.py @@ -1,4 +1,6 @@ +from django.core.exceptions import ObjectDoesNotExist from django.db import models +from django.utils import timezone from konova.utils.generators import generate_token @@ -21,3 +23,26 @@ class APIUserToken(models.Model): def __str__(self): return self.token + + @staticmethod + def get_user_from_token(token: str): + """ Getter for the related user object + + Args: + token (str): The used token + + Returns: + user (User): Otherwise None + """ + _today = timezone.now().date() + try: + token_obj = APIUserToken.objects.get( + token=token, + ) + if not token_obj.is_active: + raise PermissionError("Token unverified") + if token_obj.valid_until is not None and token_obj.valid_until < _today: + raise PermissionError("Token validity expired") + except ObjectDoesNotExist: + raise PermissionError("Token invalid") + return token_obj.user diff --git a/api/settings.py b/api/settings.py new file mode 100644 index 00000000..d580cb8c --- /dev/null +++ b/api/settings.py @@ -0,0 +1,8 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +KSP_TOKEN_HEADER_IDENTIFIER = "ksptoken" \ No newline at end of file diff --git a/api/urls/urls.py b/api/urls/urls.py index 62aa73d3..fe0ddbbc 100644 --- a/api/urls/urls.py +++ b/api/urls/urls.py @@ -10,5 +10,5 @@ from django.urls import path, include app_name = "api" urlpatterns = [ - path("v1/", include("api.urls.v1.urls")), + path("v1/", include("api.urls.v1.urls", namespace="v1")), ] \ No newline at end of file diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py index ff02623c..0d2cd6d7 100644 --- a/api/urls/v1/urls.py +++ b/api/urls/v1/urls.py @@ -7,8 +7,9 @@ Created on: 21.01.22 """ from django.urls import path -from api.views.v1.intervention import APIInterventionView +from api.views.v1.intervention import APIInterventionViewV1 +app_name = "v1" urlpatterns = [ - path("intervention/", APIInterventionView.as_view(), name="api-intervention"), + path("intervention/", APIInterventionViewV1.as_view(), name="intervention"), ] diff --git a/api/views/v1/intervention.py b/api/views/v1/intervention.py index 6085d0d5..613f608f 100644 --- a/api/views/v1/intervention.py +++ b/api/views/v1/intervention.py @@ -13,15 +13,16 @@ from api.views.v1.general import AbstractModelAPIViewV1 from intervention.models import Intervention -class APIInterventionView(AbstractModelAPIViewV1): +class APIInterventionViewV1(AbstractModelAPIViewV1): model = Intervention - fields_to_serialize = { - "identifier", - "title", - } def get(self, request: HttpRequest, identifier): - data = self.fetch_and_serialize("identifier", identifier) + _filter = { + "identifier": identifier, + "users__in": [self.user], + "deleted__isnull": True, + } + data = self.fetch_and_serialize(_filter) return JsonResponse(data) def model_to_json(self, entry: Intervention): diff --git a/api/views/views.py b/api/views/views.py index e04027b4..40f42210 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -7,8 +7,12 @@ Created on: 21.01.22 """ from abc import abstractmethod +from django.http import JsonResponse from django.views import View +from api.models import APIUserToken +from api.settings import KSP_TOKEN_HEADER_IDENTIFIER + class AbstractModelAPIView(View): """ Base class for API views @@ -19,6 +23,7 @@ class AbstractModelAPIView(View): """ model = None + user = None class Meta: abstract = True @@ -35,21 +40,29 @@ class AbstractModelAPIView(View): """ raise NotImplementedError("Must be implemented in subclasses") - def fetch_and_serialize(self, lookup_field, lookup_val): + def fetch_and_serialize(self, _filter): """ Serializes the model entry according to the given lookup data Args: - lookup_field (): Which field used for lookup - lookup_val (): Value for lookup + _filter (dict): Lookup declarations Returns: serialized_data (dict) """ - _filters = { - lookup_field: lookup_val - } - qs = self.model.objects.filter(**_filters) + qs = self.model.objects.filter(**_filter) serialized_data = {} for entry in qs: serialized_data[str(entry.pk)] = self.model_to_json(entry) return serialized_data + + def dispatch(self, request, *args, **kwargs): + try: + self.user = APIUserToken.get_user_from_token(request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)) + except PermissionError as e: + return JsonResponse( + { + "error": e.__str__() + }, + status=403 + ) + return super().dispatch(request, *args, **kwargs)