From ccfd1c703eed0c46e18c8abf70f3600537f0bba9 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 14 Nov 2023 20:30:54 +0530 Subject: [PATCH 1/6] dev: fix instance permissions and serializer --- .../plane/license/api/permissions/instance.py | 10 +++++++ .../plane/license/api/serializers/instance.py | 1 + apiserver/plane/license/api/views/instance.py | 26 ++++++++++--------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/apiserver/plane/license/api/permissions/instance.py b/apiserver/plane/license/api/permissions/instance.py index 86583ee95..1d1845f12 100644 --- a/apiserver/plane/license/api/permissions/instance.py +++ b/apiserver/plane/license/api/permissions/instance.py @@ -7,17 +7,27 @@ from plane.license.models import Instance, InstanceAdmin class InstanceOwnerPermission(BasePermission): def has_permission(self, request, view): + + if request.user.is_anonymous: + return False + instance = Instance.objects.first() return InstanceAdmin.objects.filter( role=20, instance=instance, + user=request.user, ).exists() class InstanceAdminPermission(BasePermission): def has_permission(self, request, view): + + if request.user.is_anonymous: + return False + instance = Instance.objects.first() return InstanceAdmin.objects.filter( role__gte=15, instance=instance, + user=request.user, ).exists() diff --git a/apiserver/plane/license/api/serializers/instance.py b/apiserver/plane/license/api/serializers/instance.py index 8fd8edda1..b8c990522 100644 --- a/apiserver/plane/license/api/serializers/instance.py +++ b/apiserver/plane/license/api/serializers/instance.py @@ -28,6 +28,7 @@ class InstanceAdminSerializer(BaseSerializer): class Meta: model = InstanceAdmin + fields = "__all__" read_only_fields = [ "id", "instance", diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index b51702398..ecab0740c 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -9,7 +9,11 @@ from rest_framework.permissions import AllowAny # Module imports from plane.api.views import BaseAPIView from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration -from plane.license.api.serializers import InstanceSerializer, InstanceAdminSerializer, InstanceConfigurationSerializer +from plane.license.api.serializers import ( + InstanceSerializer, + InstanceAdminSerializer, + InstanceConfigurationSerializer, +) from plane.license.api.permissions import ( InstanceOwnerPermission, InstanceAdminPermission, @@ -75,7 +79,7 @@ class TransferPrimaryOwnerEndpoint(BaseAPIView): 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, @@ -90,11 +94,7 @@ class TransferPrimaryOwnerEndpoint(BaseAPIView): class InstanceAdminEndpoint(BaseAPIView): def get_permissions(self): - if self.request.method == "GET": - self.permission_classes = [ - AllowAny, - ] - elif self.request.method in ["POST", "DELETE"]: + if self.request.method in ["POST", "DELETE"]: self.permission_classes = [ InstanceOwnerPermission, ] @@ -150,21 +150,23 @@ class InstanceAdminEndpoint(BaseAPIView): class InstanceConfigurationEndpoint(BaseAPIView): - - permission_classes = [InstanceAdminEndpoint,] + permission_classes = [ + InstanceAdminPermission, + ] 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) + 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) - From 095f64793fffee748e3655509357167f609ea74d Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 14 Nov 2023 20:31:29 +0530 Subject: [PATCH 2/6] dev: fix email senders --- apiserver/plane/bgtasks/analytic_plot_export.py | 2 +- apiserver/plane/bgtasks/email_verification_task.py | 2 +- apiserver/plane/bgtasks/forgot_password_task.py | 2 +- apiserver/plane/bgtasks/magic_link_code_task.py | 4 +--- apiserver/plane/bgtasks/project_invitation_task.py | 2 +- apiserver/plane/bgtasks/workspace_invitation_task.py | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index 19ee2306e..8cccc2299 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -61,7 +61,7 @@ def send_export_email(email, slug, csv_buffer): use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ) - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue()) msg.send(fail_silently=False) diff --git a/apiserver/plane/bgtasks/email_verification_task.py b/apiserver/plane/bgtasks/email_verification_task.py index aedc48e0e..220f96390 100644 --- a/apiserver/plane/bgtasks/email_verification_task.py +++ b/apiserver/plane/bgtasks/email_verification_task.py @@ -46,7 +46,7 @@ def email_verification(first_name, email, token, current_site): ) # Initiate email alternatives - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach_alternative(html_content, "text/html") msg.send() return diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index fa4e2ea45..676ca5a9c 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -42,7 +42,7 @@ def forgot_password(first_name, email, uidb64, token, current_site): use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ) # Initiate email alternatives - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach_alternative(html_content, "text/html") msg.send() return diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index e6d047e9c..8d64029ee 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -18,8 +18,6 @@ def magic_link(email, key, token, current_site): realtivelink = f"/magic-sign-in/?password={token}&key={key}" abs_url = current_site + realtivelink - from_email_string = settings.EMAIL_FROM - subject = "Login for Plane" context = {"magic_url": abs_url, "code": token} @@ -38,7 +36,7 @@ def magic_link(email, key, token, current_site): use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ) # Initiate email alternatives - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach_alternative(html_content, "text/html") msg.send() return diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py index 7a47b619d..a9adad02f 100644 --- a/apiserver/plane/bgtasks/project_invitation_task.py +++ b/apiserver/plane/bgtasks/project_invitation_task.py @@ -55,7 +55,7 @@ def project_invitation(email, project_id, token, current_site): use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ) # Initiate email alternatives - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach_alternative(html_content, "text/html") msg.send() return diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index ad06b53b0..d8c40a6a3 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -58,7 +58,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ) # Initiate email alternatives - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach_alternative(html_content, "text/html") msg.send() From deefdac8b403cebfd3109fb8c64973fe625a395f Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 15 Nov 2023 20:07:33 +0530 Subject: [PATCH 3/6] dev: migrate instance registration and configuration to manage commands --- apiserver/bin/instance_configuration.py | 49 ----------------- .../plane/license/management/__init__.py | 0 .../license/management/commands/__init__.py | 0 .../management/commands/configure_instance.py | 46 ++++++++++++++++ .../management/commands/register_instance.py} | 50 ++++++++--------- .../plane/license/migrations/0001_initial.py | 53 +++++++++++++------ ...e_email_instance_primary_email_and_more.py | 50 ----------------- 7 files changed, 104 insertions(+), 144 deletions(-) delete mode 100644 apiserver/bin/instance_configuration.py create mode 100644 apiserver/plane/license/management/__init__.py create mode 100644 apiserver/plane/license/management/commands/__init__.py create mode 100644 apiserver/plane/license/management/commands/configure_instance.py rename apiserver/{bin/instance_registration.py => plane/license/management/commands/register_instance.py} (70%) delete mode 100644 apiserver/plane/license/migrations/0002_rename_email_instance_primary_email_and_more.py diff --git a/apiserver/bin/instance_configuration.py b/apiserver/bin/instance_configuration.py deleted file mode 100644 index b30875057..000000000 --- a/apiserver/bin/instance_configuration.py +++ /dev/null @@ -1,49 +0,0 @@ -import os, sys - - -sys.path.append("/code") - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") - -import django - -django.setup() - - -def load_config(): - from plane.license.models import InstanceConfiguration - - config_keys = { - # Authentication Settings - "GOOGLE_CLIENT_ID": os.environ.get("GOOGLE_CLIENT_ID"), - "GITHUB_CLIENT_ID": os.environ.get("GITHUB_CLIENT_ID"), - "GITHUB_CLIENT_SECRET": os.environ.get("GITHUB_CLIENT_SECRET"), - "ENABLE_SIGNUP": os.environ.get("ENABLE_SIGNUP", "1"), - "ENABLE_EMAIL_PASSWORD": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"), - "ENABLE_MAGIC_LINK_LOGIN": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"), - # Email Settings - "EMAIL_HOST": os.environ.get("EMAIL_HOST", ""), - "EMAIL_HOST_USER": os.environ.get("EMAIL_HOST_USER", ""), - "EMAIL_HOST_PASSWORD": os.environ.get("EMAIL_HOST_PASSWORD"), - "EMAIL_PORT": os.environ.get("EMAIL_PORT", "587"), - "EMAIL_FROM": os.environ.get("EMAIL_FROM", ""), - "EMAIL_USE_TLS": os.environ.get("EMAIL_USE_TLS", "1"), - "EMAIL_USE_SSL": os.environ.get("EMAIL_USE_SSL", "0"), - # Open AI Settings - "OPENAI_API_BASE": os.environ.get("", "https://api.openai.com/v1"), - "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", "sk-"), - "GPT_ENGINE": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"), - } - - for key, value in config_keys.items(): - obj, created = InstanceConfiguration.objects.get_or_create( - key=key - ) - obj.value = value - obj.save() - - print(f"{key} loaded with value from environment variable.") - - -if __name__ == "__main__": - load_config() diff --git a/apiserver/plane/license/management/__init__.py b/apiserver/plane/license/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/license/management/commands/__init__.py b/apiserver/plane/license/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/license/management/commands/configure_instance.py b/apiserver/plane/license/management/commands/configure_instance.py new file mode 100644 index 000000000..d71d9f590 --- /dev/null +++ b/apiserver/plane/license/management/commands/configure_instance.py @@ -0,0 +1,46 @@ +# Python imports +import os + +# Django imports +from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone + +# Module imports +from plane.license.models import InstanceConfiguration + +class Command(BaseCommand): + help = "Configure instance variables" + + def handle(self, *args, **options): + config_keys = { + # Authentication Settings + "GOOGLE_CLIENT_ID": os.environ.get("GOOGLE_CLIENT_ID"), + "GITHUB_CLIENT_ID": os.environ.get("GITHUB_CLIENT_ID"), + "GITHUB_CLIENT_SECRET": os.environ.get("GITHUB_CLIENT_SECRET"), + "ENABLE_SIGNUP": os.environ.get("ENABLE_SIGNUP", "1"), + "ENABLE_EMAIL_PASSWORD": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"), + "ENABLE_MAGIC_LINK_LOGIN": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"), + # Email Settings + "EMAIL_HOST": os.environ.get("EMAIL_HOST", ""), + "EMAIL_HOST_USER": os.environ.get("EMAIL_HOST_USER", ""), + "EMAIL_HOST_PASSWORD": os.environ.get("EMAIL_HOST_PASSWORD"), + "EMAIL_PORT": os.environ.get("EMAIL_PORT", "587"), + "EMAIL_FROM": os.environ.get("EMAIL_FROM", ""), + "EMAIL_USE_TLS": os.environ.get("EMAIL_USE_TLS", "1"), + "EMAIL_USE_SSL": os.environ.get("EMAIL_USE_SSL", "0"), + # Open AI Settings + "OPENAI_API_BASE": os.environ.get("", "https://api.openai.com/v1"), + "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", "sk-"), + "GPT_ENGINE": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"), + } + + for key, value in config_keys.items(): + obj, created = InstanceConfiguration.objects.get_or_create( + key=key + ) + if created: + obj.value = value + obj.save() + self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable.")) + else: + self.stdout.write(self.style.WARNING(f"{key} configuration already exists")) \ No newline at end of file diff --git a/apiserver/bin/instance_registration.py b/apiserver/plane/license/management/commands/register_instance.py similarity index 70% rename from apiserver/bin/instance_registration.py rename to apiserver/plane/license/management/commands/register_instance.py index 854251595..52f8213d9 100644 --- a/apiserver/bin/instance_registration.py +++ b/apiserver/plane/license/management/commands/register_instance.py @@ -1,41 +1,35 @@ # Python imports -import os, sys import json -import uuid +import os import requests +import uuid # Django imports +from django.core.management.base import BaseCommand, CommandError from django.utils import timezone - -sys.path.append("/code") - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") - -import django - -django.setup() +# Module imports +from plane.db.models import User +from plane.license.models import Instance, InstanceAdmin -def instance_registration(): - try: - # Module imports - from plane.db.models import User - from plane.license.models import Instance, InstanceAdmin +class Command(BaseCommand): + help = "Check if instance in registered else register" + def handle(self, *args, **options): # Check if the instance is registered instance = Instance.objects.first() # If instance is None then register this instance if instance is None: - with open("/code/package.json", "r") as file: + with open("package.json", "r") as file: # Load JSON content from the file data = json.load(file) admin_email = os.environ.get("ADMIN_EMAIL") # Raise an exception if the admin email is not provided if not admin_email: - raise Exception("ADMIN_EMAIL is required") + raise CommandError("ADMIN_EMAIL is required") # Check if the admin email user exists user = User.objects.filter(email=admin_email).first() @@ -49,7 +43,7 @@ def instance_registration(): license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL") if not license_engine_base_url: - raise Exception("LICENSE_ENGINE_BASE_URL is required") + raise CommandError("LICENSE_ENGINE_BASE_URL is required") headers = {"Content-Type": "application/json"} @@ -84,19 +78,19 @@ def instance_registration(): role=20, ) - print(f"Instance succesfully registered with owner: {instance.primary_owner.email}") + self.stdout.write( + self.style.SUCCESS( + f"Instance succesfully registered with owner: {instance.primary_owner.email}" + ) + ) return - print("Instance could not be registered") + self.stdout.write(self.style.WARNING("Instance could not be registered")) return else: - print( - f"Instance already registered with instance owner: {instance.primary_owner.email}" + self.stdout.write( + self.style.SUCCESS( + f"Instance already registered with instance owner: {instance.primary_owner.email}" + ) ) return - except ImportError: - raise ImportError - - -if __name__ == "__main__": - instance_registration() diff --git a/apiserver/plane/license/migrations/0001_initial.py b/apiserver/plane/license/migrations/0001_initial.py index 6e9d63eb9..db620a18e 100644 --- a/apiserver/plane/license/migrations/0001_initial.py +++ b/apiserver/plane/license/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-13 14:31 +# Generated by Django 4.2.5 on 2023-11-15 14:22 from django.conf import settings from django.db import migrations, models @@ -15,6 +15,34 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Instance', + 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)), + ('instance_name', models.CharField(max_length=255)), + ('whitelist_emails', models.TextField(blank=True, null=True)), + ('instance_id', models.CharField(max_length=25, unique=True)), + ('license_key', models.CharField(blank=True, max_length=256, null=True)), + ('api_key', models.CharField(max_length=16)), + ('version', models.CharField(max_length=10)), + ('primary_email', models.CharField(max_length=256)), + ('last_checked_at', models.DateTimeField()), + ('namespace', models.CharField(blank=True, max_length=50, null=True)), + ('is_telemetry_enabled', models.BooleanField(default=True)), + ('is_support_required', models.BooleanField(default=True)), + ('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')), + ('primary_owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_primary_owner', to=settings.AUTH_USER_MODEL)), + ('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')), + ], + options={ + 'verbose_name': 'Instance', + 'verbose_name_plural': 'Instances', + 'db_table': 'instances', + 'ordering': ('-created_at',), + }, + ), migrations.CreateModel( name='InstanceConfiguration', fields=[ @@ -34,30 +62,21 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Instance', + 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)), - ('instance_name', models.CharField(max_length=255)), - ('whitelist_emails', models.TextField(blank=True, null=True)), - ('instance_id', models.CharField(max_length=25, unique=True)), - ('license_key', models.CharField(blank=True, max_length=256, null=True)), - ('api_key', models.CharField(max_length=16)), - ('version', models.CharField(max_length=10)), - ('email', models.CharField(max_length=256)), - ('last_checked_at', models.DateTimeField()), - ('namespace', models.CharField(blank=True, max_length=50, null=True)), - ('is_telemetry_enabled', models.BooleanField(default=True)), - ('is_support_required', models.BooleanField(default=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')), - ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_owner', to=settings.AUTH_USER_MODEL)), + ('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', - 'verbose_name_plural': 'Instances', - 'db_table': 'instances', + 'verbose_name': 'Instance Admin', + 'verbose_name_plural': 'Instance Admins', + 'db_table': 'instance_admins', 'ordering': ('-created_at',), }, ), 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 deleted file mode 100644 index 7589239b7..000000000 --- a/apiserver/plane/license/migrations/0002_rename_email_instance_primary_email_and_more.py +++ /dev/null @@ -1,50 +0,0 @@ -# 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',), - }, - ), - ] From 6102de37dea8c6d0a24988f81a3040f4ea2be448 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 15 Nov 2023 20:19:55 +0530 Subject: [PATCH 4/6] dev: check email validity --- .../license/management/commands/register_instance.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apiserver/plane/license/management/commands/register_instance.py b/apiserver/plane/license/management/commands/register_instance.py index 52f8213d9..855a3a035 100644 --- a/apiserver/plane/license/management/commands/register_instance.py +++ b/apiserver/plane/license/management/commands/register_instance.py @@ -7,6 +7,8 @@ import uuid # Django imports from django.core.management.base import BaseCommand, CommandError from django.utils import timezone +from django.core.exceptions import ValidationError +from django.core.validators import validate_email # Module imports from plane.db.models import User @@ -27,6 +29,12 @@ class Command(BaseCommand): data = json.load(file) admin_email = os.environ.get("ADMIN_EMAIL") + + try: + validate_email(admin_email) + except ValidationError: + CommandError(f"{admin_email} is not a valid ADMIN_EMAIL") + # Raise an exception if the admin email is not provided if not admin_email: raise CommandError("ADMIN_EMAIL is required") From 55869ee994529e4eec8f8d41e21ac164f0cb2326 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 15 Nov 2023 20:22:14 +0530 Subject: [PATCH 5/6] dev: update script to use manage command --- apiserver/bin/takeoff | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/bin/takeoff b/apiserver/bin/takeoff index 92834b8c1..44f251155 100755 --- a/apiserver/bin/takeoff +++ b/apiserver/bin/takeoff @@ -4,9 +4,9 @@ python manage.py wait_for_db python manage.py migrate # Register instance -python bin/instance_registration.py +python manage.py register_instance # Load the configuration variable -python bin/instance_configuration.py +python manage.py configure_instance # Create the default bucket python bin/bucket_script.py From 44a3097ed1faccab6fd9d32820161486e4fc4604 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 16 Nov 2023 12:13:45 +0530 Subject: [PATCH 6/6] dev: default bucket creation script --- .../db/management/commands/create_bucket.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 apiserver/plane/db/management/commands/create_bucket.py diff --git a/apiserver/plane/db/management/commands/create_bucket.py b/apiserver/plane/db/management/commands/create_bucket.py new file mode 100644 index 000000000..054523bf9 --- /dev/null +++ b/apiserver/plane/db/management/commands/create_bucket.py @@ -0,0 +1,71 @@ +# Python imports +import boto3 +import json +from botocore.exceptions import ClientError + +# Django imports +from django.core.management import BaseCommand +from django.conf import settings + +class Command(BaseCommand): + help = "Create the default bucket for the instance" + + def set_bucket_public_policy(self, s3_client, bucket_name): + public_policy = { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetObject"], + "Resource": [f"arn:aws:s3:::{bucket_name}/*"] + }] + } + + try: + s3_client.put_bucket_policy( + Bucket=bucket_name, + Policy=json.dumps(public_policy) + ) + self.stdout.write(self.style.SUCCESS(f"Public read access policy set for bucket '{bucket_name}'.")) + except ClientError as e: + self.stdout.write(self.style.ERROR(f"Error setting public read access policy: {e}")) + + + def handle(self, *args, **options): + # Create a session using the credentials from Django settings + try: + session = boto3.session.Session( + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + # Create an S3 client using the session + s3_client = session.client('s3', endpoint_url=settings.AWS_S3_ENDPOINT_URL) + bucket_name = settings.AWS_STORAGE_BUCKET_NAME + + self.stdout.write(self.style.NOTICE("Checking bucket...")) + + # Check if the bucket exists + s3_client.head_bucket(Bucket=bucket_name) + + self.set_bucket_public_policy(s3_client, bucket_name) + except ClientError as e: + error_code = int(e.response['Error']['Code']) + bucket_name = settings.AWS_STORAGE_BUCKET_NAME + if error_code == 404: + # Bucket does not exist, create it + self.stdout.write(self.style.WARNING(f"Bucket '{bucket_name}' does not exist. Creating bucket...")) + try: + s3_client.create_bucket(Bucket=bucket_name) + self.stdout.write(self.style.SUCCESS(f"Bucket '{bucket_name}' created successfully.")) + self.set_bucket_public_policy(s3_client, bucket_name) + except ClientError as create_error: + self.stdout.write(self.style.ERROR(f"Failed to create bucket: {create_error}")) + elif error_code == 403: + # Access to the bucket is forbidden + self.stdout.write(self.style.ERROR(f"Access to the bucket '{bucket_name}' is forbidden. Check permissions.")) + else: + # Another ClientError occurred + self.stdout.write(self.style.ERROR(f"Failed to check bucket: {e}")) + except Exception as ex: + # Handle any other exception + self.stdout.write(self.style.ERROR(f"An error occurred: {ex}")) \ No newline at end of file