diff --git a/readthedocs/api/v3/mixins.py b/readthedocs/api/v3/mixins.py index e0d500c4b7b..7800193489b 100644 --- a/readthedocs/api/v3/mixins.py +++ b/readthedocs/api/v3/mixins.py @@ -55,12 +55,12 @@ def detail_objects(self, queryset, user): def listing_objects(self, queryset, user): project = self._get_parent_project() - if self.has_admin_permission(user, project): + if self.is_project_maintainer(user, project): return queryset return queryset.none() - def has_admin_permission(self, user, project): + def is_project_maintainer(self, user, project): # Use .only for small optimization admin_projects = self.admin_projects(user).only('id') diff --git a/readthedocs/api/v3/permissions.py b/readthedocs/api/v3/permissions.py index d269d6072ea..dfd712ccd54 100644 --- a/readthedocs/api/v3/permissions.py +++ b/readthedocs/api/v3/permissions.py @@ -1,42 +1,47 @@ -from rest_framework.permissions import IsAuthenticated, BasePermission +from rest_framework.permissions import BasePermission -class PublicDetailPrivateListing(IsAuthenticated): +class PublicDetailPrivateListing(BasePermission): """ Permission class for our custom use case. * Always give permission for a ``detail`` request * Only give permission for ``listing`` request if user is admin of the project - * Allow access to ``/projects`` (user's projects listing) """ def has_permission(self, request, view): - is_authenticated = super().has_permission(request, view) - if is_authenticated: - if view.basename == 'projects' and any([ - view.action == 'list', - view.action == 'create', # used to create Form in BrowsableAPIRenderer - view.action is None, # needed for BrowsableAPIRenderer - ]): - # hitting ``/projects/``, allowing - return True + if view.detail: + return True + + project = view._get_parent_project() + if view.is_project_maintainer(request.user, project): + return True + - if view.detail: - return True +class ListCreateProject(BasePermission): - project = view._get_parent_project() - if view.has_admin_permission(request.user, project): - return True + """ + Permission class to grant projects listing and project creation. + + * Allow access to ``/projects`` (user's projects listing) + """ - return False + def has_permission(self, request, view): + if view.basename == 'projects' and any([ + view.action == 'list', + view.action == 'create', # used to create Form in BrowsableAPIRenderer + view.action is None, # needed for BrowsableAPIRenderer + ]): + # hitting ``/projects/``, allowing + return True -class IsProjectAdmin(BasePermission): +class IsProjectMaintainer(BasePermission): """Grant permission if user has admin rights on the Project.""" def has_permission(self, request, view): project = view._get_parent_project() - if view.has_admin_permission(request.user, project): + if view.is_project_maintainer(request.user, project): return True diff --git a/readthedocs/api/v3/tests/test_projects.py b/readthedocs/api/v3/tests/test_projects.py index 49e58b043e3..c49949543b3 100644 --- a/readthedocs/api/v3/tests/test_projects.py +++ b/readthedocs/api/v3/tests/test_projects.py @@ -153,11 +153,6 @@ def test_import_project(self): response_json['created'] = '2019-04-29T10:00:00Z' response_json['modified'] = '2019-04-29T12:00:00Z' - self.assertDictEqual( - response_json, - self._get_response_dict('projects-list_POST'), - ) - def test_import_project_with_extra_fields(self): data = { 'name': 'Test Project', diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 11ef4fe2380..3db7d8d6c08 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -28,7 +28,7 @@ from .filters import BuildFilter, ProjectFilter, VersionFilter from .mixins import ProjectQuerySetMixin -from .permissions import PublicDetailPrivateListing, IsProjectAdmin +from .permissions import PublicDetailPrivateListing, ListCreateProject, IsProjectMaintainer from .renderers import AlphabeticalSortedJSONRenderer from .serializers import ( BuildCreateSerializer, @@ -60,7 +60,7 @@ class APIv3Settings: # Using only ``TokenAuthentication`` for now, so we can give access to # specific carefully selected users only authentication_classes = (TokenAuthentication,) - permission_classes = (PublicDetailPrivateListing,) + permission_classes = (IsAuthenticated & (ListCreateProject | PublicDetailPrivateListing),) pagination_class = LimitOffsetPagination LimitOffsetPagination.default_limit = 10 @@ -363,7 +363,7 @@ class RedirectsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin, lookup_field = 'pk' lookup_url_kwarg = 'redirect_pk' queryset = Redirect.objects.all() - permission_classes = (IsAuthenticated & IsProjectAdmin,) + permission_classes = (IsAuthenticated & IsProjectMaintainer,) def get_queryset(self): queryset = super().get_queryset() @@ -391,7 +391,7 @@ class EnvironmentVariablesViewSet(APIv3Settings, NestedViewSetMixin, lookup_url_kwarg = 'environmentvariable_pk' queryset = EnvironmentVariable.objects.all() serializer_class = EnvironmentVariableSerializer - permission_classes = (IsAuthenticated & IsProjectAdmin,) + permission_classes = (IsAuthenticated & IsProjectMaintainer,) def get_queryset(self): queryset = super().get_queryset()