From 1179048ed1a4614ca9a5893d26ac79e5dd1034cf Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:06:48 -0300 Subject: [PATCH] Feature/permission cache (#406) * feat: add cache to ProjectAdminAuthentication endpoint * feat: fix authenticate credential method * feat: update external views to work with new cached authentication * feat: use new projectadmindto when not cached aswell * feat: fix authenticate_credentials * feat: use __dict__ when caching the data * feat: fix user first name if user is none on external authentication * feat: optimize query on sector, queue and agents external endpoints --- .../authentication/drf/authorization.py | 49 ++++++++++++++- chats/apps/api/v1/external/agents/filters.py | 4 +- chats/apps/api/v1/external/agents/viewsets.py | 15 ++--- chats/apps/api/v1/external/msgs/filters.py | 59 +++++++++++++++++++ chats/apps/api/v1/external/msgs/viewsets.py | 13 ++-- chats/apps/api/v1/external/permissions.py | 12 ++-- chats/apps/api/v1/external/queues/viewsets.py | 17 ++---- chats/apps/api/v1/external/rooms/viewsets.py | 35 +++++++---- .../apps/api/v1/external/sectors/viewsets.py | 15 ++--- 9 files changed, 160 insertions(+), 59 deletions(-) diff --git a/chats/apps/accounts/authentication/drf/authorization.py b/chats/apps/accounts/authentication/drf/authorization.py index b6d419ff..8d6765af 100644 --- a/chats/apps/accounts/authentication/drf/authorization.py +++ b/chats/apps/accounts/authentication/drf/authorization.py @@ -1,14 +1,32 @@ +import json + +from django.conf import settings from django.utils.translation import gettext_lazy as _ +from django_redis import get_redis_connection from rest_framework import exceptions from rest_framework.authentication import TokenAuthentication, get_authorization_header from chats.apps.projects.models import ProjectPermission +class ProjectAdminDTO: + def __init__( + self, pk: str, project: str, user_email: str, user_first_name: str, role: int + ) -> None: + self.pk = pk + self.project = project + self.user_email = user_email + self.user_first_name = user_first_name + self.role = role + + class ProjectAdminAuthentication(TokenAuthentication): keyword = "Bearer" model = ProjectPermission + cache_token = settings.OIDC_CACHE_TOKEN + cache_ttl = settings.OIDC_CACHE_TTL + def authenticate(self, request): auth = get_authorization_header(request).split() @@ -32,13 +50,38 @@ def authenticate(self, request): return self.authenticate_credentials(token) - def authenticate_credentials(self, key): + def _authenticate_credentials(self, key): model = self.get_model() try: authorization = model.auth.get(uuid=key) if not authorization.is_admin: raise exceptions.PermissionDenied() - - return (authorization.user, authorization) + authorization = ProjectAdminDTO( + pk=str(authorization.pk), + project=str(authorization.project_id), + user_email=authorization.user_id or "", + user_first_name=( + authorization.user.first_name if authorization.user else "" + ), + role=authorization.role, + ) + return (authorization.user_email, authorization) except ProjectPermission.DoesNotExist: raise exceptions.AuthenticationFailed(_("Invalid token.")) + + def authenticate_credentials(self, key): + if not self.cache_token: + return self._authenticate_credentials(key) + redis_connection = get_redis_connection() + + cache_authorization = redis_connection.get(key) + + if cache_authorization is not None: + cache_authorization = json.loads(cache_authorization) + authorization = ProjectAdminDTO(**cache_authorization) + return (authorization.user_email, authorization) + + authorization = self._authenticate_credentials(key)[1] + redis_connection.set(key, json.dumps(authorization.__dict__), self.cache_ttl) + + return (authorization.user_email, authorization) diff --git a/chats/apps/api/v1/external/agents/filters.py b/chats/apps/api/v1/external/agents/filters.py index 135678f8..b22599e5 100644 --- a/chats/apps/api/v1/external/agents/filters.py +++ b/chats/apps/api/v1/external/agents/filters.py @@ -24,7 +24,7 @@ class Meta: ) def filter_queue(self, queryset, name, value): - return queryset.filter(queue_authorizations__queue__uuid=value) + return queryset.filter(queue_authorizations__queue=value) def filter_sector(self, queryset, name, value): - return queryset.filter(sector_authorizations__sector__uuid=value) + return queryset.filter(sector_authorizations__sector=value) diff --git a/chats/apps/api/v1/external/agents/viewsets.py b/chats/apps/api/v1/external/agents/viewsets.py index 97b76a52..8e9bac50 100644 --- a/chats/apps/api/v1/external/agents/viewsets.py +++ b/chats/apps/api/v1/external/agents/viewsets.py @@ -1,6 +1,5 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets -from rest_framework.permissions import IsAuthenticated from chats.apps.accounts.authentication.drf.authorization import ( ProjectAdminAuthentication, @@ -10,24 +9,18 @@ from chats.apps.projects.models import ProjectPermission -def get_permission_token_from_request(request): - auth_header = request.META.get("HTTP_AUTHORIZATION") - return auth_header.split()[1] - - class AgentFlowViewset(viewsets.ReadOnlyModelViewSet): model = ProjectPermission queryset = ProjectPermission.objects.all() serializer_class = AgentFlowSerializer filter_backends = [DjangoFilterBackend] filterset_class = AgentFlowFilter - permission_classes = [ - IsAuthenticated, - ] lookup_field = "uuid" authentication_classes = [ProjectAdminAuthentication] def get_queryset(self): - permission = get_permission_token_from_request(self.request) + permission = self.request.auth qs = super().get_queryset() - return qs.filter(project__permissions=permission, project__permissions__role=1) + if permission is None or permission.role != 1: + return qs.none() + return qs.filter(project=permission.project) diff --git a/chats/apps/api/v1/external/msgs/filters.py b/chats/apps/api/v1/external/msgs/filters.py index e69de29b..f3b86e99 100644 --- a/chats/apps/api/v1/external/msgs/filters.py +++ b/chats/apps/api/v1/external/msgs/filters.py @@ -0,0 +1,59 @@ +from django.utils.translation import gettext_lazy as _ +from django_filters import rest_framework as filters + +from chats.apps.msgs.models import Message + + +class MessageFilter(filters.FilterSet): + class Meta: + model = Message + fields = ["contact", "room"] + + contact = filters.UUIDFilter( + field_name="contact", + required=False, + method="filter_contact", + help_text=_("Contact's UUID"), + ) + + room = filters.UUIDFilter( + field_name="room", + required=False, + method="filter_room", + help_text=_("Room's UUID"), + ) + + project = filters.UUIDFilter( + field_name="project", + required=False, + method="filter_project", + help_text=_("Projects's UUID"), + ) + + is_active = filters.BooleanFilter( + field_name="is_active", + required=False, + method="filter_is_active", + help_text=_("Is room active"), + ) + + def filter_room(self, queryset, name, value): + return queryset.filter(room__uuid=value) + + def filter_is_active(self, queryset, name, value): + return queryset.filter(room__is_active=value) + + def filter_project(self, queryset, name, value): + return queryset.filter(room__queue__sector__project__uuid=value) + + def filter_contact(self, queryset, name, value): + """ + Return msgs given a contact. + """ + permission = self.request.auth + queryset = queryset.filter( + room__queue__sector__project=permission.project, + room__contact__uuid=value, + ) + + return queryset diff --git a/chats/apps/api/v1/external/msgs/viewsets.py b/chats/apps/api/v1/external/msgs/viewsets.py index 86e38ee7..1f8c9ff4 100644 --- a/chats/apps/api/v1/external/msgs/viewsets.py +++ b/chats/apps/api/v1/external/msgs/viewsets.py @@ -4,9 +4,9 @@ from chats.apps.accounts.authentication.drf.authorization import ( ProjectAdminAuthentication, ) +from chats.apps.api.v1.external.msgs.filters import MessageFilter from chats.apps.api.v1.external.msgs.serializers import MsgFlowSerializer from chats.apps.api.v1.external.permissions import IsAdminPermission -from chats.apps.api.v1.msgs.filters import MessageFilter from chats.apps.msgs.models import Message as ChatMessage @@ -24,10 +24,15 @@ class MessageFlowViewset( authentication_classes = [ProjectAdminAuthentication] lookup_field = "uuid" - def create(self, request, *args, **kwargs): - return super().create(request, *args, **kwargs) - def perform_create(self, serializer): + validated_data = serializer.validated_data + room = validated_data.get("room") + if room.project_uuid != self.request.auth.project: + self.permission_denied( + self.request, + message="Ticketer token permission failed on room project", + code=403, + ) instance = serializer.save() instance.notify_room("create") room = instance.room diff --git a/chats/apps/api/v1/external/permissions.py b/chats/apps/api/v1/external/permissions.py index 318e014e..1bb76a18 100644 --- a/chats/apps/api/v1/external/permissions.py +++ b/chats/apps/api/v1/external/permissions.py @@ -6,7 +6,7 @@ class IsAdminPermission(permissions.BasePermission): def has_permission(self, request, view): # pragma: no cover - if view.action in ["list", "create"]: + if view.action == "list": try: permission = request.auth project = permission.project @@ -18,7 +18,6 @@ def has_permission(self, request, view): # pragma: no cover return validation.is_valid except (AttributeError, IndexError, ProjectPermission.DoesNotExist): return False - return super().has_permission(request, view) def has_object_permission(self, request, view, obj): @@ -30,7 +29,7 @@ def has_object_permission(self, request, view, obj): project = obj.project except ProjectPermission.DoesNotExist: return False - return permission.project == project + return permission.project == str(project.uuid) LEVEL_NAME_MAPPING = { @@ -74,9 +73,12 @@ def __init__(self, request_data, project) -> None: def is_valid(self): try: if self.level_name == "project": - return str(self.project.pk) == self.level_id + return str(self.project) == self.level_id if self.queryset != {}: - return self.project.sectors.filter(**self.queryset).exists() + from chats.apps.projects.models import Project + + project = Project.objects.get(pk=self.project) + return project.sectors.filter(**self.queryset).exists() except ObjectDoesNotExist: return False return False diff --git a/chats/apps/api/v1/external/queues/viewsets.py b/chats/apps/api/v1/external/queues/viewsets.py index e68f5c63..c2f6af63 100644 --- a/chats/apps/api/v1/external/queues/viewsets.py +++ b/chats/apps/api/v1/external/queues/viewsets.py @@ -4,31 +4,24 @@ from chats.apps.accounts.authentication.drf.authorization import ( ProjectAdminAuthentication, ) -from chats.apps.api.v1.external.permissions import IsAdminPermission from chats.apps.api.v1.external.queues.filters import QueueFlowFilter from chats.apps.api.v1.external.queues.serializers import QueueFlowSerializer from chats.apps.queues.models import Queue -def get_permission_token_from_request(request): - auth_header = request.META.get("HTTP_AUTHORIZATION") - return auth_header.split()[1] - - class QueueFlowViewset(viewsets.ReadOnlyModelViewSet): model = Queue queryset = Queue.objects.exclude(is_deleted=True) serializer_class = QueueFlowSerializer filter_backends = [DjangoFilterBackend] filterset_class = QueueFlowFilter - permission_classes = [ - IsAdminPermission, - ] + lookup_field = "uuid" authentication_classes = [ProjectAdminAuthentication] def get_queryset(self): - permission = get_permission_token_from_request(self.request) + permission = self.request.auth qs = super().get_queryset() - - return qs.filter(sector__project__permissions__uuid=permission) + if permission is None or permission.role != 1: + return qs.none() + return qs.filter(sector__project=permission.project) diff --git a/chats/apps/api/v1/external/rooms/viewsets.py b/chats/apps/api/v1/external/rooms/viewsets.py index ae4f1536..3a03edad 100644 --- a/chats/apps/api/v1/external/rooms/viewsets.py +++ b/chats/apps/api/v1/external/rooms/viewsets.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import ObjectDoesNotExist from django.db import IntegrityError from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend @@ -99,12 +99,21 @@ def create(self, request, *args, **kwargs): ) def perform_create(self, serializer): - serializer.save() - if serializer.instance.flowstarts.exists(): - instance = serializer.instance + validated_data = serializer.validated_data + queue_or_sector = validated_data.get("queue") or validated_data.get("sector") + project = queue_or_sector.project + if str(project.pk) != self.request.auth.project: + self.permission_denied( + self.request, + message="Ticketer token permission failed on room project", + code=403, + ) + room = serializer.save() + if room.flowstarts.exists(): + instance = room notification_type = "update" else: - instance = add_user_or_queue_to_room(serializer.instance, self.request) + instance = add_user_or_queue_to_room(room, self.request) notification_type = "create" notify_level = "user" if instance.user else "queue" @@ -139,13 +148,16 @@ def partial_update(self, request, pk=None): ) request_permission = self.request.auth project = request_permission.project - try: - room = Room.objects.get( + room = ( + Room.objects.filter( callback_url__endswith=pk, - queue__sector__project=project, + project_uuid=project, is_active=True, ) - except (Room.DoesNotExist, ValidationError): + .select_related("user", "queue__sector__project") + .first() + ) + if room is None: return Response( { "Detail": "Ticket with the given id was not found, it does not exist or it is closed" @@ -171,6 +183,7 @@ def partial_update(self, request, pk=None): ) try: agent = filters.get("agent") + project = room.project agent_permission = project.permissions.get(user__email=agent) except ObjectDoesNotExist: return Response( @@ -239,7 +252,7 @@ def partial_update(self, request, pk=None): new_custom_field_value = data["fields"][custom_field_name] update_flows_custom_fields( - project=room.queue.sector.project, + project=room.project, data=data, contact_id=room.contact.external_id, ) @@ -247,7 +260,7 @@ def partial_update(self, request, pk=None): update_custom_fields(room, custom_fields_update) feedback = { - "user": request_permission.user.first_name, + "user": request_permission.user_first_name, "custom_field_name": custom_field_name, "old": old_custom_field_value, "new": new_custom_field_value, diff --git a/chats/apps/api/v1/external/sectors/viewsets.py b/chats/apps/api/v1/external/sectors/viewsets.py index aef5f36c..f8cbc00c 100644 --- a/chats/apps/api/v1/external/sectors/viewsets.py +++ b/chats/apps/api/v1/external/sectors/viewsets.py @@ -4,16 +4,10 @@ from chats.apps.accounts.authentication.drf.authorization import ( ProjectAdminAuthentication, ) -from chats.apps.api.v1.external.permissions import IsAdminPermission from chats.apps.api.v1.external.sectors.serializers import SectorFlowSerializer from chats.apps.sectors.models import Sector -def get_permission_token_from_request(request): - auth_header = request.META.get("HTTP_AUTHORIZATION") - return auth_header.split()[1] - - class SectorFlowViewset(viewsets.ReadOnlyModelViewSet): model = Sector queryset = Sector.objects.exclude(is_deleted=True) @@ -22,13 +16,12 @@ class SectorFlowViewset(viewsets.ReadOnlyModelViewSet): filterset_fields = [ "name", ] - permission_classes = [ - IsAdminPermission, - ] lookup_field = "uuid" authentication_classes = [ProjectAdminAuthentication] def get_queryset(self): - permission = get_permission_token_from_request(self.request) + permission = self.request.auth qs = super().get_queryset() - return qs.filter(project__permissions__uuid=permission) + if permission is None or permission.role != 1: + return qs.none() + return qs.filter(project=permission.project)