diff --git a/apiserver/bin/instance_registration.py b/apiserver/bin/instance_registration.py index 2d838af74..170b8b5ca 100644 --- a/apiserver/bin/instance_registration.py +++ b/apiserver/bin/instance_registration.py @@ -73,14 +73,15 @@ def instance_registration(): license_key=data.get("license_key"), api_key=data.get("api_key"), version=data.get("version"), - email=data.get("email"), - owner=user, + primary_email=data.get("email"), + primary_owner=user, last_checked_at=timezone.now(), ) # Create instance admin _ = InstanceAdmin.objects.create( user=user, instance=instance, + role=20, ) print(f"Instance succesfully registered with owner: {instance.owner.email}") diff --git a/apiserver/plane/license/api/permissions/__init__.py b/apiserver/plane/license/api/permissions/__init__.py index e69de29bb..392b228c0 100644 --- a/apiserver/plane/license/api/permissions/__init__.py +++ b/apiserver/plane/license/api/permissions/__init__.py @@ -0,0 +1 @@ +from .instance import InstanceOwnerPermission, InstanceAdminPermission diff --git a/apiserver/plane/license/api/permissions/instance.py b/apiserver/plane/license/api/permissions/instance.py index e69de29bb..86583ee95 100644 --- a/apiserver/plane/license/api/permissions/instance.py +++ b/apiserver/plane/license/api/permissions/instance.py @@ -0,0 +1,23 @@ +# Third party imports +from rest_framework.permissions import BasePermission + +# Module imports +from plane.license.models import Instance, InstanceAdmin + + +class InstanceOwnerPermission(BasePermission): + def has_permission(self, request, view): + instance = Instance.objects.first() + return InstanceAdmin.objects.filter( + role=20, + instance=instance, + ).exists() + + +class InstanceAdminPermission(BasePermission): + def has_permission(self, request, view): + instance = Instance.objects.first() + return InstanceAdmin.objects.filter( + role__gte=15, + instance=instance, + ).exists() diff --git a/apiserver/plane/license/api/serializers/__init__.py b/apiserver/plane/license/api/serializers/__init__.py index 479ac7324..b658ff148 100644 --- a/apiserver/plane/license/api/serializers/__init__.py +++ b/apiserver/plane/license/api/serializers/__init__.py @@ -1 +1 @@ -from .instance import InstanceSerializer \ No newline at end of file +from .instance import InstanceSerializer, InstanceAdminSerializer, InstanceConfigurationSerializer \ No newline at end of file diff --git a/apiserver/plane/license/api/serializers/instance.py b/apiserver/plane/license/api/serializers/instance.py index aab4b9c90..8fd8edda1 100644 --- a/apiserver/plane/license/api/serializers/instance.py +++ b/apiserver/plane/license/api/serializers/instance.py @@ -1,18 +1,19 @@ # Module imports -from plane.license.models import Instance +from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration from plane.api.serializers import BaseSerializer from plane.api.serializers import UserAdminLiteSerializer class InstanceSerializer(BaseSerializer): - owner_details = UserAdminLiteSerializer(source="owner", read_only=True) + primary_owner_details = UserAdminLiteSerializer(source="primary_owner", read_only=True) class Meta: model = Instance fields = "__all__" read_only_fields = [ "id", - "owner", + "primary_owner", + "primary_email", "instance_id", "license_key", "api_key", @@ -20,3 +21,21 @@ class InstanceSerializer(BaseSerializer): "email", "last_checked_at", ] + + +class InstanceAdminSerializer(BaseSerializer): + user_detail = UserAdminLiteSerializer(source="user", read_only=True) + + class Meta: + model = InstanceAdmin + read_only_fields = [ + "id", + "instance", + "user", + ] + +class InstanceConfigurationSerializer(BaseSerializer): + + class Meta: + model = InstanceConfiguration + fields = "__all__" diff --git a/apiserver/plane/license/api/views/__init__.py b/apiserver/plane/license/api/views/__init__.py index 340bbfa49..4b925759a 100644 --- a/apiserver/plane/license/api/views/__init__.py +++ b/apiserver/plane/license/api/views/__init__.py @@ -1 +1,6 @@ -from .instance import InstanceEndpoint, TransferOwnerEndpoint \ No newline at end of file +from .instance import ( + InstanceEndpoint, + TransferPrimaryOwnerEndpoint, + InstanceAdminEndpoint, + InstanceConfigurationEndpoint, +) diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 4a812b5c6..b51702398 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -1,33 +1,39 @@ -# Python imports -import os -import json -import requests -import uuid - # Django imports from django.utils import timezone # Third party imports from rest_framework import status from rest_framework.response import Response +from rest_framework.permissions import AllowAny # Module imports from plane.api.views import BaseAPIView -from plane.license.models import Instance -from plane.license.api.serializers import InstanceSerializer +from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration +from plane.license.api.serializers import InstanceSerializer, InstanceAdminSerializer, InstanceConfigurationSerializer +from plane.license.api.permissions import ( + InstanceOwnerPermission, + InstanceAdminPermission, +) from plane.db.models import User class InstanceEndpoint(BaseAPIView): + def get_permissions(self): + if self.request.method == "GET": + self.permission_classes = [ + AllowAny, + ] + else: + self.permission_classes = [ + InstanceOwnerPermission, + ] + return super(InstanceEndpoint, self).get_permissions() def get(self, request): instance = Instance.objects.first() # get the instance if instance is None: return Response({"activated": False}, status=status.HTTP_400_BAD_REQUEST) - # Check the accessing user - if str(request.user.id) != str(instance.owner_id): - return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) # Return instance serializer = InstanceSerializer(instance) data = { @@ -39,9 +45,6 @@ class InstanceEndpoint(BaseAPIView): def patch(self, request): # Get the instance instance = Instance.objects.first() - # Check the accessing user - if instance is not None and str(request.user.id) != str(instance.owner_id): - return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) serializer = InstanceSerializer(instance, data=request.data, partial=True) if serializer.is_valid(): serializer.save() @@ -49,16 +52,15 @@ class InstanceEndpoint(BaseAPIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -class TransferOwnerEndpoint(BaseAPIView): +class TransferPrimaryOwnerEndpoint(BaseAPIView): + permission_classes = [ + InstanceOwnerPermission, + ] # Transfer the owner of the instance def post(self, request): instance = Instance.objects.first() - # Check the accessing user - if instance is not None and str(request.user.id) != str(instance.owner_id): - return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) - # Get the email of the new user email = request.data.get("email", False) if not email: @@ -68,13 +70,18 @@ class TransferOwnerEndpoint(BaseAPIView): # Get users user = User.objects.get(email=email) - user.is_superuser = True - user.save(update_fields=["is_superuser"]) # Save the instance user - instance.owner = user - instance.email = user.email + instance.primary_owner = user + instance.primary_email = user.email instance.save(update_fields=["owner", "email"]) + + # Add the user to admin + _ = InstanceAdmin.objects.get_or_create( + instance=instance, + user=user, + role=20, + ) return Response( {"message": "Owner successfully updated"}, status=status.HTTP_200_OK @@ -82,10 +89,82 @@ class TransferOwnerEndpoint(BaseAPIView): class InstanceAdminEndpoint(BaseAPIView): + def get_permissions(self): + if self.request.method == "GET": + self.permission_classes = [ + AllowAny, + ] + elif self.request.method in ["POST", "DELETE"]: + self.permission_classes = [ + InstanceOwnerPermission, + ] + else: + self.permission_classes = [ + InstanceAdminPermission, + ] + return super(InstanceAdminEndpoint, self).get_permissions() + # Create an instance admin + def post(self, request): + email = request.data.get("email", False) + role = request.data.get("role", 15) + + if not email: + return Response( + {"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST + ) - def get(self, request, pk=None): instance = Instance.objects.first() if instance is None: + return Response( + {"error": "Instance is not registered yet"}, + status=status.HTTP_403_FORBIDDEN, + ) + + # Fetch the user + user = User.objects.get(email=email) + + instance_admin = InstanceAdmin.objects.create( + instance=instance, + user=user, + role=role, + ) + serializer = InstanceAdminSerializer(instance_admin) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request): + instance = Instance.objects.first() + if instance is None: + return Response( + {"error": "Instance is not registered yet"}, + status=status.HTTP_403_FORBIDDEN, + ) + instance_admins = InstanceAdmin.objects.filter(instance=instance) + serializer = InstanceAdminSerializer(instance_admins, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def delete(self, request, pk): + instance = Instance.objects.first() + instance_admin = InstanceAdmin.objects.filter(instance=instance, pk=pk).delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class InstanceConfigurationEndpoint(BaseAPIView): + + permission_classes = [InstanceAdminEndpoint,] + + def get(self, request): + instance_configurations = InstanceConfiguration.objects.all() + serializer = InstanceConfigurationSerializer(instance_configurations, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def patch(self, request): + key = request.data.get("key", False) + if not key: + return Response({"error": "Key is required"}, status=status.HTTP_400_BAD_REQUEST) + configuration = InstanceConfiguration.objects.get(key=key) + configuration.value = request.data.get("value") + configuration.save() + serializer = InstanceConfigurationSerializer(configuration) + return Response(serializer.data, status=status.HTTP_200_OK) - \ No newline at end of file diff --git a/apiserver/plane/license/migrations/0002_rename_email_instance_primary_email_and_more.py b/apiserver/plane/license/migrations/0002_rename_email_instance_primary_email_and_more.py new file mode 100644 index 000000000..7589239b7 --- /dev/null +++ b/apiserver/plane/license/migrations/0002_rename_email_instance_primary_email_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.5 on 2023-11-14 10:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('license', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='instance', + old_name='email', + new_name='primary_email', + ), + migrations.RemoveField( + model_name='instance', + name='owner', + ), + migrations.AddField( + model_name='instance', + name='primary_owner', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_primary_owner', to=settings.AUTH_USER_MODEL), + ), + migrations.CreateModel( + name='InstanceAdmin', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('role', models.PositiveIntegerField(choices=[(20, 'Owner'), (15, 'Admin')], default=15)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admins', to='license.instance')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_owner', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Instance Admin', + 'verbose_name_plural': 'Instance Admins', + 'db_table': 'instance_admins', + 'ordering': ('-created_at',), + }, + ), + ] diff --git a/apiserver/plane/license/models/instance.py b/apiserver/plane/license/models/instance.py index f5af2195b..78da706af 100644 --- a/apiserver/plane/license/models/instance.py +++ b/apiserver/plane/license/models/instance.py @@ -6,6 +6,12 @@ from django.conf import settings from plane.db.models import BaseModel from plane.db.mixins import AuditModel +ROLE_CHOICES = ( + (20, "Owner"), + (15, "Admin"), +) + + class Instance(BaseModel): # General informations instance_name = models.CharField(max_length=255) @@ -15,12 +21,12 @@ class Instance(BaseModel): api_key = models.CharField(max_length=16) version = models.CharField(max_length=10) # User information - email = models.CharField(max_length=256) - owner = models.ForeignKey( + primary_email = models.CharField(max_length=256) + primary_owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, - related_name="instance_owner", + related_name="instance_primary_owner", ) # Instnace specifics last_checked_at = models.DateTimeField() @@ -43,7 +49,8 @@ class InstanceAdmin(BaseModel): null=True, related_name="instance_owner", ) - instance = models.ForeignKey("db.Instance", on_delete=models.CASCADE, related_name="admins") + instance = models.ForeignKey(Instance, on_delete=models.CASCADE, related_name="admins") + role = models.PositiveIntegerField(choices=ROLE_CHOICES, default=15) class Meta: verbose_name = "Instance Admin" diff --git a/apiserver/plane/license/urls.py b/apiserver/plane/license/urls.py index ac92efc13..6e95329bd 100644 --- a/apiserver/plane/license/urls.py +++ b/apiserver/plane/license/urls.py @@ -1,6 +1,11 @@ from django.urls import path -from plane.license.api.views import InstanceEndpoint, TransferOwnerEndpoint +from plane.license.api.views import ( + InstanceEndpoint, + TransferPrimaryOwnerEndpoint, + InstanceAdminEndpoint, + InstanceConfigurationEndpoint, +) urlpatterns = [ path( @@ -9,8 +14,23 @@ urlpatterns = [ name="instance", ), path( - "instances/transfer-owner/", - TransferOwnerEndpoint.as_view(), + "instances/transfer-primary-owner/", + TransferPrimaryOwnerEndpoint.as_view(), name="instance", ), + path( + "instances/admins/", + InstanceAdminEndpoint.as_view(), + name="instance-admins", + ), + path( + "instances/admins//", + InstanceAdminEndpoint.as_view(), + name="instance-admins", + ), + path( + "instances/configurations/", + InstanceConfigurationEndpoint.as_view(), + name="instance-configuration", + ), ]