Skip to content

Commit

Permalink
feat(api): integrated restframework_api_keys
Browse files Browse the repository at this point in the history
  • Loading branch information
tomjeannesson committed Oct 2, 2023
1 parent 40cb328 commit cccd414
Show file tree
Hide file tree
Showing 29 changed files with 209 additions and 132 deletions.
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
SHELL := /bin/bash

setup:
source setup-unix.sh

makemigrations:
source .venv/bin/activate && python tests/test_app/manage.py makemigrations

Expand All @@ -12,6 +15,9 @@ runserver:
up:
make makemigrations && make migrate && make runserver

clean:
source .venv/bin/activate && python tests/test_app/manage.py flush

celery:
source .venv/bin/activate && watchfiles --filter python celery.__main__.main --args "-A tests.test_app worker --beat -l INFO"

Expand All @@ -25,4 +31,4 @@ coverage-open:
source .venv/bin/activate && coverage run tests/test_app/manage.py test -v2 --keepdb && coverage html && coverage report && open htmlcov/index.html

reset-makemigrations:
cd django_napse && find . -path "*/migrations/*.py" -not -name "__init__.py" -delete && find . -path "*/migrations/*.pyc" -delete && cd .. && rm tests/test_app/db.sqlite3
cd django_napse && find . -path "*/migrations/*.py" -not -name "__init__.py" -delete && find . -path "*/migrations/*.pyc" -delete && cd .. && rm tests/test_app/db.sqlite3
2 changes: 0 additions & 2 deletions django_napse/api/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,10 @@ def build_main_router() -> DefaultRouter:
if url_name in url_name_list:
error_msg: str = f"Url name {url_name} already exists"
raise ConflictingUrlNames(error_msg)

main_router.register(url_name, obj, basename=url_name)
url_name_list.append(url_name)

return main_router


main_api_router = build_main_router()
print(main_api_router.urls)
55 changes: 55 additions & 0 deletions django_napse/api/custom_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from django.forms import ValidationError
from rest_framework.permissions import BasePermission

from django_napse.core.models import NapseSpace
from django_napse.utils.constants import PERMISSION_TYPES
from django_napse.utils.errors import APIError


def check_for_space(request):
if "space" not in request.query_params:
raise APIError.MissingSpace()
try:
return NapseSpace.objects.get(uuid=request.query_params["space"])
except NapseSpace.DoesNotExist as e:
raise APIError.MissingSpace() from e
except ValidationError as e:
raise APIError.MissingSpace() from e


class HasAdminPermission(BasePermission):
def has_permission(self, request, view):
space = check_for_space(request)

api_key = view.get_api_key(request)
if any(permission.permission_type == PERMISSION_TYPES.ADMIN for permission in api_key.permissions.filter(space=space)):
return True
raise APIError.InvalidPermissions()


class HasFullAccessPermission(BasePermission):
def has_permission(self, request, view):
space = check_for_space(request)

api_key = view.get_api_key(request)
for permission in api_key.permissions.filter(space=space):
if permission.permission_type in [PERMISSION_TYPES.ADMIN, PERMISSION_TYPES.FULL_ACCESS]:
return True
raise APIError.InvalidPermissions()


class HasReadPermission(BasePermission):
def has_permission(self, request, view):
space = check_for_space(request)

api_key = view.get_api_key(request)
for permission in api_key.permissions.filter(space=space):
if permission.permission_type in [PERMISSION_TYPES.ADMIN, PERMISSION_TYPES.FULL_ACCESS, PERMISSION_TYPES.READ_ONLY]:
return True
raise APIError.InvalidPermissions()


class HasSpace(BasePermission):
def has_permission(self, request, view):
check_for_space(request)
return True
12 changes: 12 additions & 0 deletions django_napse/api/custom_viewset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from rest_framework.viewsets import GenericViewSet
from rest_framework_api_key.models import APIKey

from django_napse.core.models import NapseSpace


class CustomViewSet(GenericViewSet):
def get_api_key(self, request):
return APIKey.objects.get_from_key(request.META["HTTP_AUTHORIZATION"].split()[1])

def space(self, request):
return NapseSpace.objects.get(uuid=request.query_params["space"])
1 change: 0 additions & 1 deletion django_napse/api/keys/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
from .key import NapseAPIKeySerializer
12 changes: 0 additions & 12 deletions django_napse/api/keys/serializers/key.py

This file was deleted.

18 changes: 6 additions & 12 deletions django_napse/api/keys/views/create.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
from django.db import IntegrityError
from rest_framework import status
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet

from django_napse.api.keys.serializers import NapseAPIKeySerializer
from django_napse.auth.models import NapseAPIKey
from rest_framework_api_key.models import APIKey


class Key(GenericViewSet):
permission_classes = []

def create(self, request):
if "name" not in request.data:
if "username" not in request.data:
return Response({"error": "Missing name"}, status=status.HTTP_400_BAD_REQUEST)
if "description" not in request.data:
return Response({"error": "Missing description"}, status=status.HTTP_400_BAD_REQUEST)
try:
new_key = NapseAPIKey.objects.create(name=request.data["name"], description=request.data["description"])
except IntegrityError:
return Response({"error": f"Napse API Key (name={request.data['name']}) already exists."}, status=status.HTTP_400_BAD_REQUEST)
return Response(data=NapseAPIKeySerializer(new_key).data, status=status.HTTP_200_OK)
_, key = APIKey.objects.create_key(name=request.data["username"])
return Response({"key": key}, status=status.HTTP_201_CREATED)
2 changes: 1 addition & 1 deletion django_napse/api/permissions/serializers/permission.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
class PermissionSerializer(serializers.ModelSerializer):
class Meta:
model = KeyPermission
fields = "__all__"
fields = ["uuid", "permission_type", "approved"]
1 change: 1 addition & 0 deletions django_napse/api/permissions/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .admin import AdminPermission
from .create import Permission
16 changes: 16 additions & 0 deletions django_napse/api/permissions/views/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from rest_framework import status
from rest_framework.response import Response

from django_napse.api.custom_viewset import CustomViewSet
from django_napse.api.permissions.serializers import PermissionSerializer
from django_napse.auth.models import KeyPermission


class AdminPermission(CustomViewSet):
def list(self, request):
space = self.space(request)
pending_approvals = KeyPermission.objects.filter(space=space, approved=False)
serializer = PermissionSerializer(data=pending_approvals, many=True)
serializer.is_valid()

return Response(data=serializer.data, status=status.HTTP_204_NO_CONTENT)
41 changes: 23 additions & 18 deletions django_napse/api/permissions/views/create.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
from django.db import IntegrityError
from rest_framework import status
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework_api_key.permissions import HasAPIKey

from django_napse.auth.models import KeyPermission, NapseAPIKey
from django_napse.core.models import NapseSpace
from django_napse.api.custom_permissions import HasSpace
from django_napse.api.custom_viewset import CustomViewSet
from django_napse.auth.models import KeyPermission
from django_napse.utils.constants import PERMISSION_TYPES


class Permission(GenericViewSet):
class Permission(CustomViewSet):
permission_classes = [HasAPIKey, HasSpace]

def create(self, request):
if "space_uuid" not in request.data:
return Response({"error": "Missing space_uuid"}, status=status.HTTP_400_BAD_REQUEST)
if "api_key" not in request.data:
return Response({"error": "Missing api_key"}, status=status.HTTP_400_BAD_REQUEST)
try:
key = NapseAPIKey.objects.get(napse_API_key=request.data["api_key"])
except NapseAPIKey.DoesNotExist:
return Response({"error": f"Napse API Key (name={request.data['api_key']}) does not exist."}, status=status.HTTP_400_BAD_REQUEST)
space = self.space(request)

if "permission_type" not in request.data:
return Response({"error": "Missing permission_type"}, status=status.HTTP_400_BAD_REQUEST)

if request.data["permission_type"] not in PERMISSION_TYPES:
return Response({"error": f"Permission type ({request.data['permission_type']}) does not exist."}, status=status.HTTP_400_BAD_REQUEST)

try:
space = NapseSpace.objects.get(uuid=request.data["space_uuid"])
except NapseSpace.DoesNotExist:
return Response({"error": f"Napse Space (uuid={request.data['space_uuid']}) does not exist."}, status=status.HTTP_400_BAD_REQUEST)
permission_type = PERMISSION_TYPES.FULL_ACCESS
KeyPermission.objects.create(key=key, space=space, permission_type=permission_type)
return Response(status=status.HTTP_204_NO_CONTENT)
KeyPermission.objects.create(key=self.get_api_key(request), space=space, permission_type=request.data["permission_type"])
except IntegrityError:
return Response(
{"error": f"Permission {request.data['permission_type']} already exists for this key and space."},
status=status.HTTP_400_BAD_REQUEST,
)
return Response(status=status.HTTP_201_CREATED)
15 changes: 12 additions & 3 deletions django_napse/auth/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# Generated by Django 4.2.4 on 2023-09-15 11:20
# Generated by Django 4.2.5 on 2023-10-02 15:23

from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

initial = True

dependencies = [
("rest_framework_api_key", "0005_auto_20220110_1102"),
("django_napse_core", "0001_initial"),
]

Expand Down Expand Up @@ -43,14 +45,18 @@ class Migration(migrations.Migration):
verbose_name="ID",
),
),
("valid", models.BooleanField(default=True)),
(
"uuid",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
("approved", models.BooleanField(default=False)),
("permission_type", models.CharField(max_length=200)),
(
"key",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="permissions",
to="django_napse_auth.napseapikey",
to="rest_framework_api_key.apikey",
),
),
(
Expand All @@ -61,5 +67,8 @@ class Migration(migrations.Migration):
),
),
],
options={
"unique_together": {("key", "space", "permission_type")},
},
),
]
16 changes: 16 additions & 0 deletions django_napse/auth/migrations/0002_delete_napseapikey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generated by Django 4.2.5 on 2023-10-02 15:29

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("django_napse_auth", "0001_initial"),
]

operations = [
migrations.DeleteModel(
name="NapseAPIKey",
),
]
1 change: 0 additions & 1 deletion django_napse/auth/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
from .key import *
from .permission import *
1 change: 0 additions & 1 deletion django_napse/auth/models/key/__init__.py

This file was deleted.

29 changes: 0 additions & 29 deletions django_napse/auth/models/key/key.py

This file was deleted.

1 change: 0 additions & 1 deletion django_napse/auth/models/key/managers/__init__.py

This file was deleted.

19 changes: 0 additions & 19 deletions django_napse/auth/models/key/managers/key.py

This file was deleted.

14 changes: 11 additions & 3 deletions django_napse/auth/models/permission/permission.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import uuid

from django.db import models
from rest_framework_api_key.models import APIKey

from django_napse.utils.constants import PERMISSION_TYPES


class KeyPermission(models.Model):
key = models.ForeignKey("NapseAPIKey", on_delete=models.CASCADE, related_name="permissions")
uuid = models.UUIDField(unique=True, editable=False, default=uuid.uuid4)

key = models.ForeignKey(APIKey, on_delete=models.CASCADE, related_name="permissions")
space = models.ForeignKey("django_napse_core.NapseSpace", on_delete=models.CASCADE)
valid = models.BooleanField(default=True)
approved = models.BooleanField(default=False)
permission_type = models.CharField(max_length=200)

class Meta:
unique_together = ["key", "space", "permission_type"]

def __str__(self):
return f"NAPSE KEY PERMISSION: {self.permission_type.name} - {self.key.name} - {self.exchange_account.exchange.name}"
return f"NAPSE KEY PERMISSION: {self.permission_type}"

def save(self, *args, **kwargs):
if self.permission_type not in PERMISSION_TYPES:
Expand Down
2 changes: 1 addition & 1 deletion django_napse/core/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2.4 on 2023-09-15 11:20
# Generated by Django 4.2.5 on 2023-10-02 15:23

import datetime
from django.db import migrations, models
Expand Down
Loading

0 comments on commit cccd414

Please sign in to comment.