From 70fc0532e82af9745f36f97ccde8e839b5a59cbf Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 23 Oct 2025 17:05:22 +0200 Subject: [PATCH 01/14] feat(symsorter): Add platform-restricted builtin symbol sources with org access control Adds support for restricting builtin symbol sources to specific platforms with organization-level access control via enabledConsolePlatforms. ## Changes - Add `platforms` key to builtin source definitions in server.py to restrict sources to specific platforms (e.g., nintendo-switch) - Update builtin_symbol_sources endpoint to filter sources based on: 1. Platform match: request platform must match source's platforms list 2. Org access: organization must have console platform enabled - Pass project platform from frontend to API for filtering - Add nintendo-private S3 source with credentials from environment variables - Add platform mapping for nintendo-switch in DEFAULT_SYMBOL_SOURCES ## Implementation Details Sources without a `platforms` key remain visible to all platforms (backward compatible). Sources with `platforms` are only visible when: - The request includes a matching platform parameter AND - The organization has that console platform in enabledConsolePlatforms This ensures platform-restricted sources (like Nintendo's private symbol server) are only accessible to authorized organizations with matching project platforms. --- .../api/endpoints/builtin_symbol_sources.py | 57 +++++++++- .../api/helpers/default_symbol_sources.py | 1 + src/sentry/conf/server.py | 13 +++ .../settings/projectDebugFiles/index.tsx | 19 +++- .../endpoints/test_builtin_symbol_sources.py | 102 ++++++++++++++++++ 5 files changed, 185 insertions(+), 7 deletions(-) diff --git a/src/sentry/api/endpoints/builtin_symbol_sources.py b/src/sentry/api/endpoints/builtin_symbol_sources.py index af34b014c4632f..38da5382851465 100644 --- a/src/sentry/api/endpoints/builtin_symbol_sources.py +++ b/src/sentry/api/endpoints/builtin_symbol_sources.py @@ -6,6 +6,8 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.serializers import serialize +from sentry.constants import ENABLED_CONSOLE_PLATFORMS_DEFAULT +from sentry.models.organization import Organization def normalize_symbol_source(key, source): @@ -17,6 +19,25 @@ def normalize_symbol_source(key, source): } +def organization_has_console_platform_access( + organization: Organization, platform: str +) -> bool: + """ + Check if an organization has access to a specific console platform. + + Args: + organization: The organization to check + platform: The console platform (e.g., 'nintendo-switch', 'playstation', 'xbox') + + Returns: + True if the organization has access to the console platform, False otherwise + """ + enabled_console_platforms = organization.get_option( + "sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT + ) + return platform in enabled_console_platforms + + @region_silo_endpoint class BuiltinSymbolSourcesEndpoint(Endpoint): owner = ApiOwner.OWNERS_INGEST @@ -26,10 +47,38 @@ class BuiltinSymbolSourcesEndpoint(Endpoint): permission_classes = () def get(self, request: Request, **kwargs) -> Response: - sources = [ - normalize_symbol_source(key, source) - for key, source in settings.SENTRY_BUILTIN_SOURCES.items() - ] + platform = request.GET.get("platform") + + # Get organization if organization context is available + organization = None + organization_id_or_slug = kwargs.get("organization_id_or_slug") + if organization_id_or_slug: + try: + if str(organization_id_or_slug).isdecimal(): + organization = Organization.objects.get_from_cache(id=organization_id_or_slug) + else: + organization = Organization.objects.get_from_cache( + slug=organization_id_or_slug + ) + except Organization.DoesNotExist: + pass + + sources = [] + for key, source in settings.SENTRY_BUILTIN_SOURCES.items(): + source_platforms = source.get("platforms") + + # If source has platform restrictions, check if current platform matches + if source_platforms is not None: + if not platform or platform not in source_platforms: + continue + + # Platform matches - now check if organization has access to this console platform + if not organization or not organization_has_console_platform_access( + organization, platform + ): + continue + + sources.append(normalize_symbol_source(key, source)) sources.sort(key=lambda s: s["name"]) return Response(serialize(sources)) diff --git a/src/sentry/api/helpers/default_symbol_sources.py b/src/sentry/api/helpers/default_symbol_sources.py index a0adf06a64083a..57a4e55130e2aa 100644 --- a/src/sentry/api/helpers/default_symbol_sources.py +++ b/src/sentry/api/helpers/default_symbol_sources.py @@ -7,6 +7,7 @@ "unity": ["ios", "microsoft", "android", "nuget", "unity", "nvidia", "ubuntu"], "unreal": ["ios", "microsoft", "android", "nvidia", "ubuntu"], "godot": ["ios", "microsoft", "android", "nuget", "nvidia", "ubuntu"], + "nintendo-switch": ["nintendo-private"], } diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index cc2ca8dfd0e8f9..ffe3556f33955d 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -2433,6 +2433,19 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: "filters": {"filetypes": ["elf_code", "elf_debug"]}, "is_public": True, }, + "nintendo-private": { + "type": "s3", + "id": "sentry:nintendo-private", + "name": "Nintendo Symbol Server", + "bucket": os.environ.get("NINTENDO_SYMBOL_SERVER_S3_BUCKET", ""), + "region": os.environ.get("NINTENDO_SYMBOL_SERVER_S3_REGION", ""), + "access_key": os.environ.get("NINTENDO_SYMBOL_SERVER_AWS_ACCESS_KEY_ID", ""), + "secret_key": os.environ.get("NINTENDO_SYMBOL_SERVER_AWS_SECRET_ACCESS_KEY", ""), + "layout": {"type": "native"}, + "prefix": os.environ.get("NINTENDO_SYMBOL_SERVER_S3_PREFIX", ""), + "is_public": False, + "platforms": ["nintendo-switch"], + }, } # Relay diff --git a/static/app/views/settings/projectDebugFiles/index.tsx b/static/app/views/settings/projectDebugFiles/index.tsx index 46e887d8e502ae..27d0d96a06a27e 100644 --- a/static/app/views/settings/projectDebugFiles/index.tsx +++ b/static/app/views/settings/projectDebugFiles/index.tsx @@ -53,8 +53,17 @@ function makeDebugFilesQueryKey({ return [`/projects/${orgSlug}/${projectSlug}/files/dsyms/`, {query}]; } -function makeSymbolSourcesQueryKey({orgSlug}: {orgSlug: string}): ApiQueryKey { - return [`/organizations/${orgSlug}/builtin-symbol-sources/`]; +function makeSymbolSourcesQueryKey({ + orgSlug, + platform, +}: { + orgSlug: string; + platform?: string; +}): ApiQueryKey { + return [ + `/organizations/${orgSlug}/builtin-symbol-sources/`, + {query: {platform: platform || ''}}, + ]; } function ProjectDebugSymbols({organization, project, location, router, params}: Props) { @@ -91,7 +100,10 @@ function ProjectDebugSymbols({organization, project, location, router, params}: isError: isErrorSymbolSources, refetch: refetchSymbolSources, } = useApiQuery( - makeSymbolSourcesQueryKey({orgSlug: organization.slug}), + makeSymbolSourcesQueryKey({ + orgSlug: organization.slug, + platform: project.platform, + }), { staleTime: 0, enabled: hasSymbolSourcesFeatureFlag, @@ -137,6 +149,7 @@ function ProjectDebugSymbols({organization, project, location, router, params}: queryClient.invalidateQueries({ queryKey: makeSymbolSourcesQueryKey({ orgSlug: organization.slug, + platform: project.platform, }), }); }, diff --git a/tests/sentry/api/endpoints/test_builtin_symbol_sources.py b/tests/sentry/api/endpoints/test_builtin_symbol_sources.py index 1ebc9a83658062..c280b02b1836bd 100644 --- a/tests/sentry/api/endpoints/test_builtin_symbol_sources.py +++ b/tests/sentry/api/endpoints/test_builtin_symbol_sources.py @@ -1,5 +1,33 @@ +from django.test import override_settings + from sentry.testutils.cases import APITestCase +SENTRY_BUILTIN_SOURCES_PLATFORM_TEST = { + "public-source-1": { + "id": "sentry:public-1", + "name": "Public Source 1", + "type": "http", + "url": "https://example.com/symbols/", + }, + "public-source-2": { + "id": "sentry:public-2", + "name": "Public Source 2", + "type": "http", + "url": "https://example.com/symbols2/", + }, + "nintendo-private": { + "id": "sentry:nintendo-private", + "name": "Nintendo Private", + "type": "s3", + "bucket": "nintendo-symbols", + "region": "us-east-1", + "access_key": "test-key", + "secret_key": "test-secret", + "layout": {"type": "native"}, + "platforms": ["nintendo-switch"], + }, +} + class BuiltinSymbolSourcesNoSlugTest(APITestCase): endpoint = "sentry-api-0-builtin-symbol-sources" @@ -39,3 +67,77 @@ def test_with_slug(self) -> None: assert "id" in body[0] assert "name" in body[0] assert "hidden" in body[0] + + +class BuiltinSymbolSourcesPlatformFilteringTest(APITestCase): + endpoint = "sentry-api-0-organization-builtin-symbol-sources" + + def setUp(self) -> None: + super().setUp() + self.organization = self.create_organization(owner=self.user) + self.login_as(user=self.user) + + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) + def test_platform_filtering_nintendo_switch_with_access(self) -> None: + """Nintendo Switch platform should see nintendo-private source only if org has access""" + # Enable nintendo-switch for this organization + self.organization.update_option("sentry:enabled_console_platforms", ["nintendo-switch"]) + + resp = self.get_response(self.organization.slug, qs_params={"platform": "nintendo-switch"}) + assert resp.status_code == 200 + + body = resp.data + source_keys = [source["sentry_key"] for source in body] + + # Nintendo Switch with access should see nintendo-private + assert "nintendo-private" in source_keys + # Should also see public sources (no platform restriction) + assert "public-source-1" in source_keys + assert "public-source-2" in source_keys + + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) + def test_platform_filtering_nintendo_switch_without_access(self) -> None: + """Nintendo Switch platform should NOT see nintendo-private if org lacks access""" + # Organization does not have nintendo-switch enabled (default is empty list) + + resp = self.get_response(self.organization.slug, qs_params={"platform": "nintendo-switch"}) + assert resp.status_code == 200 + + body = resp.data + source_keys = [source["sentry_key"] for source in body] + + # Should NOT see nintendo-private without console platform access + assert "nintendo-private" not in source_keys + # Should still see public sources + assert "public-source-1" in source_keys + assert "public-source-2" in source_keys + + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) + def test_platform_filtering_unity(self) -> None: + """Unity platform should NOT see nintendo-private source""" + resp = self.get_response(self.organization.slug, qs_params={"platform": "unity"}) + assert resp.status_code == 200 + + body = resp.data + source_keys = [source["sentry_key"] for source in body] + + # Unity should see public sources (no platform restriction) + assert "public-source-1" in source_keys + assert "public-source-2" in source_keys + # Unity should NOT see nintendo-private (restricted to nintendo-switch) + assert "nintendo-private" not in source_keys + + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) + def test_no_platform_parameter(self) -> None: + """Without platform parameter, should see public sources but not platform-restricted ones""" + resp = self.get_response(self.organization.slug) + assert resp.status_code == 200 + + body = resp.data + source_keys = [source["sentry_key"] for source in body] + + # Should see public sources (no platform restriction) + assert "public-source-1" in source_keys + assert "public-source-2" in source_keys + # Should NOT see platform-restricted source when no platform is provided + assert "nintendo-private" not in source_keys From 584cd00b4c799ed17761651b61a9bd1ff7631b89 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 24 Oct 2025 14:10:43 +0200 Subject: [PATCH 02/14] chore: reorder fields --- src/sentry/conf/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index ffe3556f33955d..beecc93d6ba122 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -2437,14 +2437,14 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: "type": "s3", "id": "sentry:nintendo-private", "name": "Nintendo Symbol Server", + "layout": {"type": "native"}, + "is_public": False, + "platforms": ["nintendo-switch"], "bucket": os.environ.get("NINTENDO_SYMBOL_SERVER_S3_BUCKET", ""), "region": os.environ.get("NINTENDO_SYMBOL_SERVER_S3_REGION", ""), "access_key": os.environ.get("NINTENDO_SYMBOL_SERVER_AWS_ACCESS_KEY_ID", ""), "secret_key": os.environ.get("NINTENDO_SYMBOL_SERVER_AWS_SECRET_ACCESS_KEY", ""), - "layout": {"type": "native"}, "prefix": os.environ.get("NINTENDO_SYMBOL_SERVER_S3_PREFIX", ""), - "is_public": False, - "platforms": ["nintendo-switch"], }, } From cc158661f427d8dd257076e1d6ff091922b43066 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 24 Oct 2025 14:49:02 +0200 Subject: [PATCH 03/14] formatting --- src/sentry/api/endpoints/builtin_symbol_sources.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/sentry/api/endpoints/builtin_symbol_sources.py b/src/sentry/api/endpoints/builtin_symbol_sources.py index 38da5382851465..d24bab87f87ded 100644 --- a/src/sentry/api/endpoints/builtin_symbol_sources.py +++ b/src/sentry/api/endpoints/builtin_symbol_sources.py @@ -19,9 +19,7 @@ def normalize_symbol_source(key, source): } -def organization_has_console_platform_access( - organization: Organization, platform: str -) -> bool: +def organization_has_console_platform_access(organization: Organization, platform: str) -> bool: """ Check if an organization has access to a specific console platform. @@ -57,9 +55,7 @@ def get(self, request: Request, **kwargs) -> Response: if str(organization_id_or_slug).isdecimal(): organization = Organization.objects.get_from_cache(id=organization_id_or_slug) else: - organization = Organization.objects.get_from_cache( - slug=organization_id_or_slug - ) + organization = Organization.objects.get_from_cache(slug=organization_id_or_slug) except Organization.DoesNotExist: pass From 6f916cca4ae04a087fb64a00d0506f67d4874634 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:07:26 +0100 Subject: [PATCH 04/14] Update static/app/views/settings/projectDebugFiles/index.tsx Co-authored-by: Priscila Oliveira --- static/app/views/settings/projectDebugFiles/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/settings/projectDebugFiles/index.tsx b/static/app/views/settings/projectDebugFiles/index.tsx index 27d0d96a06a27e..d12a01f040a321 100644 --- a/static/app/views/settings/projectDebugFiles/index.tsx +++ b/static/app/views/settings/projectDebugFiles/index.tsx @@ -62,7 +62,7 @@ function makeSymbolSourcesQueryKey({ }): ApiQueryKey { return [ `/organizations/${orgSlug}/builtin-symbol-sources/`, - {query: {platform: platform || ''}}, + {query: {platform}}, ]; } From b73221a5585de2437d886b7db206a392615365c5 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 28 Oct 2025 13:08:04 +0100 Subject: [PATCH 05/14] feat(symsorter): Add platform-restricted builtin symbol sources with org access control Adds support for platform-restricted symbol sources (e.g., nintendo-private) with organization-level access control for console platforms. Changes: - Add 'platforms' field to COMMON_SOURCE_PROPERTIES schema for validation - Add "nintendo-switch": ["nintendo-private"] to DEFAULT_SYMBOL_SOURCES mapping - Update set_default_symbol_sources() to check organization access for platform-restricted sources - Checks source's 'platforms' field against org's 'enabled_console_platforms' option - Only adds source if org has access to at least one required platform - Supports both Django Project and RpcProject types - Pass organization parameter in apply_default_project_settings() - Add comprehensive test suite with 8 test cases How it works: - For nintendo-switch projects, checks if org has "nintendo-switch" in enabled_console_platforms - If yes: adds nintendo-private source - If no: skips nintendo-private source - Non-restricted sources (ios, microsoft, etc.) are added without access checks Benefits: - Explicit platform mapping in DEFAULT_SYMBOL_SOURCES (follows existing pattern) - Organization access control for console platform sources - Scalable to other console platforms (PlayStation, Xbox, etc.) - Automatic org fetch from project if not provided Test coverage: - Basic platform handling (no platform, unknown, electron, unity) - Organization auto-fetch from project - Platform-restricted sources with and without org access - Non-restricted sources unaffected by console platform access --- .../api/helpers/default_symbol_sources.py | 62 ++++++++++- src/sentry/core/endpoints/team_projects.py | 2 +- src/sentry/lang/native/sources.py | 1 + .../helpers/test_default_symbol_sources.py | 104 ++++++++++++++++++ 4 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 tests/sentry/api/helpers/test_default_symbol_sources.py diff --git a/src/sentry/api/helpers/default_symbol_sources.py b/src/sentry/api/helpers/default_symbol_sources.py index 57a4e55130e2aa..b4d770160a0904 100644 --- a/src/sentry/api/helpers/default_symbol_sources.py +++ b/src/sentry/api/helpers/default_symbol_sources.py @@ -1,3 +1,7 @@ +from django.conf import settings + +from sentry.constants import ENABLED_CONSOLE_PLATFORMS_DEFAULT +from sentry.models.organization import Organization from sentry.models.project import Project from sentry.projects.services.project import RpcProject @@ -11,8 +15,56 @@ } -def set_default_symbol_sources(project: Project | RpcProject) -> None: - if project.platform and project.platform in DEFAULT_SYMBOL_SOURCES: - project.update_option( - "sentry:builtin_symbol_sources", DEFAULT_SYMBOL_SOURCES[project.platform] - ) +def set_default_symbol_sources( + project: Project | RpcProject, organization: Organization | None = None +) -> None: + """ + Sets default symbol sources for a project based on its platform. + + For sources with platform restrictions (e.g., console platforms), this function checks + if the organization has access to the required platform before adding the source. + + Args: + project: The project to configure symbol sources for + organization: Optional organization (fetched from project if not provided) + """ + if not project.platform or project.platform not in DEFAULT_SYMBOL_SOURCES: + return + + # Get organization from project if not provided + if organization is None: + if isinstance(project, Project): + organization = project.organization + else: + # For RpcProject, fetch organization by ID + organization = Organization.objects.get_from_cache(id=project.organization_id) + + # Get default sources for this platform + source_keys = DEFAULT_SYMBOL_SOURCES[project.platform] + + # Filter sources based on platform restrictions and organization access + enabled_sources = [] + for source_key in source_keys: + source_config = settings.SENTRY_BUILTIN_SOURCES.get(source_key) + if not source_config: + continue + + # Check if source has platform restrictions + required_platforms = source_config.get("platforms") + if required_platforms: + # Source is platform-restricted - check if org has access + enabled_console_platforms = organization.get_option( + "sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT + ) + + # Only add source if org has access to at least one of the required platforms + has_access = any( + platform in enabled_console_platforms for platform in required_platforms + ) + if not has_access: + continue + + enabled_sources.append(source_key) + + if enabled_sources: + project.update_option("sentry:builtin_symbol_sources", enabled_sources) diff --git a/src/sentry/core/endpoints/team_projects.py b/src/sentry/core/endpoints/team_projects.py index 43bbca677eea54..1888c56c7493e8 100644 --- a/src/sentry/core/endpoints/team_projects.py +++ b/src/sentry/core/endpoints/team_projects.py @@ -47,7 +47,7 @@ def apply_default_project_settings(organization: Organization, project: Project) set_default_disabled_detectors(project) - set_default_symbol_sources(project) + set_default_symbol_sources(project, organization) # Create project option to turn on ML similarity feature for new EA projects if project_is_seer_eligible(project): diff --git a/src/sentry/lang/native/sources.py b/src/sentry/lang/native/sources.py index b97eee02cf5dcb..fa9dfdbdb4b849 100644 --- a/src/sentry/lang/native/sources.py +++ b/src/sentry/lang/native/sources.py @@ -86,6 +86,7 @@ "filters": FILTERS_SCHEMA, "is_public": {"type": "boolean"}, "has_index": {"type": "boolean"}, + "platforms": {"type": "array", "items": {"type": "string"}}, } APP_STORE_CONNECT_SCHEMA = { diff --git a/tests/sentry/api/helpers/test_default_symbol_sources.py b/tests/sentry/api/helpers/test_default_symbol_sources.py new file mode 100644 index 00000000000000..ff8bd9b26cfbae --- /dev/null +++ b/tests/sentry/api/helpers/test_default_symbol_sources.py @@ -0,0 +1,104 @@ +from django.test import override_settings + +from sentry.api.helpers.default_symbol_sources import set_default_symbol_sources +from sentry.testutils.cases import TestCase + + +class SetDefaultSymbolSourcesTest(TestCase): + def setUp(self): + super().setUp() + self.organization = self.create_organization(owner=self.user) + + def test_no_platform(self): + """Projects without a platform should not get any default symbol sources""" + project = self.create_project(organization=self.organization, platform=None) + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is None + + def test_unknown_platform(self): + """Projects with unknown platforms should not get any default symbol sources""" + project = self.create_project(organization=self.organization, platform="unknown-platform") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is None + + def test_electron_platform(self): + """Electron projects should get the correct default sources""" + project = self.create_project(organization=self.organization, platform="electron") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is not None + assert "ios" in sources + assert "microsoft" in sources + assert "electron" in sources + + def test_unity_platform(self): + """Unity projects should get the correct default sources""" + project = self.create_project(organization=self.organization, platform="unity") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is not None + assert "ios" in sources + assert "microsoft" in sources + assert "android" in sources + assert "nuget" in sources + assert "unity" in sources + assert "nvidia" in sources + assert "ubuntu" in sources + + def test_organization_auto_fetch_from_project(self): + """Function should auto-fetch organization from project if not provided""" + project = self.create_project(organization=self.organization, platform="electron") + # Don't pass organization parameter + set_default_symbol_sources(project) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is not None + assert "electron" in sources + + +class PlatformRestrictedSymbolSourcesTest(TestCase): + """Tests for platform-restricted symbol sources (e.g., console platforms)""" + + def setUp(self): + super().setUp() + self.organization = self.create_organization(owner=self.user) + + def test_nintendo_switch_with_org_access(self): + """Nintendo Switch project should get nintendo-private source if org has access""" + # Grant org access to nintendo-switch console platform + self.organization.update_option("sentry:enabled_console_platforms", ["nintendo-switch"]) + + project = self.create_project(organization=self.organization, platform="nintendo-switch") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is not None + assert "nintendo-private" in sources + + def test_nintendo_switch_without_org_access(self): + """Nintendo Switch project should NOT get nintendo-private source if org lacks access""" + # Org has no enabled console platforms (default is empty list) + project = self.create_project(organization=self.organization, platform="nintendo-switch") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + # Should be None or empty since no sources are available + assert sources is None or sources == [] + + def test_unity_not_affected_by_console_restrictions(self): + """Unity projects should get sources regardless of console platform access""" + # Org has no enabled console platforms + project = self.create_project(organization=self.organization, platform="unity") + set_default_symbol_sources(project, self.organization) + + sources = project.get_option("sentry:builtin_symbol_sources") + assert sources is not None + # Unity sources have no platform restrictions, so they should all be added + assert "unity" in sources + assert "microsoft" in sources From 64eab6548dcc6729085c8336993df86684506995 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 28 Oct 2025 15:00:38 +0100 Subject: [PATCH 06/14] cleanup --- src/sentry/api/endpoints/builtin_symbol_sources.py | 2 +- src/sentry/api/helpers/default_symbol_sources.py | 2 +- static/app/views/settings/projectDebugFiles/index.tsx | 5 +---- tests/sentry/api/helpers/test_default_symbol_sources.py | 2 -- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/sentry/api/endpoints/builtin_symbol_sources.py b/src/sentry/api/endpoints/builtin_symbol_sources.py index d24bab87f87ded..2ad34f255826f5 100644 --- a/src/sentry/api/endpoints/builtin_symbol_sources.py +++ b/src/sentry/api/endpoints/builtin_symbol_sources.py @@ -61,7 +61,7 @@ def get(self, request: Request, **kwargs) -> Response: sources = [] for key, source in settings.SENTRY_BUILTIN_SOURCES.items(): - source_platforms = source.get("platforms") + source_platforms: list[str] | None = source.get("platforms") # If source has platform restrictions, check if current platform matches if source_platforms is not None: diff --git a/src/sentry/api/helpers/default_symbol_sources.py b/src/sentry/api/helpers/default_symbol_sources.py index b4d770160a0904..9abfdc169c49ec 100644 --- a/src/sentry/api/helpers/default_symbol_sources.py +++ b/src/sentry/api/helpers/default_symbol_sources.py @@ -50,7 +50,7 @@ def set_default_symbol_sources( continue # Check if source has platform restrictions - required_platforms = source_config.get("platforms") + required_platforms: list[str] | None = source_config.get("platforms") if required_platforms: # Source is platform-restricted - check if org has access enabled_console_platforms = organization.get_option( diff --git a/static/app/views/settings/projectDebugFiles/index.tsx b/static/app/views/settings/projectDebugFiles/index.tsx index d12a01f040a321..4614216ae60e30 100644 --- a/static/app/views/settings/projectDebugFiles/index.tsx +++ b/static/app/views/settings/projectDebugFiles/index.tsx @@ -60,10 +60,7 @@ function makeSymbolSourcesQueryKey({ orgSlug: string; platform?: string; }): ApiQueryKey { - return [ - `/organizations/${orgSlug}/builtin-symbol-sources/`, - {query: {platform}}, - ]; + return [`/organizations/${orgSlug}/builtin-symbol-sources/`, {query: {platform}}]; } function ProjectDebugSymbols({organization, project, location, router, params}: Props) { diff --git a/tests/sentry/api/helpers/test_default_symbol_sources.py b/tests/sentry/api/helpers/test_default_symbol_sources.py index ff8bd9b26cfbae..7cb14fd147c866 100644 --- a/tests/sentry/api/helpers/test_default_symbol_sources.py +++ b/tests/sentry/api/helpers/test_default_symbol_sources.py @@ -1,5 +1,3 @@ -from django.test import override_settings - from sentry.api.helpers.default_symbol_sources import set_default_symbol_sources from sentry.testutils.cases import TestCase From ffccdeca9f31c3d791df462153422206c22341f3 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 29 Oct 2025 08:50:27 +0100 Subject: [PATCH 07/14] fix: Preserve non-existent symbol sources in platform defaults The original implementation incorrectly filtered out symbol sources like "ios" and "android" that don't exist in SENTRY_BUILTIN_SOURCES. These sources are intentionally included in defaults even though they don't exist in the config - they get filtered at runtime in sources.py. Changes: - Only check platform restrictions for sources that exist in config - Sources without config entries are included without restriction checks - Always update project option for recognized platforms (even if empty) - Fix tests to verify epoch defaults are preserved for unknown platforms This ensures the existing test_team_projects.py tests continue to pass while properly implementing platform-restricted sources. --- .../api/helpers/default_symbol_sources.py | 36 ++++++++++--------- src/sentry/projectoptions/defaults.py | 7 ++-- .../helpers/test_default_symbol_sources.py | 16 ++++++--- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/sentry/api/helpers/default_symbol_sources.py b/src/sentry/api/helpers/default_symbol_sources.py index 9abfdc169c49ec..133195bc013e2a 100644 --- a/src/sentry/api/helpers/default_symbol_sources.py +++ b/src/sentry/api/helpers/default_symbol_sources.py @@ -46,25 +46,27 @@ def set_default_symbol_sources( enabled_sources = [] for source_key in source_keys: source_config = settings.SENTRY_BUILTIN_SOURCES.get(source_key) - if not source_config: - continue - # Check if source has platform restrictions - required_platforms: list[str] | None = source_config.get("platforms") - if required_platforms: - # Source is platform-restricted - check if org has access - enabled_console_platforms = organization.get_option( - "sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT - ) + # If source exists in config, check for platform restrictions + if source_config: + required_platforms: list[str] | None = source_config.get("platforms") + if required_platforms: + # Source is platform-restricted - check if org has access + enabled_console_platforms = organization.get_option( + "sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT + ) - # Only add source if org has access to at least one of the required platforms - has_access = any( - platform in enabled_console_platforms for platform in required_platforms - ) - if not has_access: - continue + # Only add source if org has access to at least one of the required platforms + has_access = any( + platform in enabled_console_platforms for platform in required_platforms + ) + if not has_access: + continue + # Include the source (either it passed platform check or doesn't exist in config) + # Non-existent sources will be filtered out at runtime in sources.py enabled_sources.append(source_key) - if enabled_sources: - project.update_option("sentry:builtin_symbol_sources", enabled_sources) + # Always update the option for recognized platforms, even if empty + # This ensures platform-specific defaults override epoch defaults + project.update_option("sentry:builtin_symbol_sources", enabled_sources) diff --git a/src/sentry/projectoptions/defaults.py b/src/sentry/projectoptions/defaults.py index 6da9c2265e2c36..ff667e4dbcf356 100644 --- a/src/sentry/projectoptions/defaults.py +++ b/src/sentry/projectoptions/defaults.py @@ -33,9 +33,10 @@ epoch_defaults={1: "4.x", 2: "5.x", 7: "6.x", 8: "7.x", 13: "8.x", 14: "9.x", 15: "10.x"}, ) -# Default symbol sources. The ios source does not exist by default and -# will be skipped later. The microsoft source exists by default and is -# unlikely to be disabled. +# Default symbol sources. The ios source does not exist by default and +# will be skipped later. The microsoft source exists by default and is +# unlikely to be disabled. Platform-specific sources may be added via +# set_default_symbol_sources() when a project is created. register( key="sentry:builtin_symbol_sources", epoch_defaults={ diff --git a/tests/sentry/api/helpers/test_default_symbol_sources.py b/tests/sentry/api/helpers/test_default_symbol_sources.py index 7cb14fd147c866..616affabcca31f 100644 --- a/tests/sentry/api/helpers/test_default_symbol_sources.py +++ b/tests/sentry/api/helpers/test_default_symbol_sources.py @@ -8,20 +8,28 @@ def setUp(self): self.organization = self.create_organization(owner=self.user) def test_no_platform(self): - """Projects without a platform should not get any default symbol sources""" + """Projects without a platform should keep their epoch defaults""" project = self.create_project(organization=self.organization, platform=None) + # Capture epoch defaults before calling set_default_symbol_sources + epoch_defaults = project.get_option("sentry:builtin_symbol_sources") + set_default_symbol_sources(project, self.organization) + # Should not change the defaults for projects without a platform sources = project.get_option("sentry:builtin_symbol_sources") - assert sources is None + assert sources == epoch_defaults def test_unknown_platform(self): - """Projects with unknown platforms should not get any default symbol sources""" + """Projects with unknown platforms should keep their epoch defaults""" project = self.create_project(organization=self.organization, platform="unknown-platform") + # Capture epoch defaults before calling set_default_symbol_sources + epoch_defaults = project.get_option("sentry:builtin_symbol_sources") + set_default_symbol_sources(project, self.organization) + # Should not change the defaults for projects with unknown platforms sources = project.get_option("sentry:builtin_symbol_sources") - assert sources is None + assert sources == epoch_defaults def test_electron_platform(self): """Electron projects should get the correct default sources""" From 921d8dc8a62e12092d91099707afff0282fbdb86 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 29 Oct 2025 10:30:06 +0100 Subject: [PATCH 08/14] fix(types): Add type casts for platform restrictions to fix mypy errors Use typing.cast to explicitly cast source.get('platforms') to list[str] | None to satisfy mypy's type checking. The cast is safe because we check if the source exists before accessing it, and the platforms field is always either None or list[str] per the SENTRY_BUILTIN_SOURCES configuration. --- src/sentry/api/endpoints/builtin_symbol_sources.py | 4 +++- src/sentry/api/helpers/default_symbol_sources.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sentry/api/endpoints/builtin_symbol_sources.py b/src/sentry/api/endpoints/builtin_symbol_sources.py index 2ad34f255826f5..24eaaf0c4a5efb 100644 --- a/src/sentry/api/endpoints/builtin_symbol_sources.py +++ b/src/sentry/api/endpoints/builtin_symbol_sources.py @@ -1,3 +1,5 @@ +from typing import cast + from django.conf import settings from rest_framework.request import Request from rest_framework.response import Response @@ -61,7 +63,7 @@ def get(self, request: Request, **kwargs) -> Response: sources = [] for key, source in settings.SENTRY_BUILTIN_SOURCES.items(): - source_platforms: list[str] | None = source.get("platforms") + source_platforms: list[str] | None = cast("list[str] | None", source.get("platforms")) # If source has platform restrictions, check if current platform matches if source_platforms is not None: diff --git a/src/sentry/api/helpers/default_symbol_sources.py b/src/sentry/api/helpers/default_symbol_sources.py index 133195bc013e2a..f7c4e9364d05b9 100644 --- a/src/sentry/api/helpers/default_symbol_sources.py +++ b/src/sentry/api/helpers/default_symbol_sources.py @@ -1,3 +1,5 @@ +from typing import cast + from django.conf import settings from sentry.constants import ENABLED_CONSOLE_PLATFORMS_DEFAULT @@ -49,7 +51,9 @@ def set_default_symbol_sources( # If source exists in config, check for platform restrictions if source_config: - required_platforms: list[str] | None = source_config.get("platforms") + required_platforms: list[str] | None = cast( + "list[str] | None", source_config.get("platforms") + ) if required_platforms: # Source is platform-restricted - check if org has access enabled_console_platforms = organization.get_option( From f7b555dbe1d59c4aff9880042b6c6a64f6fc9caa Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 29 Oct 2025 12:59:52 +0100 Subject: [PATCH 09/14] chore: Remove frontend changes (will be in separate PR) --- .../views/settings/projectDebugFiles/index.tsx | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/static/app/views/settings/projectDebugFiles/index.tsx b/static/app/views/settings/projectDebugFiles/index.tsx index 4614216ae60e30..46e887d8e502ae 100644 --- a/static/app/views/settings/projectDebugFiles/index.tsx +++ b/static/app/views/settings/projectDebugFiles/index.tsx @@ -53,14 +53,8 @@ function makeDebugFilesQueryKey({ return [`/projects/${orgSlug}/${projectSlug}/files/dsyms/`, {query}]; } -function makeSymbolSourcesQueryKey({ - orgSlug, - platform, -}: { - orgSlug: string; - platform?: string; -}): ApiQueryKey { - return [`/organizations/${orgSlug}/builtin-symbol-sources/`, {query: {platform}}]; +function makeSymbolSourcesQueryKey({orgSlug}: {orgSlug: string}): ApiQueryKey { + return [`/organizations/${orgSlug}/builtin-symbol-sources/`]; } function ProjectDebugSymbols({organization, project, location, router, params}: Props) { @@ -97,10 +91,7 @@ function ProjectDebugSymbols({organization, project, location, router, params}: isError: isErrorSymbolSources, refetch: refetchSymbolSources, } = useApiQuery( - makeSymbolSourcesQueryKey({ - orgSlug: organization.slug, - platform: project.platform, - }), + makeSymbolSourcesQueryKey({orgSlug: organization.slug}), { staleTime: 0, enabled: hasSymbolSourcesFeatureFlag, @@ -146,7 +137,6 @@ function ProjectDebugSymbols({organization, project, location, router, params}: queryClient.invalidateQueries({ queryKey: makeSymbolSourcesQueryKey({ orgSlug: organization.slug, - platform: project.platform, }), }); }, From b77b91961ab76aadbfb480c9fd9ae7b52e012587 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 29 Oct 2025 20:10:41 +0100 Subject: [PATCH 10/14] perf: Address Seer bot review feedback - Move organization.get_option() call outside loop to avoid repeated DB calls - Add error handling for Organization.DoesNotExist when fetching from RpcProject - Add comment explaining optimization Addresses HIGH and MEDIUM severity issues identified by Seer bot review. --- src/sentry/api/helpers/default_symbol_sources.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/sentry/api/helpers/default_symbol_sources.py b/src/sentry/api/helpers/default_symbol_sources.py index f7c4e9364d05b9..241927473eec65 100644 --- a/src/sentry/api/helpers/default_symbol_sources.py +++ b/src/sentry/api/helpers/default_symbol_sources.py @@ -39,11 +39,20 @@ def set_default_symbol_sources( organization = project.organization else: # For RpcProject, fetch organization by ID - organization = Organization.objects.get_from_cache(id=project.organization_id) + try: + organization = Organization.objects.get_from_cache(id=project.organization_id) + except Organization.DoesNotExist: + # If organization doesn't exist, cannot set defaults + return # Get default sources for this platform source_keys = DEFAULT_SYMBOL_SOURCES[project.platform] + # Get enabled console platforms once (optimization to avoid repeated DB calls) + enabled_console_platforms = organization.get_option( + "sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT + ) + # Filter sources based on platform restrictions and organization access enabled_sources = [] for source_key in source_keys: @@ -56,10 +65,6 @@ def set_default_symbol_sources( ) if required_platforms: # Source is platform-restricted - check if org has access - enabled_console_platforms = organization.get_option( - "sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT - ) - # Only add source if org has access to at least one of the required platforms has_access = any( platform in enabled_console_platforms for platform in required_platforms From 868544d5ff56c94d04649f31fcb3b9eff408cffb Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:03:56 +0100 Subject: [PATCH 11/14] Apply suggestions from code review Co-authored-by: Sebastian Zivota --- .../api/helpers/default_symbol_sources.py | 2 +- .../endpoints/test_builtin_symbol_sources.py | 24 +++++++++---------- .../helpers/test_default_symbol_sources.py | 6 ++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/sentry/api/helpers/default_symbol_sources.py b/src/sentry/api/helpers/default_symbol_sources.py index 241927473eec65..870332378cc81b 100644 --- a/src/sentry/api/helpers/default_symbol_sources.py +++ b/src/sentry/api/helpers/default_symbol_sources.py @@ -13,7 +13,7 @@ "unity": ["ios", "microsoft", "android", "nuget", "unity", "nvidia", "ubuntu"], "unreal": ["ios", "microsoft", "android", "nvidia", "ubuntu"], "godot": ["ios", "microsoft", "android", "nuget", "nvidia", "ubuntu"], - "nintendo-switch": ["nintendo-private"], + "nintendo-switch": ["nintendo"], } diff --git a/tests/sentry/api/endpoints/test_builtin_symbol_sources.py b/tests/sentry/api/endpoints/test_builtin_symbol_sources.py index c280b02b1836bd..fbde6cc3f601ff 100644 --- a/tests/sentry/api/endpoints/test_builtin_symbol_sources.py +++ b/tests/sentry/api/endpoints/test_builtin_symbol_sources.py @@ -15,8 +15,8 @@ "type": "http", "url": "https://example.com/symbols2/", }, - "nintendo-private": { - "id": "sentry:nintendo-private", + "nintendo": { + "id": "sentry:nintendo", "name": "Nintendo Private", "type": "s3", "bucket": "nintendo-symbols", @@ -79,7 +79,7 @@ def setUp(self) -> None: @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) def test_platform_filtering_nintendo_switch_with_access(self) -> None: - """Nintendo Switch platform should see nintendo-private source only if org has access""" + """Nintendo Switch platform should see nintendo source only if org has access""" # Enable nintendo-switch for this organization self.organization.update_option("sentry:enabled_console_platforms", ["nintendo-switch"]) @@ -89,15 +89,15 @@ def test_platform_filtering_nintendo_switch_with_access(self) -> None: body = resp.data source_keys = [source["sentry_key"] for source in body] - # Nintendo Switch with access should see nintendo-private - assert "nintendo-private" in source_keys + # Nintendo Switch with access should see nintendo + assert "nintendo" in source_keys # Should also see public sources (no platform restriction) assert "public-source-1" in source_keys assert "public-source-2" in source_keys @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) def test_platform_filtering_nintendo_switch_without_access(self) -> None: - """Nintendo Switch platform should NOT see nintendo-private if org lacks access""" + """Nintendo Switch platform should NOT see nintendo if org lacks access""" # Organization does not have nintendo-switch enabled (default is empty list) resp = self.get_response(self.organization.slug, qs_params={"platform": "nintendo-switch"}) @@ -106,15 +106,15 @@ def test_platform_filtering_nintendo_switch_without_access(self) -> None: body = resp.data source_keys = [source["sentry_key"] for source in body] - # Should NOT see nintendo-private without console platform access - assert "nintendo-private" not in source_keys + # Should NOT see nintendo without console platform access + assert "nintendo" not in source_keys # Should still see public sources assert "public-source-1" in source_keys assert "public-source-2" in source_keys @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) def test_platform_filtering_unity(self) -> None: - """Unity platform should NOT see nintendo-private source""" + """Unity platform should NOT see nintendo source""" resp = self.get_response(self.organization.slug, qs_params={"platform": "unity"}) assert resp.status_code == 200 @@ -124,8 +124,8 @@ def test_platform_filtering_unity(self) -> None: # Unity should see public sources (no platform restriction) assert "public-source-1" in source_keys assert "public-source-2" in source_keys - # Unity should NOT see nintendo-private (restricted to nintendo-switch) - assert "nintendo-private" not in source_keys + # Unity should NOT see nintendo (restricted to nintendo-switch) + assert "nintendo" not in source_keys @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST) def test_no_platform_parameter(self) -> None: @@ -140,4 +140,4 @@ def test_no_platform_parameter(self) -> None: assert "public-source-1" in source_keys assert "public-source-2" in source_keys # Should NOT see platform-restricted source when no platform is provided - assert "nintendo-private" not in source_keys + assert "nintendo" not in source_keys diff --git a/tests/sentry/api/helpers/test_default_symbol_sources.py b/tests/sentry/api/helpers/test_default_symbol_sources.py index 616affabcca31f..48d66bc14f1425 100644 --- a/tests/sentry/api/helpers/test_default_symbol_sources.py +++ b/tests/sentry/api/helpers/test_default_symbol_sources.py @@ -76,7 +76,7 @@ def setUp(self): self.organization = self.create_organization(owner=self.user) def test_nintendo_switch_with_org_access(self): - """Nintendo Switch project should get nintendo-private source if org has access""" + """Nintendo Switch project should get nintendo source if org has access""" # Grant org access to nintendo-switch console platform self.organization.update_option("sentry:enabled_console_platforms", ["nintendo-switch"]) @@ -85,10 +85,10 @@ def test_nintendo_switch_with_org_access(self): sources = project.get_option("sentry:builtin_symbol_sources") assert sources is not None - assert "nintendo-private" in sources + assert "nintendo" in sources def test_nintendo_switch_without_org_access(self): - """Nintendo Switch project should NOT get nintendo-private source if org lacks access""" + """Nintendo Switch project should NOT get nintendo source if org lacks access""" # Org has no enabled console platforms (default is empty list) project = self.create_project(organization=self.organization, platform="nintendo-switch") set_default_symbol_sources(project, self.organization) From 6a4b9f93a1dd36ea03e72a82f3effee1cd4dadcd Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:05:49 +0100 Subject: [PATCH 12/14] Apply suggestions from code review --- tests/sentry/api/endpoints/test_builtin_symbol_sources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sentry/api/endpoints/test_builtin_symbol_sources.py b/tests/sentry/api/endpoints/test_builtin_symbol_sources.py index fbde6cc3f601ff..326f4e08af5159 100644 --- a/tests/sentry/api/endpoints/test_builtin_symbol_sources.py +++ b/tests/sentry/api/endpoints/test_builtin_symbol_sources.py @@ -17,7 +17,7 @@ }, "nintendo": { "id": "sentry:nintendo", - "name": "Nintendo Private", + "name": "Nintendo SDK", "type": "s3", "bucket": "nintendo-symbols", "region": "us-east-1", From 60763414dd29616e5f7e5c1c52cadee042034321 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 27 Nov 2025 10:01:40 +0100 Subject: [PATCH 13/14] feat(symbol-sources): remove Nintendo Symbol Server configuration --- src/sentry/conf/server.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index e9d4a72394fc54..8f0e356088b6e9 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -2452,19 +2452,6 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: "filters": {"filetypes": ["elf_code", "elf_debug"]}, "is_public": True, }, - "nintendo-private": { - "type": "s3", - "id": "sentry:nintendo-private", - "name": "Nintendo Symbol Server", - "layout": {"type": "native"}, - "is_public": False, - "platforms": ["nintendo-switch"], - "bucket": os.environ.get("NINTENDO_SYMBOL_SERVER_S3_BUCKET", ""), - "region": os.environ.get("NINTENDO_SYMBOL_SERVER_S3_REGION", ""), - "access_key": os.environ.get("NINTENDO_SYMBOL_SERVER_AWS_ACCESS_KEY_ID", ""), - "secret_key": os.environ.get("NINTENDO_SYMBOL_SERVER_AWS_SECRET_ACCESS_KEY", ""), - "prefix": os.environ.get("NINTENDO_SYMBOL_SERVER_S3_PREFIX", ""), - }, } # Relay From c94b5dfeec851c707fef9f2b1cbd4dd65578b58a Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 27 Nov 2025 10:56:29 +0100 Subject: [PATCH 14/14] refactor(symbol-sources): extract console platform access check to shared utility Move organization_has_console_platform_access to sentry/utils/console_platforms.py so it can be reused by both builtin_symbol_sources.py and tempest/utils.py. Also fix platform-restricted tests to use mock settings since nintendo source is defined in getsentry, not in the default SENTRY_BUILTIN_SOURCES. --- .../api/endpoints/builtin_symbol_sources.py | 19 +-------------- src/sentry/tempest/utils.py | 7 ++---- src/sentry/utils/console_platforms.py | 19 +++++++++++++++ .../helpers/test_default_symbol_sources.py | 23 +++++++++++++++++-- 4 files changed, 43 insertions(+), 25 deletions(-) create mode 100644 src/sentry/utils/console_platforms.py diff --git a/src/sentry/api/endpoints/builtin_symbol_sources.py b/src/sentry/api/endpoints/builtin_symbol_sources.py index 24eaaf0c4a5efb..43d965c370aead 100644 --- a/src/sentry/api/endpoints/builtin_symbol_sources.py +++ b/src/sentry/api/endpoints/builtin_symbol_sources.py @@ -8,8 +8,8 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.serializers import serialize -from sentry.constants import ENABLED_CONSOLE_PLATFORMS_DEFAULT from sentry.models.organization import Organization +from sentry.utils.console_platforms import organization_has_console_platform_access def normalize_symbol_source(key, source): @@ -21,23 +21,6 @@ def normalize_symbol_source(key, source): } -def organization_has_console_platform_access(organization: Organization, platform: str) -> bool: - """ - Check if an organization has access to a specific console platform. - - Args: - organization: The organization to check - platform: The console platform (e.g., 'nintendo-switch', 'playstation', 'xbox') - - Returns: - True if the organization has access to the console platform, False otherwise - """ - enabled_console_platforms = organization.get_option( - "sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT - ) - return platform in enabled_console_platforms - - @region_silo_endpoint class BuiltinSymbolSourcesEndpoint(Endpoint): owner = ApiOwner.OWNERS_INGEST diff --git a/src/sentry/tempest/utils.py b/src/sentry/tempest/utils.py index 24f7cc3ff0d168..53d2caedb88133 100644 --- a/src/sentry/tempest/utils.py +++ b/src/sentry/tempest/utils.py @@ -1,12 +1,9 @@ from sentry.models.organization import Organization +from sentry.utils.console_platforms import organization_has_console_platform_access def has_tempest_access(organization: Organization | None) -> bool: - if not organization: return False - enabled_platforms = organization.get_option("sentry:enabled_console_platforms", []) - has_playstation_access = "playstation" in enabled_platforms - - return has_playstation_access + return organization_has_console_platform_access(organization, "playstation") diff --git a/src/sentry/utils/console_platforms.py b/src/sentry/utils/console_platforms.py new file mode 100644 index 00000000000000..96544e116beb42 --- /dev/null +++ b/src/sentry/utils/console_platforms.py @@ -0,0 +1,19 @@ +from sentry.constants import ENABLED_CONSOLE_PLATFORMS_DEFAULT +from sentry.models.organization import Organization + + +def organization_has_console_platform_access(organization: Organization, platform: str) -> bool: + """ + Check if an organization has access to a specific console platform. + + Args: + organization: The organization to check + platform: The console platform (e.g., 'nintendo-switch', 'playstation', 'xbox') + + Returns: + True if the organization has access to the console platform, False otherwise + """ + enabled_console_platforms = organization.get_option( + "sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT + ) + return platform in enabled_console_platforms diff --git a/tests/sentry/api/helpers/test_default_symbol_sources.py b/tests/sentry/api/helpers/test_default_symbol_sources.py index 48d66bc14f1425..4d486ad46fccdb 100644 --- a/tests/sentry/api/helpers/test_default_symbol_sources.py +++ b/tests/sentry/api/helpers/test_default_symbol_sources.py @@ -1,6 +1,23 @@ +from django.test import override_settings + from sentry.api.helpers.default_symbol_sources import set_default_symbol_sources from sentry.testutils.cases import TestCase +# Mock SENTRY_BUILTIN_SOURCES with a platform-restricted source for testing +SENTRY_BUILTIN_SOURCES_TEST = { + "nintendo": { + "id": "sentry:nintendo", + "name": "Nintendo SDK", + "type": "s3", + "bucket": "nintendo-symbols", + "region": "us-east-1", + "access_key": "test-key", + "secret_key": "test-secret", + "layout": {"type": "native"}, + "platforms": ["nintendo-switch"], + }, +} + class SetDefaultSymbolSourcesTest(TestCase): def setUp(self): @@ -75,6 +92,7 @@ def setUp(self): super().setUp() self.organization = self.create_organization(owner=self.user) + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_TEST) def test_nintendo_switch_with_org_access(self): """Nintendo Switch project should get nintendo source if org has access""" # Grant org access to nintendo-switch console platform @@ -87,6 +105,7 @@ def test_nintendo_switch_with_org_access(self): assert sources is not None assert "nintendo" in sources + @override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_TEST) def test_nintendo_switch_without_org_access(self): """Nintendo Switch project should NOT get nintendo source if org lacks access""" # Org has no enabled console platforms (default is empty list) @@ -94,8 +113,8 @@ def test_nintendo_switch_without_org_access(self): set_default_symbol_sources(project, self.organization) sources = project.get_option("sentry:builtin_symbol_sources") - # Should be None or empty since no sources are available - assert sources is None or sources == [] + # Should be empty since no sources are available (nintendo is restricted) + assert sources == [] def test_unity_not_affected_by_console_restrictions(self): """Unity projects should get sources regardless of console platform access"""