From 4e6daea4278dd1db6027829279b5a44311051477 Mon Sep 17 00:00:00 2001 From: Shashank Jarmale Date: Thu, 17 Sep 2026 14:39:01 -0700 Subject: [PATCH] Require active superuser access to template debugger --- .../internal_registered_templates.py | 4 +- .../test_internal_registered_templates.py | 53 ++++++++++++++++--- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/sentry/notifications/platform/api/endpoints/internal_registered_templates.py b/src/sentry/notifications/platform/api/endpoints/internal_registered_templates.py index 98a1428a8943..c461787cbae7 100644 --- a/src/sentry/notifications/platform/api/endpoints/internal_registered_templates.py +++ b/src/sentry/notifications/platform/api/endpoints/internal_registered_templates.py @@ -7,7 +7,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint -from sentry.api.permissions import SentryIsAuthenticated +from sentry.api.permissions import SuperuserPermission from sentry.notifications.platform.discord.provider import DiscordRenderable, DiscordRenderer from sentry.notifications.platform.email.provider import EmailRenderer from sentry.notifications.platform.msteams.provider import MSTeamsRenderable, MSTeamsRenderer @@ -27,7 +27,7 @@ @control_silo_endpoint class InternalRegisteredTemplatesEndpoint(Endpoint): owner = ApiOwner.NOTIFICATIONS - permission_classes = (SentryIsAuthenticated,) + permission_classes = (SuperuserPermission,) publish_status = {"GET": ApiPublishStatus.PRIVATE} def get(self, request: Request) -> Response: diff --git a/tests/sentry/notifications/platform/api/endpoints/test_internal_registered_templates.py b/tests/sentry/notifications/platform/api/endpoints/test_internal_registered_templates.py index 19f33e687fc6..c260d568d111 100644 --- a/tests/sentry/notifications/platform/api/endpoints/test_internal_registered_templates.py +++ b/tests/sentry/notifications/platform/api/endpoints/test_internal_registered_templates.py @@ -1,4 +1,5 @@ from typing import Any +from unittest import mock from sentry.notifications.platform.api.endpoints.internal_registered_templates import ( serialize_slack_preview, @@ -18,8 +19,35 @@ def test_unauthenticated(self) -> None: response = self.get_response() assert response.status_code == 401 + def test_non_superuser(self) -> None: + user = self.create_user(is_staff=False, is_superuser=False) + self.login_as(user) + + with mock.patch( + "sentry.notifications.platform.api.endpoints.internal_registered_templates.serialize_template", + wraps=serialize_template, + ) as serialize: + response = self.get_response() + + assert response.status_code == 403 + serialize.assert_not_called() + + def test_staff_without_superuser(self) -> None: + user = self.create_user(is_staff=True, is_superuser=False) + self.login_as(user, staff=True) + + response = self.get_response() + assert response.status_code == 403 + + def test_superuser_without_elevation(self) -> None: + user = self.create_user(is_staff=False, is_superuser=True) + self.login_as(user, superuser=False) + + response = self.get_response() + assert response.status_code == 403 + def test_get_all_registered_templates(self) -> None: - self.login_as(self.user) + self.login_as(self.user, superuser=True) response = self.get_response() assert response.status_code == 200 for source, template_cls in template_registry.registrations.items(): @@ -33,8 +61,8 @@ def test_get_all_registered_templates(self) -> None: ) def test_valid_template_serialization(self) -> None: - self.login_as(self.user) - response = self.get_response() + self.login_as(self.user, superuser=True) + response = self.get_success_response() for templates_by_category in response.data.values(): for template in templates_by_category: assert "source" in template @@ -50,8 +78,8 @@ def test_valid_template_serialization(self) -> None: assert "slack" in template["previews"] def test_email_preview(self) -> None: - self.login_as(self.user) - response = self.get_response() + self.login_as(self.user, superuser=True) + response = self.get_success_response() for templates_by_category in response.data.values(): for template in templates_by_category: assert "email" in template["previews"] @@ -60,8 +88,8 @@ def test_email_preview(self) -> None: assert isinstance(template["previews"]["email"]["html_content"], str) def test_discord_preview(self) -> None: - self.login_as(self.user) - response = self.get_response() + self.login_as(self.user, superuser=True) + response = self.get_success_response() for templates_by_category in response.data.values(): for template in templates_by_category: assert "discord" in template["previews"] @@ -75,6 +103,17 @@ def test_discord_preview(self) -> None: assert template["previews"]["discord"]["content"] == "" assert len(template["previews"]["discord"]["embeds"]) == 1 + def test_hidden_template_is_excluded(self) -> None: + self.login_as(self.user, superuser=True) + + with mock.patch.object(MockNotificationTemplate, "hide_from_debugger", True): + response = self.get_success_response() + + sources = { + template["source"] for templates in response.data.values() for template in templates + } + assert MockNotificationTemplate.example_data.source not in sources + def find_block_by_type(blocks: list[dict[str, Any]], block_type: str) -> dict[str, Any] | None: """Find the first block with the specified type."""