Skip to content

Commit

Permalink
feat: use django permissions to allow/deny actions
Browse files Browse the repository at this point in the history
  • Loading branch information
pablolmedorado committed Jun 13, 2021
1 parent 75852b5 commit 460d028
Show file tree
Hide file tree
Showing 33 changed files with 280 additions and 129 deletions.
15 changes: 8 additions & 7 deletions backend/breakfasts/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

from .serializers import BaseSerializer, BreadSerializer, BreakfastSerializer, DrinkSerializer, IngredientSerializer
from ..models import Base, Bread, Breakfast, Drink, Ingredient
from common.api.permissions import IsAdminUserOrReadOnly, IsOwnerOrReadOnly
from common.api.permissions import HasDjangoPermissionOrReadOnly, IsOwnerOrReadOnly
from common.api.viewsets import AtomicFlexFieldsModelViewSet
from common.api.utils import check_api_user_permissions


class BreadViewSet(AtomicFlexFieldsModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsAdminUserOrReadOnly)
permission_classes = (permissions.IsAuthenticated, HasDjangoPermissionOrReadOnly)
queryset = Bread.objects.all()
serializer_class = BreadSerializer
search_fields = ("name",)
Expand All @@ -16,7 +17,7 @@ class BreadViewSet(AtomicFlexFieldsModelViewSet):


class BaseViewSet(AtomicFlexFieldsModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsAdminUserOrReadOnly)
permission_classes = (permissions.IsAuthenticated, HasDjangoPermissionOrReadOnly)
queryset = Base.objects.all()
serializer_class = BaseSerializer
search_fields = ("name",)
Expand All @@ -25,7 +26,7 @@ class BaseViewSet(AtomicFlexFieldsModelViewSet):


class IngredientViewSet(AtomicFlexFieldsModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsAdminUserOrReadOnly)
permission_classes = (permissions.IsAuthenticated, HasDjangoPermissionOrReadOnly)
queryset = Ingredient.objects.all()
serializer_class = IngredientSerializer
search_fields = ("name",)
Expand All @@ -34,7 +35,7 @@ class IngredientViewSet(AtomicFlexFieldsModelViewSet):


class DrinkViewSet(AtomicFlexFieldsModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsAdminUserOrReadOnly)
permission_classes = (permissions.IsAuthenticated, HasDjangoPermissionOrReadOnly)
queryset = Drink.objects.all()
serializer_class = DrinkSerializer
search_fields = ("name",)
Expand All @@ -60,11 +61,11 @@ class BreakfastViewSet(AtomicFlexFieldsModelViewSet):
ordering = ("user__acronym",)

def perform_create(self, serializer):
if not self.request.user.is_superuser:
if not check_api_user_permissions(self):
serializer.validated_data["user"] = self.request.user
return super().perform_create(serializer)

def perform_update(self, serializer):
if not self.request.user.is_superuser:
if not check_api_user_permissions(self):
serializer.validated_data["user"] = serializer.instance.user
return super().perform_update(serializer)
15 changes: 13 additions & 2 deletions backend/common/api/permissions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from rest_framework import permissions

from .utils import check_api_user_permissions


class IsAdminUserOrReadOnly(permissions.BasePermission):
"""
Expand All @@ -10,13 +12,22 @@ def has_permission(self, request, view):
return bool(request.method in permissions.SAFE_METHODS or request.user.is_superuser)


class HasDjangoPermissionOrReadOnly(permissions.BasePermission):
"""
The requester has the needed permission over the Django Model, or is a read-only request.
"""

def has_permission(self, request, view):
return bool(request.method in permissions.SAFE_METHODS or check_api_user_permissions(view))


class IsOwnerOrReadOnly(permissions.BasePermission):
"""
The requester is the owner of the record, or is a read-only request.
"""

def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS or request.user.is_superuser:
if request.method in permissions.SAFE_METHODS or check_api_user_permissions(view):
return True
if not hasattr(obj, "OWNERSHIP_FIELD"):
return False
Expand All @@ -26,6 +37,6 @@ def has_object_permission(self, request, view, obj):

class NotificationPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS or request.user.is_superuser:
if request.method in permissions.SAFE_METHODS or check_api_user_permissions(view):
return True
return obj.recipient == request.user
18 changes: 18 additions & 0 deletions backend/common/api/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.db.models import QuerySet

from rest_framework.generics import GenericAPIView

PERMISSION_ACTIONS_BY_METHOD = {"GET": "view", "POST": "add", "PUT": "change", "PATCH": "change", "DELETE": "delete"}


def check_api_user_permissions(view: GenericAPIView) -> bool:
if view.request.user.is_superuser:
return True

queryset: QuerySet = view.get_queryset()
app: str = queryset.model._meta.app_label
model: str = queryset.model._meta.model_name
action: str = PERMISSION_ACTIONS_BY_METHOD[view.request.method]

permission: str = f"{app}.{action}_{model}"
return permission in view.request.user.get_all_permissions()
1 change: 1 addition & 0 deletions backend/common/templatetags/user_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ def user_data(user):
"acronym": user.acronym,
"is_staff": user.is_staff,
"is_superuser": user.is_superuser,
"permissions": [] if user.is_superuser else sorted(list(user.get_all_permissions())),
}
return mark_safe(f"<script>var djangoUserData = {json.dumps(user_data)};</script>")
4 changes: 2 additions & 2 deletions backend/events/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
from ..models import Event, EventType
from ..utils import monthly_chart_data
from common.api.mixins import AuthorshipMixin
from common.api.permissions import IsAdminUserOrReadOnly, IsOwnerOrReadOnly
from common.api.permissions import HasDjangoPermissionOrReadOnly, IsOwnerOrReadOnly
from common.api.viewsets import AtomicFlexFieldsModelViewSet


class EventTypeViewSet(AtomicFlexFieldsModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsAdminUserOrReadOnly)
permission_classes = (permissions.IsAuthenticated, HasDjangoPermissionOrReadOnly)
queryset = EventType.objects.all()
serializer_class = EventTypeSerializer
filter_fields = ("important", "system")
Expand Down
2 changes: 1 addition & 1 deletion backend/events/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class EventQuerySet(models.QuerySet):
def by_user(self, user):
if user.is_superuser:
if user.is_superuser or "events.view_event" in user.get_all_permissions():
return self
return self.filter(Q(visibility="PU") | Q(attendees=user) | Q(groups__user=user) | Q(creation_user=user))

Expand Down
12 changes: 7 additions & 5 deletions backend/scrum/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@

from rest_framework import permissions

from common.api.utils import check_api_user_permissions


class UserStoryPermission(permissions.BasePermission):
def has_permission(self, request, view):
if view.action in ["update", "partial_update", "validate"] or request.user.is_superuser:
if view.action in ["update", "partial_update", "validate"] or check_api_user_permissions(view):
return True
return request.method in permissions.SAFE_METHODS

def has_object_permission(self, request, view, obj):
if request.user.is_superuser:
if check_api_user_permissions(view):
return True
if request.user in [obj.development_user, obj.validation_user, obj.support_user]:
return bool(
Expand All @@ -23,12 +25,12 @@ def has_object_permission(self, request, view, obj):

class TaskPermission(permissions.BasePermission):
def has_permission(self, request, view):
if view.action == "toggle" or request.user.is_superuser:
if view.action == "toggle" or check_api_user_permissions(view):
return True
return request.method in permissions.SAFE_METHODS

def has_object_permission(self, request, view, obj):
if request.user.is_superuser:
if check_api_user_permissions(view):
return True
if request.user == obj.user_story.development_user:
return bool(request.method in permissions.SAFE_METHODS or view.action == "toggle")
Expand All @@ -37,7 +39,7 @@ def has_object_permission(self, request, view, obj):

class EffortPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.user.is_superuser:
if check_api_user_permissions(view):
return True
if request.user == obj.user and obj.creation_datetime >= timezone.now() - timedelta(minutes=30):
return True
Expand Down
11 changes: 6 additions & 5 deletions backend/scrum/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@
user_story_user_chart_data,
)
from common.api.mixins import AuthorshipMixin, OrderedMixin
from common.api.permissions import IsAdminUserOrReadOnly
from common.api.permissions import HasDjangoPermissionOrReadOnly
from common.api.utils import check_api_user_permissions
from common.api.viewsets import AtomicFlexFieldsModelViewSet


class SprintViewSet(AuthorshipMixin, AtomicFlexFieldsModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsAdminUserOrReadOnly)
permission_classes = (permissions.IsAuthenticated, HasDjangoPermissionOrReadOnly)
serializer_class = SprintSerializer
filterset_class = SprintFilterSet
search_fields = ("name",)
Expand Down Expand Up @@ -95,7 +96,7 @@ def deployment_report(self, request, *args, **kwargs):


class EpicViewSet(AuthorshipMixin, AtomicFlexFieldsModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsAdminUserOrReadOnly)
permission_classes = (permissions.IsAuthenticated, HasDjangoPermissionOrReadOnly)
queryset = Epic.objects.with_current_progress().all().prefetch_related("tags").distinct()
serializer_class = EpicSerializer
filterset_class = EpicFilterSet
Expand All @@ -105,7 +106,7 @@ class EpicViewSet(AuthorshipMixin, AtomicFlexFieldsModelViewSet):


class UserStoryTypeViewSet(AtomicFlexFieldsModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsAdminUserOrReadOnly)
permission_classes = (permissions.IsAuthenticated, HasDjangoPermissionOrReadOnly)
queryset = UserStoryType.objects.all()
serializer_class = UserStoryTypeSerializer
search_fields = ("name",)
Expand Down Expand Up @@ -167,7 +168,7 @@ def get_queryset(self, *args, **kwargs):

def get_serializer_class(self):
serializer_class = super().get_serializer_class()
if self.request.user.is_superuser:
if check_api_user_permissions(self):
return serializer_class
if self.request.method not in permissions.SAFE_METHODS and self.detail:
instance = self.get_object()
Expand Down
10 changes: 6 additions & 4 deletions backend/work_organization/api/permissions.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from rest_framework import permissions

from common.api.utils import check_api_user_permissions


class HolidayPermission(permissions.BasePermission):
def has_permission(self, request, view):
if view.action in ["request", "cancel"] or request.user.is_superuser:
if view.action in ["request", "cancel"] or check_api_user_permissions(view):
return True
return request.method in permissions.SAFE_METHODS

def has_object_permission(self, request, view, obj):
if request.user.is_superuser:
if check_api_user_permissions(view):
return True
if request.user == obj.user:
return bool(request.method in permissions.SAFE_METHODS or view.action == "cancel")
Expand All @@ -17,11 +19,11 @@ def has_object_permission(self, request, view, obj):

class GreenWorkingDayPermission(permissions.BasePermission):
def has_permission(self, request, view):
if view.action == "toggle_volunteer" or request.user.is_superuser:
if view.action == "toggle_volunteer" or check_api_user_permissions(view):
return True
return request.method in permissions.SAFE_METHODS

def has_object_permission(self, request, view, obj):
if request.user.is_superuser:
if check_api_user_permissions(view):
return True
return bool(request.method in permissions.SAFE_METHODS or view.action == "toggle_volunteer")
6 changes: 3 additions & 3 deletions backend/work_organization/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from ..models import GreenWorkingDay, Holiday, HolidayType, SupportWorkingDay
from ..utils import green_working_day_user_chart_data, support_working_day_user_chart_data, user_availability_chart_data
from common.api.mixins import AtomicBulkCreateModelMixin, AuthorshipMixin
from common.api.permissions import IsAdminUserOrReadOnly
from common.api.permissions import HasDjangoPermissionOrReadOnly
from common.api.viewsets import AtomicFlexFieldsModelViewSet


Expand Down Expand Up @@ -56,7 +56,7 @@ def user_chart(self, request, *args, **kwargs):


class SupportWorkingDayViewSet(AuthorshipMixin, FlatDatesMixin, BulkCreateModelMixin, AtomicFlexFieldsModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsAdminUserOrReadOnly)
permission_classes = (permissions.IsAuthenticated, HasDjangoPermissionOrReadOnly)
queryset = SupportWorkingDay.objects.all()
serializer_class = SupportWorkingDaySerializer
permit_list_expands = ["user"]
Expand All @@ -72,7 +72,7 @@ def user_chart(self, request, *args, **kwargs):


class HolidayTypeViewSet(AtomicFlexFieldsModelViewSet):
permission_classes = (permissions.IsAuthenticated, IsAdminUserOrReadOnly)
permission_classes = (permissions.IsAuthenticated, HasDjangoPermissionOrReadOnly)
queryset = HolidayType.objects.all()
serializer_class = HolidayTypeSerializer
filter_fields = ("system",)
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/calendar/EventCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@
</v-menu>
<template v-if="!eventTypesMap[item.type].system">
<v-divider vertical inset />
<v-btn v-if="isEditable" icon @click="onEdit(item)">
<v-btn v-if="canEdit" icon @click="onEdit(item)">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon @click="onEdit(itemForCopy)">
<v-icon>mdi-content-copy</v-icon>
</v-btn>
<v-btn v-if="isEditable" icon @click.stop="onDelete(item)">
<v-btn v-if="canDelete" icon @click.stop="onDelete(item)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/common/TruncatedText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<v-menu
:disabled="!isTruncated"
:close-on-content-click="false"
:open-on-hover="true"
:nudge-width="200"
open-on-hover
max-width="400px"
offset-x
>
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/scrum/dialogs/EffortReportDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
</v-chip>
</v-col>
</v-row>
<v-row v-if="loggedUser.is_superuser">
<v-row v-if="showUserChart">
<v-col>
<v-card outlined>
<EffortUserTimelineChart
Expand Down Expand Up @@ -76,6 +76,7 @@ import EffortRoleTimelineChart from "@/components/scrum/charts/EffortRoleTimelin
import EffortUserTimelineChart from "@/components/scrum/charts/EffortUserTimelineChart";
import useLoading from "@/composables/useLoading";
import { userHasPermission } from "@/utils/permissions";
export default {
name: "EffortReportDialog",
Expand All @@ -97,6 +98,9 @@ export default {
computed: {
...mapState(["loggedUser"]),
...mapGetters("users", ["usersMap"]),
showUserChart() {
return userHasPermission("scrum.view_effort");
},
filteredUsers() {
if (!this.filters.user_id__in) {
return undefined;
Expand All @@ -119,7 +123,7 @@ export default {
this.showDialog = false;
},
refresh() {
if (this.loggedUser.is_superuser) {
if (this.showUserChart) {
this.$refs.userChart.fetchChartData();
}
this.$refs.roleChart.fetchChartData();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<template #item.done="{ item }">
<v-btn
icon
:disabled="isLoading || (!loggedUser.is_superuser && loggedUser.id !== item.user_story.development_user)"
:disabled="isLoading || !canEdit(item)"
:loading="isTaskLoading('toggle-item', item.id)"
@click="toggleItem(item)"
>
Expand All @@ -52,6 +52,7 @@ import DialogMixin from "@/mixins/dialog-mixin";
import TaskService from "@/services/scrum/task-service";
import useLoading from "@/composables/useLoading";
import { userHasPermission } from "@/utils/permissions";
export default {
name: "TaskQuickManagementDialog",
Expand Down Expand Up @@ -109,6 +110,9 @@ export default {
},
},
methods: {
canEdit(item) {
return this.loggedUser.id === item.user_story.development_user || userHasPermission("scrum.change_task");
},
async toggleItem(item) {
this.addTask("toggle-item", item.id);
try {
Expand Down
Loading

0 comments on commit 460d028

Please sign in to comment.