Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("planner", "0045_alter_daysettings_housing_corporation_combiteam"),
]

operations = [
migrations.AlterModelOptions(
name="teamsettings",
options={
"ordering": ["name"],
"permissions": [("manage_settings", "Can manage settings")],
"verbose_name_plural": "Team settings",
},
),
]
1 change: 1 addition & 0 deletions app/apps/planner/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def fetch_tags(self, auth_header=None):
return response.json().get("results", [])

class Meta:
permissions = [("manage_settings", "Can manage settings")]
verbose_name_plural = "Team settings"
ordering = ["name"]

Expand Down
53 changes: 51 additions & 2 deletions app/apps/planner/tests/tests_api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from apps.planner.models import DaySettings, TeamSettings
from django.contrib.auth.models import Permission
from django.urls import reverse
from model_bakery import baker
from rest_framework import status
from rest_framework.test import APITestCase

from app.utils.unittest_helpers import (
get_authenticated_client,
get_test_user,
get_unauthenticated_client,
)

Expand Down Expand Up @@ -53,6 +55,22 @@ def test_authenticated_requests_two_team_settings(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.json().get("results")), 2)

def test_authenticated_post_requires_manage_settings_permission(self):
client = get_authenticated_client()

response = client.post(self.get_url(), {"name": "Team settings"})

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_authenticated_post_with_manage_settings_permission(self):
user = get_test_user()
user.user_permissions.add(Permission.objects.get(codename="manage_settings"))
client = get_authenticated_client()

response = client.post(self.get_url(), {"name": "Team settings"})

self.assertEqual(response.status_code, status.HTTP_201_CREATED)


class DaySettingsViewSet(APITestCase):
"""
Expand Down Expand Up @@ -125,6 +143,17 @@ def test_authenticated_requests_update_day_settings(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json().get("name"), DAY_SETTINGS_NAME)

def test_authenticated_post_requires_manage_settings_permission(self):
team_settings = baker.make(TeamSettings)
client = get_authenticated_client()

response = client.post(
self.get_url(),
{"name": "Day settings", "team_settings": team_settings.id},
)

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class DaySettingsUpdateTestViewSet(APITestCase):
"""
Expand Down Expand Up @@ -179,5 +208,25 @@ def test_authenticated_update_empty_payload(self):

client = get_authenticated_client()
response = client.put(url, {})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# self.assertEqual(response.json().get("name"), DAY_SETTINGS_NAME)

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_authenticated_put_with_manage_settings_permission(self):
day_settings_id = 1
team_settings = baker.make(TeamSettings)
baker.make(
DaySettings,
team_settings=team_settings,
id=day_settings_id,
name="FOO_NAME",
)

user = get_test_user()
user.user_permissions.add(Permission.objects.get(codename="manage_settings"))

url = reverse("v1:day-settings-detail", kwargs={"pk": day_settings_id})
client = get_authenticated_client()
response = client.put(url, {"name": "UPDATED_NAME"})

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json().get("name"), "UPDATED_NAME")
3 changes: 3 additions & 0 deletions app/apps/planner/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
TeamSettingsSerializer,
TeamSettingsThemeSerializer,
)
from apps.users.permissions import CanManageSettingsOrReadOnly
from apps.users.utils import get_auth_header_from_request
from django.conf import settings
from django.contrib.auth.decorators import user_passes_test
Expand All @@ -38,6 +39,7 @@ class TeamSettingsViewSet(ModelViewSet):

serializer_class = TeamSettingsSerializer
queryset = TeamSettings.objects.filter(enabled=True)
permission_classes = [CanManageSettingsOrReadOnly]

@extend_schema(
description="Gets the reasons associated with the requested team",
Expand Down Expand Up @@ -204,6 +206,7 @@ class DaySettingsViewSet(ModelViewSet):

serializer_class = DaySettingsSerializer
queryset = DaySettings.objects.all()
permission_classes = [CanManageSettingsOrReadOnly]

def destroy(self, request, *args, **kwargs):
try:
Expand Down
12 changes: 11 additions & 1 deletion app/apps/users/permissions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from rest_framework.permissions import BasePermission
from rest_framework.permissions import SAFE_METHODS, BasePermission


class InAuthGroup(BasePermission):
Expand All @@ -10,3 +10,13 @@ class IsInAuthorizedRealm(InAuthGroup):
"""
Keep for backwards compatibility
"""


class CanManageSettingsOrReadOnly(InAuthGroup):
permission = "planner.manage_settings"

def has_permission(self, request, view):
if not super().has_permission(request, view):
return False
# Read access is allowed for all authenticated users, but require the manage_settings permission for write access
return request.method in SAFE_METHODS or request.user.has_perm(self.permission)
28 changes: 28 additions & 0 deletions app/apps/users/tests/tests_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from unittest.mock import Mock, patch

from django.contrib.auth.models import Group, Permission
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
Expand Down Expand Up @@ -107,3 +108,30 @@ def test_with_failing_authentication_code(self, mock_AuthenticationBackend):
response = client.post(url, {"code": "FOO-CODE"})

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)


class CurrentUserPermissionsViewTest(APITestCase):
def test_authenticated_requests_return_all_current_user_permissions(self):
url = reverse("v1:permissions")
user = get_test_user()
group = Group.objects.create(name="planners")
permission = Permission.objects.get(codename="view_user")
group.permissions.add(permission)
user.groups.add(group)

client = get_authenticated_client()
response = client.get(url)

expected_permission = (
f"{permission.content_type.app_label}.{permission.codename}"
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json(), {"permissions": [expected_permission]})

def test_unauthenticated_requests_are_rejected(self):
url = reverse("v1:permissions")
client = get_unauthenticated_client()

response = client.get(url)

self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
5 changes: 5 additions & 0 deletions app/apps/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ def get(self, request):
return Response({"is_authorized": is_authorized})


class CurrentUserPermissionsView(APIView):
def get(self, request):
return Response({"permissions": sorted(request.user.get_all_permissions())})


class OIDCAuthenticateSerializer(serializers.Serializer):
code = serializers.CharField(required=True)

Expand Down
7 changes: 6 additions & 1 deletion app/settings/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
from apps.planner import router as planner_router
from apps.planner.views import dumpdata
from apps.users import router as users_router
from apps.users.views import IsAuthorizedView, ObtainAuthTokenOIDC
from apps.users.views import (
CurrentUserPermissionsView,
IsAuthorizedView,
ObtainAuthTokenOIDC,
)
from apps.visits import router as visits_router
from django.conf import settings
from django.conf.urls.static import static
Expand Down Expand Up @@ -49,6 +53,7 @@ def get(self, request, *args, **kwargs):
name="oidc-authenticate",
),
path("is-authorized/", IsAuthorizedView.as_view(), name="is-authorized"),
path("permissions/", CurrentUserPermissionsView.as_view(), name="permissions"),
path("feedback/", FeedbackView.as_view(), name="feedback"),
]
)
Expand Down
Loading