Browse Source

feat(dashboards): Add django permissions class to dashboard details endpoint (#79288)

<!-- Describe your PR here. -->
#78550

The permissions class will protect dashboard details endpoints when dashboard edit access is restricted.
---------

Co-authored-by: harshithadurai <harshi.durai@esentry.io>
Harshitha Durai 4 months ago
parent
commit
6a8ff4c61b

+ 25 - 1
src/sentry/api/endpoints/organization_dashboard_details.py

@@ -3,6 +3,7 @@ from django.db import IntegrityError, router, transaction
 from django.db.models import F
 from django.utils import timezone
 from drf_spectacular.utils import extend_schema
+from rest_framework.permissions import BasePermission
 from rest_framework.request import Request
 from rest_framework.response import Response
 
@@ -30,9 +31,32 @@ EDIT_FEATURE = "organizations:dashboards-edit"
 READ_FEATURE = "organizations:dashboards-basic"
 
 
+class DashboardPermissions(BasePermission):
+    """
+    Django Permissions Class for managing Dashboard Edit
+    permissions defined in the DashboardPermissions Model
+    """
+
+    scope_map = {
+        "GET": ["org:read", "org:write", "org:admin"],
+        "POST": ["org:read", "org:write", "org:admin"],
+        "PUT": ["org:read", "org:write", "org:admin"],
+        "DELETE": ["org:read", "org:write", "org:admin"],
+    }
+
+    def has_object_permission(self, request: Request, view, obj):
+        if isinstance(obj, Dashboard) and features.has(
+            "organizations:dashboards-edit-access", obj.organization, actor=request.user
+        ):
+            # Check if user has permissions to edit dashboard
+            if hasattr(obj, "permissions"):
+                return obj.permissions.has_edit_permissions(request.user.id)
+        return True
+
+
 class OrganizationDashboardBase(OrganizationEndpoint):
     owner = ApiOwner.PERFORMANCE
-    permission_classes = (OrganizationDashboardsPermission,)
+    permission_classes = (OrganizationDashboardsPermission, DashboardPermissions)
 
     def convert_args(
         self, request: Request, organization_id_or_slug, dashboard_id, *args, **kwargs

+ 5 - 0
src/sentry/models/dashboard_permissions.py

@@ -20,6 +20,11 @@ class DashboardPermissions(Model):
         "sentry.Dashboard", on_delete=models.CASCADE, related_name="permissions"
     )
 
+    def has_edit_permissions(self, userId):
+        if not self.is_creator_only_editable:
+            return True
+        return userId == self.dashboard.created_by_id
+
     class Meta:
         app_label = "sentry"
         db_table = "sentry_dashboardpermissions"

+ 112 - 0
tests/sentry/api/endpoints/test_organization_dashboard_details.py

@@ -375,6 +375,22 @@ class OrganizationDashboardDetailsGetTest(OrganizationDashboardDetailsTestCase):
         assert "permissions" in response.data
         assert not response.data["permissions"]
 
+    def test_dashboard_viewable_with_no_edit_permissions(self):
+        dashboard = Dashboard.objects.create(
+            title="Dashboard With Dataset Source",
+            created_by_id=1142,
+            organization=self.organization,
+        )
+        DashboardPermissions.objects.create(is_creator_only_editable=True, dashboard=dashboard)
+
+        user = self.create_user(id=1289)
+        self.create_member(user=user, organization=self.organization)
+        self.login_as(user)
+
+        with self.feature({"organizations:dashboards-edit-access": True}):
+            response = self.do_request("get", self.url(dashboard.id))
+        assert response.status_code == 200, response.content
+
 
 class OrganizationDashboardDetailsDeleteTest(OrganizationDashboardDetailsTestCase):
     def test_delete(self):
@@ -505,6 +521,54 @@ class OrganizationDashboardDetailsDeleteTest(OrganizationDashboardDetailsTestCas
             response = self.do_request("delete", self.url("default-overview"))
             assert response.status_code == 404
 
+    def test_delete_dashboard_with_edit_permissions_not_granted(self):
+        dashboard = Dashboard.objects.create(
+            title="Dashboard With Dataset Source",
+            created_by_id=11452,
+            organization=self.organization,
+        )
+        DashboardPermissions.objects.create(is_creator_only_editable=True, dashboard=dashboard)
+
+        user = self.create_user(id=1235)
+        self.create_member(user=user, organization=self.organization)
+        self.login_as(user)
+
+        with self.feature({"organizations:dashboards-edit-access": True}):
+            response = self.do_request("delete", self.url(dashboard.id))
+        assert response.status_code == 403
+
+    def test_delete_dashboard_with_edit_permissions_disabled(self):
+        dashboard = Dashboard.objects.create(
+            title="Dashboard With Dataset Source",
+            created_by_id=11452,
+            organization=self.organization,
+        )
+        DashboardPermissions.objects.create(is_creator_only_editable=False, dashboard=dashboard)
+
+        user = self.create_user(id=1235)
+        self.create_member(user=user, organization=self.organization)
+        self.login_as(user)
+
+        with self.feature({"organizations:dashboards-edit-access": True}):
+            response = self.do_request("delete", self.url(dashboard.id))
+        assert response.status_code == 204
+
+    def test_delete_dashboard_with_edit_permissions_granted(self):
+        dashboard = Dashboard.objects.create(
+            title="Dashboard With Dataset Source",
+            created_by_id=12333,
+            organization=self.organization,
+        )
+        DashboardPermissions.objects.create(is_creator_only_editable=True, dashboard=dashboard)
+
+        user = self.create_user(id=12333)
+        self.create_member(user=user, organization=self.organization)
+        self.login_as(user)
+
+        with self.feature({"organizations:dashboards-edit-access": True}):
+            response = self.do_request("delete", self.url(dashboard.id))
+        assert response.status_code == 204, response.content
+
 
 class OrganizationDashboardDetailsPutTest(OrganizationDashboardDetailsTestCase):
     def setUp(self):
@@ -1809,6 +1873,54 @@ class OrganizationDashboardDetailsPutTest(OrganizationDashboardDetailsTestCase):
         )
         assert response.status_code == 200, response.data
 
+    def test_edit_dashboard_with_edit_permissions_not_granted(self):
+        dashboard = Dashboard.objects.create(
+            title="Dashboard With Dataset Source",
+            created_by_id=12333,
+            organization=self.organization,
+        )
+        DashboardPermissions.objects.create(is_creator_only_editable=True, dashboard=dashboard)
+
+        user = self.create_user(id=3456)
+        self.create_member(user=user, organization=self.organization)
+        self.login_as(user)
+
+        with self.feature({"organizations:dashboards-edit-access": True}):
+            response = self.do_request("put", self.url(dashboard.id))
+        assert response.status_code == 403
+
+    def test_edit_dashboard_with_edit_permissions_disabled(self):
+        dashboard = Dashboard.objects.create(
+            title="Dashboard With Dataset Source",
+            created_by_id=12333,
+            organization=self.organization,
+        )
+        DashboardPermissions.objects.create(is_creator_only_editable=False, dashboard=dashboard)
+
+        user = self.create_user(id=3456)
+        self.create_member(user=user, organization=self.organization)
+        self.login_as(user)
+
+        with self.feature({"organizations:dashboards-edit-access": True}):
+            response = self.do_request("put", self.url(dashboard.id))
+        assert response.status_code == 200
+
+    def test_edit_dashboard_with_edit_permissions_granted(self):
+        dashboard = Dashboard.objects.create(
+            title="Dashboard With Dataset Source",
+            created_by_id=12333,
+            organization=self.organization,
+        )
+        DashboardPermissions.objects.create(is_creator_only_editable=True, dashboard=dashboard)
+
+        user = self.create_user(id=12333)
+        self.create_member(user=user, organization=self.organization)
+        self.login_as(user)
+
+        with self.feature({"organizations:dashboards-edit-access": True}):
+            response = self.do_request("put", self.url(self.dashboard.id))
+        assert response.status_code == 200, response.content
+
 
 class OrganizationDashboardDetailsOnDemandTest(OrganizationDashboardDetailsTestCase):
     widget_type = DashboardWidgetTypes.DISCOVER