Skip to content

Commit

Permalink
feat: permission cache
Browse files Browse the repository at this point in the history
  • Loading branch information
helllllllder committed Oct 17, 2024
1 parent 5aa6b8c commit 6ceabec
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 37 deletions.
47 changes: 44 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,36 @@ 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,
user_first_name=authorization.user.first_name,
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)
7 changes: 5 additions & 2 deletions chats/apps/api/v1/external/agents/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class AgentFlowViewset(viewsets.ReadOnlyModelViewSet):
authentication_classes = [ProjectAdminAuthentication]

def get_queryset(self):
permission = get_permission_token_from_request(self.request)
# 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)
return qs.filter(
project__permissions=permission.pk, project__permissions__role=1
)
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
9 changes: 6 additions & 3 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 Down Expand Up @@ -74,9 +74,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
9 changes: 2 additions & 7 deletions chats/apps/api/v1/external/queues/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@
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)
Expand All @@ -28,7 +23,7 @@ class QueueFlowViewset(viewsets.ReadOnlyModelViewSet):
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)
return qs.filter(sector__project__permissions__uuid=permission.pk)
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
9 changes: 2 additions & 7 deletions chats/apps/api/v1/external/sectors/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@
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)
Expand All @@ -29,6 +24,6 @@ class SectorFlowViewset(viewsets.ReadOnlyModelViewSet):
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)
return qs.filter(project__permissions__uuid=permission.pk)

0 comments on commit 6ceabec

Please sign in to comment.