From e214e6f3e590dc949add5f7418712eda4955b5b0 Mon Sep 17 00:00:00 2001 From: Eric Costa Date: Thu, 12 Dec 2024 17:04:50 -0300 Subject: [PATCH] feature: Validating before delete organization permission if the user are the last admin --- connect/api/v1/organization/views.py | 165 ++++++++++++---------- connect/usecases/authorizations/delete.py | 70 ++++++--- 2 files changed, 146 insertions(+), 89 deletions(-) diff --git a/connect/api/v1/organization/views.py b/connect/api/v1/organization/views.py index a1b8333e..5181efa9 100644 --- a/connect/api/v1/organization/views.py +++ b/connect/api/v1/organization/views.py @@ -25,7 +25,7 @@ OrganizationHasPermission, OrganizationAdminManagerAuthorization, IsCRMUser, - _is_orm_user + _is_orm_user, ) from connect.api.v1.organization.serializers import ( OrganizationSeralizer, @@ -51,7 +51,9 @@ from connect import billing from connect.billing.gateways.stripe_gateway import StripeGateway from connect.utils import count_contacts -from connect.api.v1.internal.intelligence.intelligence_rest_client import IntelligenceRESTClient +from connect.api.v1.internal.intelligence.intelligence_rest_client import ( + IntelligenceRESTClient, +) import pendulum from connect.common import tasks import logging @@ -59,7 +61,10 @@ from connect.internals.event_driven.producer.rabbitmq_publisher import RabbitmqPublisher from connect.usecases.authorizations.update import UpdateAuthorizationUseCase -from connect.usecases.authorizations.dto import DeleteAuthorizationDTO, UpdateAuthorizationDTO +from connect.usecases.authorizations.dto import ( + DeleteAuthorizationDTO, + UpdateAuthorizationDTO, +) from connect.usecases.authorizations.delete import DeleteAuthorizationUseCase @@ -76,7 +81,11 @@ class OrganizationViewSet( ): queryset = Organization.objects.all() serializer_class = OrganizationSeralizer - permission_classes = [IsAuthenticated, OrganizationHasPermission | IsCRMUser, Has2FA] + permission_classes = [ + IsAuthenticated, + OrganizationHasPermission | IsCRMUser, + Has2FA, + ] lookup_field = "uuid" metadata_class = Metadata @@ -101,7 +110,9 @@ def list(self, request, *args, **kwargs): page = self.paginate_queryset( self.filter_queryset(self.get_queryset().order_by("name")), ) - organization_serializer = OrganizationSeralizer(page, many=True, context=self.get_serializer_context()) + organization_serializer = OrganizationSeralizer( + page, many=True, context=self.get_serializer_context() + ) return self.get_paginated_response(organization_serializer.data) def create(self, request, *args, **kwargs): @@ -116,32 +127,23 @@ def create(self, request, *args, **kwargs): try: ai_client = IntelligenceRESTClient() ai_org = ai_client.create_organization( - user_email=user.email, - organization_name=org_info.get("name") - ) - org_info.update( - dict( - intelligence_organization=ai_org.get("id") - ) + user_email=user.email, organization_name=org_info.get("name") ) + org_info.update(dict(intelligence_organization=ai_org.get("id"))) except Exception as error: - data.update({ - "message": "Could not create organization in AI module", - "status": "FAILED" - }) + data.update( + { + "message": "Could not create organization in AI module", + "status": "FAILED", + } + ) logger.error(error) return Response(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) else: # for testing purposes - org_info.update( - dict( - intelligence_organization=randint(1, 99) - ) - ) + org_info.update(dict(intelligence_organization=randint(1, 99))) - cycle = BillingPlan._meta.get_field( - "cycle" - ).default + cycle = BillingPlan._meta.get_field("cycle").default new_organization = Organization.objects.create( name=org_info.get("name"), @@ -158,27 +160,23 @@ def create(self, request, *args, **kwargs): flows_info = tasks.create_template_project( project_info.get("name"), user.email, - project_info.get("timezone") + project_info.get("timezone"), ) else: flows_info = tasks.create_project( project_name=project_info.get("name"), user_email=user.email, - project_timezone=project_info.get("timezone") + project_timezone=project_info.get("timezone"), ) except Exception as error: - data.update({ - "message": "Could not create project", - "status": "FAILED" - }) + data.update( + {"message": "Could not create project", "status": "FAILED"} + ) logger.error(error) new_organization.delete() return Response(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) else: - flows_info = { - "id": randint(1, 100), - "uuid": uuid.uuid4() - } + flows_info = {"id": randint(1, 100), "uuid": uuid.uuid4()} project = Project.objects.create( name=project_info.get("name"), @@ -188,14 +186,14 @@ def create(self, request, *args, **kwargs): organization=new_organization, is_template=True if project_info.get("template") else False, created_by=user, - template_type=project_info.get("template_type") + template_type=project_info.get("template_type"), ) if len(Project.objects.filter(created_by=user)) == 1: data = dict( send_request_flow=settings.SEND_REQUEST_FLOW_PRODUCT, flow_uuid=settings.FLOW_PRODUCT_UUID, - token_authorization=settings.TOKEN_AUTHORIZATION_FLOW_PRODUCT + token_authorization=settings.TOKEN_AUTHORIZATION_FLOW_PRODUCT, ) user.send_request_flow_user_info(data) @@ -204,7 +202,7 @@ def create(self, request, *args, **kwargs): email=user.email, organization=new_organization, role=OrganizationRole.ADMIN.value, - created_by=user + created_by=user, ) # Create user's organizations authorizations @@ -213,27 +211,30 @@ def create(self, request, *args, **kwargs): email=auth.get("user_email"), organization=new_organization, role=auth.get("role"), - created_by=user + created_by=user, ) if project_info.get("template"): - data = { - "project": project, - "organization": new_organization - } + data = {"project": project, "organization": new_organization} project_data = TemplateProjectSerializer().create(data, request) if project_data.get("status") == "FAILED": new_organization.delete() project.delete() - return Response(project_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + project_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) - serializer = OrganizationSeralizer(new_organization, context={"request": request}) - project_serializer = ProjectSerializer(project, context={"request": request}) + serializer = OrganizationSeralizer( + new_organization, context={"request": request} + ) + project_serializer = ProjectSerializer( + project, context={"request": request} + ) response_data = dict( project=project_serializer.data, status="SUCCESS", message="", - organization=serializer.data + organization=serializer.data, ) except Exception as exception: @@ -247,15 +248,20 @@ def perform_destroy(self, instance): instance.send_email_delete_organization() instance.delete() ai_client = IntelligenceRESTClient() - ai_client.delete_organization(organization_id=intelligence_organization, user_email=self.request.user.email) + ai_client.delete_organization( + organization_id=intelligence_organization, + user_email=self.request.user.email, + ) def update(self, request, *args, **kwargs): data = request.data - partial = kwargs.pop('partial', False) + partial = kwargs.pop("partial", False) instance = self.get_object() if data.get("name"): - instance.send_email_change_organization_name(instance.name, data.get("name")) + instance.send_email_change_organization_name( + instance.name, data.get("name") + ) serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) @@ -363,7 +369,9 @@ def get_contact_active( "uuid": project.uuid, "name": project.name, "flow_organization": project.flow_organization, - "active_contacts": count_contacts(project=project, before=str(before), after=str(after)), + "active_contacts": count_contacts( + project=project, before=str(before), after=str(after) + ), } ) @@ -665,12 +673,7 @@ def set_2fa_required(self, request, organization_uuid): data = {"2fa_required": organization.enforce_2fa} return JsonResponse(data=data, status=status.HTTP_200_OK) - @action( - detail=True, - methods=['POST'], - url_path="billing/validate-customer-card" - - ) + @action(detail=True, methods=["POST"], url_path="billing/validate-customer-card") def validate_customer_card(self, request): customer = request.data.get("customer") if customer: @@ -682,17 +685,21 @@ def validate_customer_card(self, request): response["charge"] = gateway.card_verification_charge(customer) return JsonResponse(data=response, status=status.HTTP_200_OK) - return JsonResponse(data={"response": "no customer"}, status=status.HTTP_400_BAD_REQUEST) + return JsonResponse( + data={"response": "no customer"}, status=status.HTTP_400_BAD_REQUEST + ) @action( detail=True, methods=["POST"], url_name="organization-retrieve", - url_path="internal/retrieve" + url_path="internal/retrieve", ) def retrieve_organization(self, request): flow_organization_uuid = request.uuid - organization = Organization.objects.get(project__flow_organization=flow_organization_uuid) + organization = Organization.objects.get( + project__flow_organization=flow_organization_uuid + ) return { "status": status.HTTP_200_OK, "response": { @@ -702,7 +709,7 @@ def retrieve_organization(self, request): "inteligence_organization": organization.inteligence_organization, "extra_integration": organization.extra_integration, "is_suspended": organization.is_suspended, - } + }, } @action( @@ -718,7 +725,10 @@ def upgrade_plan(self, request, organization_uuid): self.check_object_permissions(self.request, organization) if not organization.organization_billing.stripe_customer: - return JsonResponse(data={"status": "FAILURE", "message": "Empty customer"}, status=status.HTTP_304_NOT_MODIFIED) + return JsonResponse( + data={"status": "FAILURE", "message": "Empty customer"}, + status=status.HTTP_304_NOT_MODIFIED, + ) org_billing = organization.organization_billing old_plan = organization.organization_billing.plan @@ -727,13 +737,19 @@ def upgrade_plan(self, request, organization_uuid): if not plan_info["valid"]: return JsonResponse( - data={"status": "FAILURE", "message": "Invalid plan choice"}, status=status.HTTP_400_BAD_REQUEST + data={"status": "FAILURE", "message": "Invalid plan choice"}, + status=status.HTTP_400_BAD_REQUEST, ) price = BillingPlan.plan_info(plan)["price"] if settings.TESTING: - p_intent = stripe.PaymentIntent(amount_received=price, id="pi_test_id", amount=price, charges={"amount": price, "amount_captured": price}) + p_intent = stripe.PaymentIntent( + amount_received=price, + id="pi_test_id", + amount=price, + charges={"amount": price, "amount_captured": price}, + ) purchase_result = {"status": "SUCCESS", "response": p_intent} if request.data.get("stripe_failure"): data["status"] = "FAILURE" @@ -762,17 +778,21 @@ def upgrade_plan(self, request, organization_uuid): old_plan, ) return JsonResponse( - data={"status": "SUCCESS", "old_plan": old_plan, "plan": org_billing.plan}, - status=status.HTTP_200_OK + data={ + "status": "SUCCESS", + "old_plan": old_plan, + "plan": org_billing.plan, + }, + status=status.HTTP_200_OK, ) return JsonResponse( data={"status": "FAILURE", "message": "Invalid plan choice"}, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) return JsonResponse( data={"status": "FAILURE", "message": "Stripe error"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR + status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) @@ -843,13 +863,15 @@ def update(self, *args, **kwargs): id=self.kwargs.get("user__id"), org_uuid=self.kwargs.get("organization__uuid"), role=int(data.get("role")), - request_user=self.request.user + request_user=self.request.user, ) usecase = UpdateAuthorizationUseCase(message_publisher=RabbitmqPublisher()) authorization = usecase.update_authorization(auth_dto) - instance.organization.send_email_permission_change(instance.user, old_permission, new_permission) + instance.organization.send_email_permission_change( + instance.user, old_permission, new_permission + ) return Response(data={"role": authorization.role}) @@ -862,12 +884,13 @@ def destroy(self, request, *args, **kwargs): IsAuthenticated, OrganizationAdminManagerAuthorization, ] + obj = self.get_object() self.filter_class = None self.lookup_field = "user__id" auth_dto = DeleteAuthorizationDTO( - id=self.kwargs.get("user__id"), - org_uuid=self.kwargs.get("organization__uuid"), + id=obj.user.id, + org_uuid=obj.organization.uuid, request_user=self.request.user, ) diff --git a/connect/usecases/authorizations/delete.py b/connect/usecases/authorizations/delete.py index bc3bd1c4..7e0ce8aa 100644 --- a/connect/usecases/authorizations/delete.py +++ b/connect/usecases/authorizations/delete.py @@ -2,17 +2,20 @@ from rest_framework.exceptions import PermissionDenied from connect.common.models import ( Organization, + OrganizationRole, User, Project, OrganizationAuthorization, RequestPermissionProject, ProjectAuthorization, - ) from connect.usecases.organizations.retrieve import RetrieveOrganizationUseCase from connect.usecases.users.retrieve import RetrieveUserUseCase -from connect.usecases.authorizations.dto import DeleteAuthorizationDTO, DeleteProjectAuthorizationDTO +from connect.usecases.authorizations.dto import ( + DeleteAuthorizationDTO, + DeleteProjectAuthorizationDTO, +) from connect.usecases.authorizations.usecase import AuthorizationUseCase @@ -21,6 +24,16 @@ class DeleteAuthorizationUseCase(AuthorizationUseCase): def delete_organization_authorization(self, user: User, org: Organization): try: authorization = org.authorizations.get(user=user) + if ( + org.authorizations.exclude(uuid=authorization.uuid) + .filter(role=OrganizationRole.ADMIN.value) + .count() + == 0 + ): + raise PermissionDenied( + "There must be at least one admin in the organization" + ) + authorization.delete() if self.publish_message: @@ -29,17 +42,24 @@ def delete_organization_authorization(self, user: User, org: Organization): org_uuid=str(org.uuid), user_email=user.email, role=authorization.role, - org_intelligence=org.inteligence_organization + org_intelligence=org.inteligence_organization, ) except OrganizationAuthorization.DoesNotExist: - print(f"OrganizationAuthorization matching query does not exist: Org {org.uuid} User {user.email}") + print( + f"OrganizationAuthorization matching query does not exist: Org {org.uuid} User {user.email}" + ) - def delete_project_authorization(self, project: Project, user: User, role: int = None): + def delete_project_authorization( + self, project: Project, user: User, role: int = None + ): authorization = project.project_authorizations.get(user=user) authorization.delete() - - if not ProjectAuthorization.objects.filter(user=user, organization_authorization=authorization.organization_authorization).exists(): + + if not ProjectAuthorization.objects.filter( + user=user, + organization_authorization=authorization.organization_authorization, + ).exists(): self.delete_organization_authorization(user=user, org=project.organization) if not role: @@ -55,38 +75,52 @@ def delete_project_authorization(self, project: Project, user: User, role: int = def delete_authorization(self, auth_dto: DeleteAuthorizationDTO): if auth_dto.request_user: - request_user : User = RetrieveUserUseCase().get_user_by_email(email=auth_dto.request_user) + request_user: User = RetrieveUserUseCase().get_user_by_email( + email=auth_dto.request_user + ) if auth_dto.user_email: - user: User = RetrieveUserUseCase().get_user_by_email(email=auth_dto.user_email) + user: User = RetrieveUserUseCase().get_user_by_email( + email=auth_dto.user_email + ) elif auth_dto.id: user: User = RetrieveUserUseCase().get_user_by_id(id=auth_dto.id) - org: Organization = RetrieveOrganizationUseCase().get_organization_by_uuid(org_uuid=auth_dto.org_uuid) + org: Organization = RetrieveOrganizationUseCase().get_organization_by_uuid( + org_uuid=auth_dto.org_uuid + ) if not org.authorizations.filter(user=request_user).exists(): - raise PermissionDenied("User does not have permission to perform this action") + raise PermissionDenied( + "User does not have permission to perform this action" + ) org_auth = org.authorizations.get(user=user) - projects_uuids: QuerySet = user.project_authorizations_user.filter(organization_authorization__organization=org).values_list("project", flat=True) + projects_uuids: QuerySet = user.project_authorizations_user.filter( + organization_authorization__organization=org + ).values_list("project", flat=True) for project_uuid in projects_uuids: project = Project.objects.get(uuid=project_uuid) project_role = self.organization_permission_mapper.get(org_auth.role) self.delete_project_authorization( - project=project, - user=user, - role=project_role + project=project, user=user, role=project_role ) - org_auth: OrganizationAuthorization = self.delete_organization_authorization(user=user, org=org) + org_auth: OrganizationAuthorization = self.delete_organization_authorization( + user=user, org=org + ) def delete_single_project_permission(self, auth_dto: DeleteProjectAuthorizationDTO): project = Project.objects.get(uuid=auth_dto.project_uuid) try: - request_auth = RequestPermissionProject.objects.get(email=auth_dto.user_email, project=project) + request_auth = RequestPermissionProject.objects.get( + email=auth_dto.user_email, project=project + ) request_auth.delete() except RequestPermissionProject.DoesNotExist: - user: User = RetrieveUserUseCase().get_user_by_email(email=auth_dto.user_email) + user: User = RetrieveUserUseCase().get_user_by_email( + email=auth_dto.user_email + ) self.delete_project_authorization(project=project, user=user)