diff --git a/greg/api/views/consent_type.py b/greg/api/views/consent_type.py index 88daf30123463b249f434e94976a2892e22cc12f..1484bf5ad4a212c3d3654425736d2b86a20a60ee 100644 --- a/greg/api/views/consent_type.py +++ b/greg/api/views/consent_type.py @@ -1,4 +1,4 @@ -from rest_framework import viewsets +from rest_framework import viewsets, permissions from greg.api.pagination import PrimaryKeyCursorPagination from greg.api.serializers.consent_type import ConsentTypeSerializer @@ -11,4 +11,5 @@ class ConsentTypeViewSet(viewsets.ModelViewSet): queryset = ConsentType.objects.all().order_by("id") serializer_class = ConsentTypeSerializer pagination_class = PrimaryKeyCursorPagination + permission_classes = (permissions.IsAdminUser,) lookup_field = "id" diff --git a/greg/api/views/organizational_unit.py b/greg/api/views/organizational_unit.py index c6d2236cd68f3318629a4595f4ee6d3f1755918e..2c655c177753e9d6b1e711b160d73c7728e889ca 100644 --- a/greg/api/views/organizational_unit.py +++ b/greg/api/views/organizational_unit.py @@ -1,4 +1,4 @@ -from rest_framework import viewsets +from rest_framework import viewsets, permissions from greg.api.pagination import PrimaryKeyCursorPagination from greg.api.serializers.organizational_unit import OrganizationalUnitSerializer @@ -11,4 +11,5 @@ class OrganizationalUnitViewSet(viewsets.ModelViewSet): queryset = OrganizationalUnit.objects.all().order_by("id") serializer_class = OrganizationalUnitSerializer pagination_class = PrimaryKeyCursorPagination + permission_classes = (permissions.IsAdminUser,) lookup_field = "id" diff --git a/greg/api/views/person.py b/greg/api/views/person.py index 3214b41504d1023b9160fc29283233424f70081a..8a9fb3f2b77cc1de30c2d49643838e3cf2cae373 100644 --- a/greg/api/views/person.py +++ b/greg/api/views/person.py @@ -1,7 +1,7 @@ from django.core.exceptions import ValidationError from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema, OpenApiParameter -from rest_framework import viewsets, status +from rest_framework import viewsets, status, permissions from rest_framework.response import Response from greg.api.filters import PersonFilter, RoleFilter, IdentityFilter @@ -20,6 +20,7 @@ class PersonViewSet(viewsets.ModelViewSet): queryset = Person.objects.all().order_by("id") serializer_class = PersonSerializer pagination_class = PrimaryKeyCursorPagination + permission_classes = (permissions.IsAdminUser,) lookup_field = "id" filter_backends = (filters.DjangoFilterBackend,) filterset_class = PersonFilter @@ -50,6 +51,7 @@ class RoleViewSet(viewsets.ModelViewSet): queryset = Role.objects.all().order_by("id") pagination_class = PrimaryKeyCursorPagination + permission_classes = (permissions.IsAdminUser,) filter_backends = (filters.DjangoFilterBackend,) filterset_class = RoleFilter lookup_field = "id" @@ -93,6 +95,7 @@ class IdentityViewSet(viewsets.ModelViewSet): queryset = Identity.objects.all().order_by("id") serializer_class = IdentitySerializer pagination_class = PrimaryKeyCursorPagination + permission_classes = (permissions.IsAdminUser,) filter_backends = (filters.DjangoFilterBackend,) filterset_class = IdentityFilter # This is set so that the id parameter in the path of the URL is used for looking up objects diff --git a/greg/api/views/role_type.py b/greg/api/views/role_type.py index cb39712515c5e87325dac0ae3551e3b5399af8ac..85f54f3004a8514b53ce7c9dcccd0e74601e39f7 100644 --- a/greg/api/views/role_type.py +++ b/greg/api/views/role_type.py @@ -1,4 +1,4 @@ -from rest_framework import viewsets +from rest_framework import viewsets, permissions from greg.api.pagination import PrimaryKeyCursorPagination from greg.api.serializers.role_type import RoleTypeSerializer @@ -11,4 +11,5 @@ class RoleTypeViewSet(viewsets.ModelViewSet): queryset = RoleType.objects.all().order_by("id") serializer_class = RoleTypeSerializer pagination_class = PrimaryKeyCursorPagination + permission_classes = (permissions.IsAdminUser,) lookup_field = "id" diff --git a/greg/api/views/sponsor.py b/greg/api/views/sponsor.py index 818fc302b610693f994111a4c1b6f70a3ff93440..8c7a337f6c1ecdd17fb49ee5631bf5efc94170b1 100644 --- a/greg/api/views/sponsor.py +++ b/greg/api/views/sponsor.py @@ -3,7 +3,7 @@ import logging from django.db.models import ProtectedError from django.core.exceptions import ValidationError from drf_spectacular.utils import extend_schema, OpenApiParameter -from rest_framework import mixins, status +from rest_framework import mixins, status, permissions from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet, ModelViewSet @@ -22,6 +22,7 @@ class SponsorViewSet(ModelViewSet): queryset = Sponsor.objects.all().order_by("id") serializer_class = SponsorSerializer pagination_class = PrimaryKeyCursorPagination + permission_classes = (permissions.IsAdminUser,) lookup_field = "id" def destroy(self, request, *args, **kwargs): @@ -52,6 +53,7 @@ class SponsorGuestsViewSet(mixins.ListModelMixin, GenericViewSet): queryset = Person.objects.all().order_by("id") serializer_class = PersonSerializer pagination_class = PrimaryKeyCursorPagination + permission_classes = (permissions.IsAdminUser,) lookup_field = "id" def get_queryset(self): @@ -74,6 +76,7 @@ class SponsorOrgunitLinkView( queryset = OrganizationalUnit.objects.all().order_by("id") serializer_class = OrganizationalUnitSerializer pagination_class = PrimaryKeyCursorPagination + permission_classes = (permissions.IsAdminUser,) # This is set so that the orgunit_id parameter in the path of the URL is used for looking up objects lookup_url_kwarg = "orgunit_id" diff --git a/greg/tests/api/test_authz.py b/greg/tests/api/test_authz.py new file mode 100644 index 0000000000000000000000000000000000000000..a2e00a2a638a47680db4f03614c858156c1ee51b --- /dev/null +++ b/greg/tests/api/test_authz.py @@ -0,0 +1,64 @@ +import pytest + +from rest_framework.reverse import reverse +from rest_framework.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN + + +ROUTES: tuple[tuple[str, dict], ...] = ( + ("v1:consenttype-list", {}), + ("v1:consenttype-detail", {"id": 1}), + ("v1:orgunit-list", {}), + ("v1:orgunit-detail", {"id": 1}), + ("v1:person-list", {}), + ("v1:person-detail", {"id": 1}), + ("v1:person_identity-list", {"person_id": 1}), + ("v1:person_identity-detail", {"person_id": 1, "id": 1}), + ("v1:person_role-list", {"person_id": 1}), + ("v1:person_role-detail", {"person_id": 1, "id": 1}), + ("v1:roletype-list", {}), + ("v1:roletype-detail", {"id": 1}), + ("v1:sponsor-list", {}), + ("v1:sponsor-detail", {"id": 1}), +) + + +def assert_returns_status(endpoint, query_kwargs, client, status): + url = reverse(endpoint, kwargs=query_kwargs) + get_response = client.get(url) + assert get_response.status_code == status + post_response = client.post(url, data={"foo": "bar"}) + assert post_response.status_code == status + patch_response = client.post(url, data={"foo": "bar"}) + assert patch_response.status_code == status + delete_response = client.get(url) + assert delete_response.status_code == status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ("endpoint", "query_kwargs"), + ROUTES, +) +def test_forbids_non_admin_users(endpoint, query_kwargs, non_admin_client): + # client authenticates with a token, but is not an administrator + assert_returns_status( + endpoint=endpoint, + query_kwargs=query_kwargs, + client=non_admin_client, + status=HTTP_403_FORBIDDEN, + ) + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ("endpoint", "query_kwargs"), + ROUTES, +) +def test_requires_authentication(endpoint, query_kwargs, unauthenticated_client): + # client does not authenticate + assert_returns_status( + endpoint=endpoint, + query_kwargs=query_kwargs, + client=unauthenticated_client, + status=HTTP_401_UNAUTHORIZED, + ) diff --git a/greg/tests/conftest.py b/greg/tests/conftest.py index 7b6acedc3680b78217467923cae30bed61fa769a..abeafcdf3c7e88bc7d38fc45f5be11fe1b22d3be 100644 --- a/greg/tests/conftest.py +++ b/greg/tests/conftest.py @@ -29,6 +29,15 @@ logging.getLogger("faker").setLevel(logging.ERROR) @pytest.fixture def client() -> APIClient: + user, _ = get_user_model().objects.get_or_create(username="test", is_staff=True) + token, _ = Token.objects.get_or_create(user=user) + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"Token {token.key}") + return client + + +@pytest.fixture +def non_admin_client() -> APIClient: user, _ = get_user_model().objects.get_or_create(username="test") token, _ = Token.objects.get_or_create(user=user) client = APIClient() @@ -36,6 +45,12 @@ def client() -> APIClient: return client +@pytest.fixture +def unauthenticated_client() -> APIClient: + client = APIClient() + return client + + @pytest.fixture def pcm_mock(): class PCMMock: diff --git a/gregsite/settings/base.py b/gregsite/settings/base.py index 86267c22ad5cbeea9ae96a13b5b15d83ee410a7d..d916a29e4acb20fae15b24d459e2e44877831ec9 100644 --- a/gregsite/settings/base.py +++ b/gregsite/settings/base.py @@ -89,7 +89,7 @@ REST_FRAMEWORK = { "rest_framework.authentication.TokenAuthentication", "rest_framework.authentication.SessionAuthentication", ), - "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAdminUser",), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", # Rate limit settings of invite endpoint "DEFAULT_THROTTLE_CLASSES": [