Browse Source

Add scoped permissions to issues views

David Burke 4 years ago
parent
commit
56e81c8ccb

+ 18 - 0
issues/permissions.py

@@ -0,0 +1,18 @@
+from glitchtip.permissions import ScopedPermission
+
+
+class IssuePermission(ScopedPermission):
+    scope_map = {
+        "GET": ["event:read", "event:write", "event:admin"],
+        "POST": ["event:write", "event:admin"],
+        "PUT": ["event:write", "event:admin"],
+        "DELETE": ["event:admin"],
+    }
+
+    def get_user_scopes(self, obj, user):
+        return obj.project.organization.get_user_scopes(user)
+
+
+class EventPermission(IssuePermission):
+    def get_user_scopes(self, obj, user):
+        return obj.issue.project.organization.get_user_scopes(user)

+ 143 - 0
issues/tests/test_api_permissions.py

@@ -0,0 +1,143 @@
+from django.urls import reverse
+from model_bakery import baker
+from organizations_ext.models import OrganizationUserRole
+from glitchtip.test_utils.test_case import APIPermissionTestCase
+
+
+class IssueAPIPermissionTests(APIPermissionTestCase):
+    def setUp(self):
+        self.create_user_org()
+        self.set_client_credentials(self.auth_token.token)
+        self.team = baker.make("teams.Team", organization=self.organization)
+        self.team.members.add(self.org_user)
+        self.project = baker.make("projects.Project", organization=self.organization)
+        self.project.team_set.add(self.team)
+        self.issue = baker.make("issues.Issue", project=self.project)
+        self.list_url = reverse("issue-list")
+        self.organization_list_url = reverse(
+            "organization-issues-list",
+            kwargs={"organization_slug": self.organization.slug},
+        )
+        self.project_list_url = reverse(
+            "project-issues-list",
+            kwargs={"project_pk": self.organization.slug + "/" + self.project.slug},
+        )
+        self.detail_url = reverse("issue-detail", kwargs={"pk": self.issue.pk})
+        self.organization_detail_url = reverse(
+            "organization-issues-detail",
+            kwargs={"organization_slug": self.organization.slug, "pk": self.issue.pk},
+        )
+        self.project_detail_url = reverse(
+            "project-issues-detail",
+            kwargs={
+                "project_pk": self.organization.slug + "/" + self.project.slug,
+                "pk": self.issue.pk,
+            },
+        )
+
+    def test_list(self):
+        self.assertGetReqStatusCode(self.list_url, 403)
+        self.assertGetReqStatusCode(self.organization_list_url, 403)
+        self.assertGetReqStatusCode(self.project_list_url, 403)
+        self.auth_token.add_permission("event:read")
+        self.assertGetReqStatusCode(self.list_url, 200)
+        self.assertGetReqStatusCode(self.project_list_url, 200)
+
+    def test_retrieve(self):
+        self.assertGetReqStatusCode(self.detail_url, 403)
+        self.assertGetReqStatusCode(self.organization_detail_url, 403)
+        self.assertGetReqStatusCode(self.project_detail_url, 403)
+
+        self.auth_token.add_permission("event:read")
+        self.assertGetReqStatusCode(self.detail_url, 200)
+        self.assertGetReqStatusCode(self.organization_detail_url, 200)
+        self.assertGetReqStatusCode(self.project_detail_url, 200)
+
+    def test_create(self):
+        data = {"not": "supported"}
+        self.auth_token.add_permission("event:admin")
+        self.assertPostReqStatusCode(self.list_url, data, 405)
+
+    def test_destroy(self):
+        self.auth_token.add_permissions(["event:read", "event:write"])
+        self.assertDeleteReqStatusCode(self.detail_url, 403)
+
+        self.auth_token.add_permission("event:admin")
+        self.assertDeleteReqStatusCode(self.detail_url, 204)
+
+    def test_user_destroy(self):
+        self.client.force_login(self.user)
+        self.set_user_role(OrganizationUserRole.MEMBER)
+        self.assertDeleteReqStatusCode(self.detail_url, 204)
+
+    def test_update(self):
+        self.auth_token.add_permission("event:read")
+        data = {"status": "resolved"}
+        self.assertPutReqStatusCode(self.detail_url, data, 403)
+        self.assertPutReqStatusCode(self.organization_detail_url, data, 403)
+        self.assertPutReqStatusCode(self.project_detail_url, data, 403)
+
+        self.auth_token.add_permission("event:write")
+        self.assertPutReqStatusCode(self.detail_url, data, 200)
+        self.assertPutReqStatusCode(self.organization_detail_url, data, 200)
+        self.assertPutReqStatusCode(self.project_detail_url, data, 200)
+
+
+class EventAPIPermissionTests(APIPermissionTestCase):
+    def setUp(self):
+        self.create_user_org()
+        self.set_client_credentials(self.auth_token.token)
+        self.team = baker.make("teams.Team", organization=self.organization)
+        self.team.members.add(self.org_user)
+        self.project = baker.make("projects.Project", organization=self.organization)
+        self.project.team_set.add(self.team)
+        self.event = baker.make("issues.Event", issue__project=self.project)
+        self.list_url = reverse(
+            "issue-events-list", kwargs={"issue_pk": self.event.issue.pk}
+        )
+        self.project_list_url = reverse(
+            "project-events-list",
+            kwargs={"project_pk": self.organization.slug + "/" + self.project.slug},
+        )
+        self.detail_url = reverse(
+            "issue-events-detail",
+            kwargs={"issue_pk": self.event.issue.pk, "pk": self.event.pk},
+        )
+        self.project_detail_url = reverse(
+            "project-events-detail",
+            kwargs={
+                "project_pk": self.organization.slug + "/" + self.project.slug,
+                "pk": self.event.pk,
+            },
+        )
+        self.latest_detail_url = self.list_url + "latest/"
+
+    def test_list(self):
+        self.assertGetReqStatusCode(self.list_url, 403)
+        self.assertGetReqStatusCode(self.project_list_url, 403)
+        self.auth_token.add_permission("event:read")
+        self.assertGetReqStatusCode(self.list_url, 200)
+        self.assertGetReqStatusCode(self.project_list_url, 200)
+
+    def test_retrieve(self):
+        self.assertGetReqStatusCode(self.detail_url, 403)
+        self.assertGetReqStatusCode(self.project_detail_url, 403)
+        self.assertGetReqStatusCode(self.latest_detail_url, 403)
+
+        self.auth_token.add_permission("event:read")
+        self.assertGetReqStatusCode(self.detail_url, 200)
+        self.assertGetReqStatusCode(self.project_detail_url, 200)
+        self.assertGetReqStatusCode(self.latest_detail_url, 200)
+
+    def test_event_json_view(self):
+        url = reverse(
+            "event_json",
+            kwargs={
+                "org": self.organization.slug,
+                "issue": self.event.issue.pk,
+                "event": self.event.pk,
+            },
+        )
+        self.assertGetReqStatusCode(url, 403)
+        self.auth_token.add_permission("event:read")
+        self.assertGetReqStatusCode(url, 200)

+ 2 - 2
issues/tests/test_user_reports.py

@@ -30,7 +30,7 @@ class IssuesUserReportTestCase(GlitchTipTestCase):
 
     def test_issues_list_user_report_count(self):
         url = reverse("issue-detail", kwargs={"pk": self.event.issue.pk})
-        with self.assertNumQueries(4):
+        with self.assertNumQueries(6):
             res = self.client.get(url)
         self.assertEqual(res.data["userReportCount"], 1)
 
@@ -43,7 +43,7 @@ class IssuesUserReportTestCase(GlitchTipTestCase):
             event_id=event2.pk.hex,
         )
         url = reverse(
-            "user-reports-issues-list", kwargs={"issue_pk": self.event.issue.pk}
+            "issue-user-reports-list", kwargs={"issue_pk": self.event.issue.pk}
         )
         res = self.client.get(url)
         self.assertContains(res, self.user_report.email)

+ 1 - 1
issues/tests/tests.py

@@ -45,7 +45,7 @@ class EventTestCase(GlitchTipTestCase):
         issue2_event1 = baker.make("issues.Event", issue=issue2)
         issue1_event2 = baker.make("issues.Event", issue=issue1)
 
-        url = reverse("event-issues-latest", args=[issue1.id])
+        url = reverse("issue-events-latest", args=[issue1.id])
         res = self.client.get(url)
         self.assertContains(res, issue1_event2.pk.hex)
         self.assertEqual(res.data["previousEventID"], issue1_event1.pk.hex)

+ 2 - 2
issues/urls.py

@@ -8,9 +8,9 @@ router = BulkSimpleRouter()
 router.register(r"issues", IssueViewSet)
 
 issues_router = routers.NestedSimpleRouter(router, r"issues", lookup="issue")
-issues_router.register(r"events", EventViewSet, basename="event-issues")
+issues_router.register(r"events", EventViewSet, basename="issue-events")
 issues_router.register(
-    r"user-reports", UserReportViewSet, basename="user-reports-issues"
+    r"user-reports", UserReportViewSet, basename="issue-user-reports"
 )
 
 # The API URLs are now determined automatically by the router.

+ 13 - 2
issues/views.py

@@ -1,5 +1,5 @@
 from django.shortcuts import get_object_or_404
-from rest_framework import viewsets, views
+from rest_framework import viewsets, views, mixins
 from rest_framework.decorators import action
 from rest_framework.response import Response
 from .models import Issue, Event, EventStatus
@@ -9,9 +9,16 @@ from .serializers import (
     EventDetailSerializer,
 )
 from .filters import IssueFilter
+from .permissions import IssuePermission, EventPermission
 
 
-class IssueViewSet(viewsets.ModelViewSet):
+class IssueViewSet(
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
     """
     View and bulk update issues.
 
@@ -28,6 +35,7 @@ class IssueViewSet(viewsets.ModelViewSet):
     queryset = Issue.objects.all()
     serializer_class = IssueSerializer
     filterset_class = IssueFilter
+    permission_classes = [IssuePermission]
 
     def get_queryset(self):
         if not self.request.user.is_authenticated:
@@ -82,6 +90,7 @@ class IssueViewSet(viewsets.ModelViewSet):
 class EventViewSet(viewsets.ReadOnlyModelViewSet):
     queryset = Event.objects.all()
     serializer_class = EventSerializer
+    permission_classes = [EventPermission]
 
     def get_serializer_class(self):
         if self.action in ["retrieve", "latest"]:
@@ -121,6 +130,8 @@ class EventJsonView(views.APIView):
     Exists mainly for Sentry API compatibility
     """
 
+    permission_classes = [EventPermission]
+
     def get(self, request, org, issue, event, format=None):
         event = get_object_or_404(
             Event,