-
Notifications
You must be signed in to change notification settings - Fork 7
Permissions Proof of Concept
Bibek Pandey edited this page Jul 5, 2023
·
3 revisions
from functools import reduce
from operator import concat
from typing import TypeAlias, Literal, List, TypedDict, Type
from django.db import models
from rest_framework import permissions, viewsets, request
# Each user role is associated with one of the visibility levels
VisibilityLevel: TypeAlias = Literal["ifrc", "ns", "movement", "membership", "public"]
RoleScope: TypeAlias = Literal["country", "region", "global"]
VISIBILITY_LEVELS: List[VisibilityLevel] = ["ifrc", "ns", "movement", "membership", "public"]
class VisibilityChoices(models.TextChoices):
IFRC = "ifrc"
NS = "ns"
MOVEMENT = "movement"
MEMBERSHIP = "membership"
PUBLIC = "public"
FieldReportPermission: TypeAlias = Literal["create", "edit", "delete", "publish"]
EmergencyPermission: TypeAlias = Literal["create", "edit", "delete"]
... # other
PermissionModule: TypeAlias = Literal[
"field_report",
"emergency",
# ...
]
# NOTE: The above PermissionModule and the following keys should be in sync.
# Unfortunately, can't get key of a TypedDict in python at the moment.
class Permissions(TypedDict, total=False):
field_report: List[FieldReportPermission]
emergency: List[EmergencyPermission] # NOTE: Need to handle this differently
... # other
class RoleChoices(models.IntegerChoices):
REGIONAL_ADMIN = 1
EMERGENCY_ADMIN = 2
...
class Role(TypedDict):
visibility_level: VisibilityLevel
permissions: Permissions
role_scope: RoleScope
ROLES: dict[int, Role] = {
RoleChoices.REGIONAL_ADMIN: {
"visibility_level": "ifrc",
"permissions": {
"field_report": ["create", "edit", "delete", "publish"]
},
"role_scope": "region",
},
RoleChoices.EMERGENCY_ADMIN: {
"visibility_level": "ifrc",
"permissions": {
"emergency": ["create", "edit"],
},
"role_scope": "global",
},
# ...
}
class UserRole(models.Model):
user = models.ForeignKey("User")
role = models.PositiveIntegerField(choices=RoleChoices.choices)
class UserCountry(models.Model):
user = models.ForeignKey("User")
country = models.ForeignKey("Country")
class UserRegion(models.Model):
user = models.ForeignKey("User")
region = models.ForeignKey("Region")
def get_permission_class_for(module: PermissionModule, scope: RoleScope = "global") -> Type[permissions.BasePermission]:
class IFRCGeneralPermission(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
user_roles = UserRole.objects.filter(user=request.user)
role_permissions = [ROLES[userrole.role]["permissions"] for userrole in user_roles]
module_permissions = [x.get(module, []) for x in role_permissions]
perms = reduce(concat, module_permissions)
if request.method == "POST" and "create" not in perms:
# TODO: Allow create in particular region, country?
return False
if request.method in ["PUT", "PATCH"] and "edit" not in perms:
return False
if request.method == "DELETE" and "delete" not in perms:
return False
# TODO: for other actions like publish/unpublish, for example:
# if request.action == "publish" and "publish" not in perms:
# return False
return True
def has_object_permission(self, request, view, obj):
user_countries = UserCountry.objects.filter(user=request.user)
user_regions = UserRegion.objects.filter(user=request.user)
if scope == "country" and hasattr(obj, "country"):
return obj.country in [x.country for x in user_countries]
elif scope == "region" and hasattr(obj, "region"):
return obj.region in [x.region for x in user_regions]
return True
return IFRCGeneralPermission
# EXAMPLE
class FieldReportViewSet(viewsets.ModelViewset):
permission_classes = [get_permission_class_for("field_report", "global")]
...
class ReadOnlyVisibilityViewsetMixin:
request: request.Request
def get_visibility_queryset(self, queryset):
choices = VisibilityChoices
if not self.request.user.is_authenticated:
return queryset.filter(visibility=choices.PUBLIC)
user_roles = UserRole.objects.filter(user=self.request.user)
visibility_levels = [ROLES[userrole.role]["visibility_level"] for userrole in user_roles]
if "ifrc" in visibility_levels:
return queryset
elif "movement" in visibility_levels:
return queryset.filter(visibility__in=[choices.MOVEMENT, choices.PUBLIC, choices.MEMBERSHIP])
elif "membership" in visibility_levels:
return queryset.filter(visibility__in=[choices.MEMBERSHIP, choices.PUBLIC])
elif "ns" in visibility_levels:
levels = [choices.MOVEMENT, choices.PUBLIC, choices.MEMBERSHIP]
user_countries = UserCountry.objects.filter(user=self.request.user)\
.values_list('id', flat=True)
return queryset.filter(
models.Q(visibility__in=levels) |
models.Q(visibility=choices.NS, country_id__in=user_countries) # TODO: check regions
)
return queryset.filter(visibility=choices.PUBLIC)
def get_queryset(self):
queryset = super().get_queryset()
return self.get_visibility_queryset(queryset)