Skip to content

Commit

Permalink
Feature/permission cache (#406)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
helllllllder authored Oct 22, 2024
1 parent b351e26 commit 1179048
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 59 deletions.
49 changes: 46 additions & 3 deletions chats/apps/accounts/authentication/drf/authorization.py
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -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)
4 changes: 2 additions & 2 deletions chats/apps/api/v1/external/agents/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
15 changes: 4 additions & 11 deletions chats/apps/api/v1/external/agents/viewsets.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)
59 changes: 59 additions & 0 deletions chats/apps/api/v1/external/msgs/filters.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 9 additions & 4 deletions chats/apps/api/v1/external/msgs/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
Expand Down
12 changes: 7 additions & 5 deletions chats/apps/api/v1/external/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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 = {
Expand Down Expand Up @@ -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
17 changes: 5 additions & 12 deletions chats/apps/api/v1/external/queues/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
35 changes: 24 additions & 11 deletions chats/apps/api/v1/external/rooms/viewsets.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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(
Expand Down Expand Up @@ -239,15 +252,15 @@ 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,
)

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,
Expand Down
Loading

0 comments on commit 1179048

Please sign in to comment.