From 56f1495bb7e01ab03a0a1fe608aecd40e6b0b9a8 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 24 Nov 2025 16:26:25 -0500 Subject: [PATCH 1/3] feat(cells) Add silo annotations to remaining views/endpoints Add silo annotations to the rest of endpoints/views that didn't have silo assignments defined. Refs INFRENG-201 --- src/sentry/auth/providers/saml2/provider.py | 2 ++ .../integrations/web/discord_extension_configuration.py | 2 ++ src/sentry/integrations/web/vercel_extension_configuration.py | 3 +++ src/sentry/web/api.py | 1 + src/sentry/web/frontend/generic.py | 4 ++++ src/sentry/web/frontend/pipeline_advancer.py | 3 ++- src/social_auth/views.py | 4 ++++ 7 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/sentry/auth/providers/saml2/provider.py b/src/sentry/auth/providers/saml2/provider.py index 6bf78f5420ecb9..a98418bbd0adb4 100644 --- a/src/sentry/auth/providers/saml2/provider.py +++ b/src/sentry/auth/providers/saml2/provider.py @@ -128,6 +128,7 @@ def dispatch(self, request: HttpRequest, organization_slug: str) -> HttpResponse return pipeline.current_step() +@control_silo_view class SAML2ACSView(AuthView): @method_decorator(csrf_exempt) def dispatch(self, request: HttpRequest, pipeline: AuthHelper) -> HttpResponseBase: @@ -152,6 +153,7 @@ def dispatch(self, request: HttpRequest, pipeline: AuthHelper) -> HttpResponseBa return pipeline.next_step() +@control_silo_view class SAML2SLSView(BaseView): @method_decorator(csrf_exempt) def dispatch(self, request: HttpRequest, organization_slug: str) -> HttpResponseRedirect: diff --git a/src/sentry/integrations/web/discord_extension_configuration.py b/src/sentry/integrations/web/discord_extension_configuration.py index 73a4a8ec64ba3a..854cabf32982af 100644 --- a/src/sentry/integrations/web/discord_extension_configuration.py +++ b/src/sentry/integrations/web/discord_extension_configuration.py @@ -1,8 +1,10 @@ from sentry.integrations.types import IntegrationProviderSlug +from sentry.web.frontend.base import control_silo_view from .integration_extension_configuration import IntegrationExtensionConfigurationView +@control_silo_view class DiscordExtensionConfigurationView(IntegrationExtensionConfigurationView): provider = IntegrationProviderSlug.DISCORD.value external_provider_key = IntegrationProviderSlug.DISCORD.value diff --git a/src/sentry/integrations/web/vercel_extension_configuration.py b/src/sentry/integrations/web/vercel_extension_configuration.py index 3c2be960fdb759..a5b511900ac08b 100644 --- a/src/sentry/integrations/web/vercel_extension_configuration.py +++ b/src/sentry/integrations/web/vercel_extension_configuration.py @@ -1,6 +1,9 @@ +from sentry.web.frontend.base import control_silo_view + from .integration_extension_configuration import IntegrationExtensionConfigurationView +@control_silo_view class VercelExtensionConfigurationView(IntegrationExtensionConfigurationView): provider = "vercel" external_provider_key = "vercel" diff --git a/src/sentry/web/api.py b/src/sentry/web/api.py index f61a3d5bf4c052..96644f7a558dd3 100644 --- a/src/sentry/web/api.py +++ b/src/sentry/web/api.py @@ -67,6 +67,7 @@ } +@all_silo_view class ClientConfigView(BaseView): def get(self, request: Request) -> HttpResponse: return HttpResponse(json.dumps(get_client_config(request)), content_type="application/json") diff --git a/src/sentry/web/frontend/generic.py b/src/sentry/web/frontend/generic.py index a440df6db59fde..91a7b16988dead 100644 --- a/src/sentry/web/frontend/generic.py +++ b/src/sentry/web/frontend/generic.py @@ -7,6 +7,8 @@ from django.http import Http404, HttpResponseNotFound from django.views import static +from sentry.web.frontend.base import all_silo_view + FOREVER_CACHE = "max-age=315360000" # See @@ -45,6 +47,7 @@ def resolve(path): return os.path.split(absolute_path) +@all_silo_view def frontend_app_static_media(request, **kwargs): """ Serve static files that should not have any versioned paths/filenames. @@ -64,6 +67,7 @@ def frontend_app_static_media(request, **kwargs): return response +@all_silo_view def static_media(request, **kwargs): """ Serve static files below a given point in the directory structure. diff --git a/src/sentry/web/frontend/pipeline_advancer.py b/src/sentry/web/frontend/pipeline_advancer.py index 8c8f606ab0cb3a..08113e4c3f45c4 100644 --- a/src/sentry/web/frontend/pipeline_advancer.py +++ b/src/sentry/web/frontend/pipeline_advancer.py @@ -9,7 +9,7 @@ from sentry.integrations.types import IntegrationProviderSlug from sentry.organizations.absolute_url import generate_organization_url from sentry.utils.http import absolute_uri, create_redirect_url -from sentry.web.frontend.base import BaseView +from sentry.web.frontend.base import BaseView, all_silo_view # The request doesn't contain the pipeline type (pipeline information is stored # in redis keyed by the pipeline name), so we try to construct multiple pipelines @@ -17,6 +17,7 @@ PIPELINE_CLASSES = (IntegrationPipeline, IdentityPipeline) +@all_silo_view class PipelineAdvancerView(BaseView): """Gets the current pipeline from the request and executes the current step.""" diff --git a/src/social_auth/views.py b/src/social_auth/views.py index 57299992f4ab17..56cd14ff43f6c4 100644 --- a/src/social_auth/views.py +++ b/src/social_auth/views.py @@ -14,6 +14,7 @@ from django.utils.http import url_has_allowed_host_and_scheme from django.views.decorators.csrf import csrf_exempt +from sentry.web.frontend.base import control_silo_view from social_auth.decorators import dsa_view from social_auth.exceptions import AuthException from social_auth.utils import backend_setting, clean_partial_pipeline, setting @@ -23,6 +24,7 @@ PIPELINE_KEY = setting("SOCIAL_AUTH_PARTIAL_PIPELINE_KEY", "partial_pipeline") +@control_silo_view @dsa_view(setting("SOCIAL_AUTH_COMPLETE_URL_NAME", "socialauth_associate_complete_auth_sso")) def auth(request, backend): """Authenticate using social backend""" @@ -53,6 +55,7 @@ def auth(request, backend): return HttpResponse(backend.auth_html(), content_type="text/html;charset=UTF-8") +@control_silo_view @csrf_exempt @login_required @dsa_view() @@ -90,6 +93,7 @@ def complete(request, backend, *args, **kwargs): return HttpResponseRedirect(url) +@control_silo_view def auth_complete(request, backend, user, *args, **kwargs): """Complete auth process. Return authenticated user or None.""" if request.session.get(PIPELINE_KEY): From 0bfa333f3deb928faf5c444cf38f72197ba779f4 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 25 Nov 2025 10:40:41 -0500 Subject: [PATCH 2/3] Break import loop --- src/sentry/web/constants.py | 14 ++++++++++++++ src/sentry/web/frontend/base.py | 2 +- src/sentry/web/frontend/generic.py | 16 +--------------- 3 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 src/sentry/web/constants.py diff --git a/src/sentry/web/constants.py b/src/sentry/web/constants.py new file mode 100644 index 00000000000000..0d71d76995517f --- /dev/null +++ b/src/sentry/web/constants.py @@ -0,0 +1,14 @@ +FOREVER_CACHE = "max-age=315360000" + +# See +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#requiring_revalidation +# This means that clients *CAN* cache the resource, but they must revalidate +# before using it This means we will have a small HTTP request overhead to +# verify that the local resource is not outdated +# +# Note that the above docs state that "no-cache" is the same as "max-age=0, +# must-revalidate", but some CDNs will not treat them as the same +NO_CACHE = "max-age=0, must-revalidate" + +# no-store means that the response should not be stored in *ANY* cache +NEVER_CACHE = "max-age=0, no-cache, no-store, must-revalidate" diff --git a/src/sentry/web/frontend/base.py b/src/sentry/web/frontend/base.py index 499e2f2655dd7e..a1262f9e008731 100644 --- a/src/sentry/web/frontend/base.py +++ b/src/sentry/web/frontend/base.py @@ -49,7 +49,7 @@ from sentry.utils.audit import create_audit_entry from sentry.utils.auth import construct_link_with_query, is_valid_redirect from sentry.utils.http import absolute_uri, is_using_customer_domain, origin_from_request -from sentry.web.frontend.generic import FOREVER_CACHE +from sentry.web.constants import FOREVER_CACHE from sentry.web.helpers import render_to_response from sudo.views import redirect_to_sudo diff --git a/src/sentry/web/frontend/generic.py b/src/sentry/web/frontend/generic.py index 91a7b16988dead..76b0bdeabdf299 100644 --- a/src/sentry/web/frontend/generic.py +++ b/src/sentry/web/frontend/generic.py @@ -7,23 +7,9 @@ from django.http import Http404, HttpResponseNotFound from django.views import static +from sentry.web.constants import FOREVER_CACHE, NEVER_CACHE, NO_CACHE from sentry.web.frontend.base import all_silo_view -FOREVER_CACHE = "max-age=315360000" - -# See -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#requiring_revalidation -# This means that clients *CAN* cache the resource, but they must revalidate -# before using it This means we will have a small HTTP request overhead to -# verify that the local resource is not outdated -# -# Note that the above docs state that "no-cache" is the same as "max-age=0, -# must-revalidate", but some CDNs will not treat them as the same -NO_CACHE = "max-age=0, must-revalidate" - -# no-store means that the response should not be stored in *ANY* cache -NEVER_CACHE = "max-age=0, no-cache, no-store, must-revalidate" - def dev_favicon(request, extension): document_root, path = resolve("sentry/images/favicon-dev.png") From 39f9f9a6ec474170fd50e5ae73aac5143dad65be Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 25 Nov 2025 18:56:41 -0500 Subject: [PATCH 3/3] Fix mypy and update URL list for control --- static/app/data/controlsiloUrlPatterns.ts | 5 +++++ tests/sentry/sentry_apps/web/test_sentryapp_avatar.py | 2 +- tests/sentry/users/web/test_user_avatar.py | 2 +- tests/sentry/web/frontend/generic/test_static_media.py | 2 +- tests/sentry/web/frontend/test_doc_integration_avatar.py | 2 +- tests/sentry/web/frontend/test_organization_avatar.py | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/static/app/data/controlsiloUrlPatterns.ts b/static/app/data/controlsiloUrlPatterns.ts index f0dfb7995110da..844dfa5a4fa04c 100644 --- a/static/app/data/controlsiloUrlPatterns.ts +++ b/static/app/data/controlsiloUrlPatterns.ts @@ -59,6 +59,7 @@ const patterns: RegExp[] = [ new RegExp('^500/'), new RegExp('^404/'), new RegExp('^_warmup/$'), + new RegExp('^api/client-config/?$'), new RegExp('^api/0/organizations/[^/]+/api-keys/$'), new RegExp('^api/0/organizations/[^/]+/api-keys/[^/]+/$'), new RegExp('^api/0/organizations/[^/]+/audit-logs/$'), @@ -165,6 +166,7 @@ const patterns: RegExp[] = [ new RegExp('^oauth/token/$'), new RegExp('^oauth/userinfo/$'), new RegExp('^saml/acs/[^/]+/$'), + new RegExp('^saml/sls/[^/]+/$'), new RegExp('^auth/login/$'), new RegExp('^auth/login/[^/]+/$'), new RegExp('^auth/channel/[^/]+/[^/]+/$'), @@ -188,6 +190,7 @@ const patterns: RegExp[] = [ new RegExp('^avatar/[^/]+/$'), new RegExp('^sentry-app-avatar/[^/]+/$'), new RegExp('^doc-integration-avatar/[^/]+/$'), + new RegExp('^extensions/[^/]+/setup/$'), new RegExp('^extensions/jira/ui-hook/$'), new RegExp('^extensions/jira/descriptor/$'), new RegExp('^extensions/jira/installed/$'), @@ -209,6 +212,7 @@ const patterns: RegExp[] = [ new RegExp('^extensions/bitbucket/installed/$'), new RegExp('^extensions/bitbucket/uninstalled/$'), new RegExp('^extensions/bitbucket/search/[^/]+/[^/]+/$'), + new RegExp('^extensions/vercel/configure/$'), new RegExp('^extensions/vercel/delete/$'), new RegExp('^extensions/vercel/webhook/$'), new RegExp('^extensions/msteams/webhook/$'), @@ -216,6 +220,7 @@ const patterns: RegExp[] = [ new RegExp('^extensions/msteams/link-identity/[^/]+/$'), new RegExp('^extensions/msteams/unlink-identity/[^/]+/$'), new RegExp('^extensions/discord/interactions/$'), + new RegExp('^extensions/discord/configure/$'), new RegExp('^extensions/discord/link-identity/[^/]+/$'), new RegExp('^extensions/discord/unlink-identity/[^/]+/$'), new RegExp('^share/(?:group|issue)/[^/]+/$'), diff --git a/tests/sentry/sentry_apps/web/test_sentryapp_avatar.py b/tests/sentry/sentry_apps/web/test_sentryapp_avatar.py index 8482d0847b109b..f6e8c4b979e941 100644 --- a/tests/sentry/sentry_apps/web/test_sentryapp_avatar.py +++ b/tests/sentry/sentry_apps/web/test_sentryapp_avatar.py @@ -6,7 +6,7 @@ from sentry.sentry_apps.models.sentry_app_avatar import SentryAppAvatar from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test -from sentry.web.frontend.generic import FOREVER_CACHE +from sentry.web.constants import FOREVER_CACHE @control_silo_test diff --git a/tests/sentry/users/web/test_user_avatar.py b/tests/sentry/users/web/test_user_avatar.py index d429c07f5fa2f1..28ccddbd4cbc02 100644 --- a/tests/sentry/users/web/test_user_avatar.py +++ b/tests/sentry/users/web/test_user_avatar.py @@ -6,7 +6,7 @@ from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test from sentry.users.models.user_avatar import UserAvatar -from sentry.web.frontend.generic import FOREVER_CACHE +from sentry.web.constants import FOREVER_CACHE @control_silo_test diff --git a/tests/sentry/web/frontend/generic/test_static_media.py b/tests/sentry/web/frontend/generic/test_static_media.py index 05617d7c1ab7f5..7c2d754fcee4b9 100644 --- a/tests/sentry/web/frontend/generic/test_static_media.py +++ b/tests/sentry/web/frontend/generic/test_static_media.py @@ -5,7 +5,7 @@ from sentry.testutils.cases import TestCase from sentry.testutils.helpers.response import close_streaming_response from sentry.utils.assets import get_frontend_app_asset_url -from sentry.web.frontend.generic import FOREVER_CACHE, NEVER_CACHE, NO_CACHE +from sentry.web.constants import FOREVER_CACHE, NEVER_CACHE, NO_CACHE class StaticMediaTest(TestCase): diff --git a/tests/sentry/web/frontend/test_doc_integration_avatar.py b/tests/sentry/web/frontend/test_doc_integration_avatar.py index 60fba902a8c550..e230810b5e804e 100644 --- a/tests/sentry/web/frontend/test_doc_integration_avatar.py +++ b/tests/sentry/web/frontend/test_doc_integration_avatar.py @@ -2,7 +2,7 @@ from sentry.testutils.cases import APITestCase from sentry.testutils.silo import control_silo_test -from sentry.web.frontend.generic import FOREVER_CACHE +from sentry.web.constants import FOREVER_CACHE @control_silo_test diff --git a/tests/sentry/web/frontend/test_organization_avatar.py b/tests/sentry/web/frontend/test_organization_avatar.py index 6c5e0f98b71476..4698613e6e5469 100644 --- a/tests/sentry/web/frontend/test_organization_avatar.py +++ b/tests/sentry/web/frontend/test_organization_avatar.py @@ -5,7 +5,7 @@ from sentry.models.avatars.organization_avatar import OrganizationAvatar from sentry.models.files.file import File from sentry.testutils.cases import TestCase -from sentry.web.frontend.generic import FOREVER_CACHE +from sentry.web.constants import FOREVER_CACHE class OrganizationAvatarTest(TestCase):