Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/sentry/management/commands/generate_controlsilo_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.urls import URLPattern, URLResolver

from sentry.silo.base import SiloMode
from sentry.web.frontend.react_page import GenericReactPageView, ReactPageView

# There are a handful of catchall routes
# that we don't want to handle in controlsilo
Expand Down Expand Up @@ -95,7 +96,7 @@ def handle(self, **options):
func = info.callable
if isinstance(func, functools.partial):
func = func.func
if hasattr(func, "view_class"):
elif hasattr(func, "view_class"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic change breaks view_class extraction for partial-wrapped views

Medium Severity

The change from if to elif for the view_class check alters the control flow logic. Previously, if func was a functools.partial, the code would unwrap it and then independently check if the unwrapped function has a view_class attribute. With elif, these checks are now mutually exclusive - if func is a partial, the code unwraps it but never extracts view_class from the unwrapped function. This could cause incorrect module/function name resolution and break the issubclass check for ReactPageView/GenericReactPageView for any partial-wrapped class-based views.

Fix in Cursor Fix in Web

func = func.view_class

if hasattr(func, "__name__"):
Expand All @@ -114,7 +115,21 @@ def handle(self, **options):
except ImportError as err:
raise CommandError(f"Could not load view in {module}: {err}")

if not hasattr(view_func, "silo_limit"):
# If a view/endpoint doesn't have a silo_limit it is likely a basic django view.
# We have tests in tests/sentry/silo/test_base.py that ensure all views/endpoints
# have silo annotations on them. We skip including URLs for react views
# as the UI doesn't make requests to them, but the UI code *does* make
# requests to other HTML views (like sentry-wizard)
if not hasattr(view_func, "silo_limit") or (
isinstance(view_func, type)
and issubclass(
view_func,
(
ReactPageView,
GenericReactPageView,
),
)
):
continue

silo_limit = view_func.silo_limit
Expand Down
5 changes: 3 additions & 2 deletions src/sentry/testutils/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,8 +719,9 @@ def path_2fa(self):
return reverse("sentry-account-settings-security")

def enable_org_2fa(self, organization):
organization.flags.require_2fa = True
organization.save()
with assume_test_silo_mode(SiloMode.REGION):
organization.flags.require_2fa = True
organization.save()

def api_enable_org_2fa(self, organization, user):
self.login_as(user)
Expand Down
11 changes: 3 additions & 8 deletions src/sentry/web/frontend/react_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@
)
from sentry.utils.http import is_using_customer_domain, query_string
from sentry.web.client_config import get_client_config
from sentry.web.frontend.base import (
BaseView,
ControlSiloOrganizationView,
all_silo_view,
control_silo_view,
)
from sentry.web.frontend.base import BaseView, ControlSiloOrganizationView, control_silo_view
from sentry.web.helpers import render_to_response

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -194,7 +189,7 @@ def handle_react(

# While most of our HTML views are rendered in control silo,
# our tests frequently redirect to HTML views
@all_silo_view
@control_silo_view
class ReactPageView(ControlSiloOrganizationView, ReactMixin):
def handle_auth_required(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# If user is a superuser (but not active, because otherwise this method would never be called)
Expand All @@ -221,7 +216,7 @@ def handle(self, request: HttpRequest, organization, **kwargs) -> HttpResponse:
return self.handle_react(request, organization=organization)


@all_silo_view
@control_silo_view
class GenericReactPageView(BaseView, ReactMixin):
def handle(self, request: HttpRequest, **kwargs) -> HttpResponse:
return self.handle_react(request, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/web/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -1089,7 +1089,7 @@
re_path(
r"^(?P<organization_slug>[^/]+)/projects/(?P<project_id_or_slug>[^/]+)/events/(?P<client_event_id>[^/]+)/$",
ProjectEventRedirect.as_view(),
name="sentry-project-event-redirect",
name="sentry-organization-project-event-redirect",
),
re_path(
r"^(?P<organization_slug>[^/]+)/api-keys/$",
Expand Down
4 changes: 0 additions & 4 deletions static/app/data/controlsiloUrlPatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ const patterns: RegExp[] = [
new RegExp('^api/0/tempest-ips/$'),
new RegExp('^api/0/secret-scanning/github/$'),
new RegExp('^api/hooks/mailgun/inbound/'),
new RegExp('^auth-v2/'),
new RegExp('^oauth/authorize/$'),
new RegExp('^oauth/token/$'),
new RegExp('^oauth/userinfo/$'),
Expand Down Expand Up @@ -194,10 +193,8 @@ const patterns: RegExp[] = [
new RegExp('^auth/sso/account/settings/social/associate/complete/[^/]+/$'),
new RegExp('^out/$'),
new RegExp('^settings/organization/auth/configure/$'),
new RegExp('^disabled-member/'),
new RegExp('^organizations/[^/]+/auth/configure/$'),
new RegExp('^organizations/[^/]+/integrations/[^/]+/setup/$'),
new RegExp('^organizations/[^/]+/disabled-member/$'),
new RegExp('^avatar/[^/]+/$'),
new RegExp('^sentry-app-avatar/[^/]+/$'),
new RegExp('^doc-integration-avatar/[^/]+/$'),
Expand Down Expand Up @@ -229,7 +226,6 @@ const patterns: RegExp[] = [
new RegExp('^extensions/discord/configure/$'),
new RegExp('^extensions/discord/link-identity/[^/]+/$'),
new RegExp('^extensions/discord/unlink-identity/[^/]+/$'),
new RegExp('^share/(?:group|issue)/[^/]+/$'),
];

export default patterns;
19 changes: 14 additions & 5 deletions tests/sentry/core/endpoints/test_organization_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
from sentry.silo.base import SiloMode
from sentry.testutils.cases import APITestCase, TwoFactorAPITestCase
from sentry.testutils.hybrid_cloud import HybridCloudTestMixin
from sentry.testutils.silo import assume_test_silo_mode, create_test_regions, region_silo_test
from sentry.testutils.silo import (
assume_test_silo_mode,
control_silo_test,
create_test_regions,
region_silo_test,
)
from sentry.users.models.authenticator import Authenticator
from sentry.utils.slug import ORG_SLUG_PATTERN

Expand Down Expand Up @@ -328,7 +333,7 @@ def test_streamline_only_is_true(self) -> None:


@region_silo_test(regions=create_test_regions("de", "us"))
class OrganizationsCreateInRegionTest(OrganizationIndexTest, HybridCloudTestMixin):
class OrganizationsCreateInRegionTest(OrganizationIndexTest):
method = "post"

@override_settings(SENTRY_MONOLITH_REGION="us", SENTRY_REGION="de")
Expand All @@ -348,7 +353,9 @@ def test_success(self) -> None:
assert mapping.region_name == "de"


@control_silo_test
class OrganizationIndex2faTest(TwoFactorAPITestCase):
# This is the HTML view, not an API endpoint
endpoint = "sentry-organization-home"

def setUp(self) -> None:
Expand All @@ -365,8 +372,7 @@ def test_preexisting_members_must_enable_2fa(self) -> None:
self.login_as(self.no_2fa_user)
self.assert_redirected_to_2fa()

with assume_test_silo_mode(SiloMode.CONTROL):
TotpInterface().enroll(self.no_2fa_user)
TotpInterface().enroll(self.no_2fa_user)
self.get_success_response(self.org_2fa.slug)

def test_new_member_must_enable_2fa(self) -> None:
Expand Down Expand Up @@ -396,7 +402,9 @@ def test_superuser_can_access_org_home(self) -> None:
self.get_success_response(self.org_2fa.slug)


@control_silo_test
class OrganizationIndexMemberLimitTest(APITestCase):
# This is a react view, not an API endpoint
endpoint = "sentry-organization-index"

def setup_user(self, is_superuser=False):
Expand All @@ -417,4 +425,5 @@ def test_member_limit_redirect(self) -> None:

def test_member_limit_superuser_no_redirect(self) -> None:
self.setup_user(is_superuser=True)
self.get_success_response(self.organization.slug, status_code=200)
response = self.get_success_response(self.organization.slug, status_code=200)
assert response.headers["Content-Type"] == "text/html"
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,14 @@ def test_write_file(self) -> None:
tf.seek(0)
result = tf.read().decode("utf8")
assert "This is generated code" in result
assert "new RegExp('^api/0/users/$')," in result
assert "const patterns" in result
assert "export default patterns;" in result
assert "new RegExp('^api/0/users/$')," in result
# Wizard is an HTML view that gets POST from UI code.
assert "new RegExp('^account/settings/wizard/[^/]+/$')," in result
# Views that render react application (like /dashboards)
# are not be used by UI code, and shouldn't be in the URL list
assert "new RegExp('^dashboards" not in result

def test_no_missing_urls(self) -> None:
pattern_file = "static/app/data/controlsiloUrlPatterns.ts"
Expand Down
19 changes: 13 additions & 6 deletions tests/sentry/middleware/test_customer_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@

from sentry.api.base import Endpoint
from sentry.middleware.customer_domain import CustomerDomainMiddleware
from sentry.silo.base import SiloMode
from sentry.testutils.cases import APITestCase, TestCase
from sentry.testutils.helpers import with_feature
from sentry.testutils.silo import all_silo_test, create_test_regions, no_silo_test
from sentry.testutils.silo import (
all_silo_test,
assume_test_silo_mode,
create_test_regions,
no_silo_test,
)
from sentry.web.frontend.auth_logout import AuthLogoutView


Expand Down Expand Up @@ -162,11 +168,12 @@ def test_billing_route(self) -> None:
self.login_as(user=non_staff_user)
self.create_organization(name="albertos-apples", owner=non_staff_user)

response = self.client.get(
"/settings/billing/overview/",
data={"querystring": "value"},
follow=True,
)
with assume_test_silo_mode(SiloMode.CONTROL):
response = self.client.get(
"/settings/billing/overview/",
data={"querystring": "value"},
follow=True,
)
assert response.status_code == 200
assert response.redirect_chain == [
("http://albertos-apples.testserver/settings/billing/overview/?querystring=value", 302)
Expand Down
3 changes: 2 additions & 1 deletion tests/sentry/web/frontend/test_project_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def test_redirect_to_event(self) -> None:
self.assertRedirects(
resp,
f"http://testserver/organizations/{self.org.slug}/issues/{self.event.group_id}/events/{self.event.event_id}/",
fetch_redirect_response=False,
)

def test_event_not_found(self) -> None:
Expand Down Expand Up @@ -79,7 +80,7 @@ def test_redirect_to_event_customer_domain(self) -> None:
self.org.refresh_from_db()
resp = self.client.get(
reverse(
"sentry-project-event-redirect",
"sentry-organization-project-event-redirect",
args=[self.org.slug, self.project.slug, self.event.event_id],
)
)
Expand Down
Loading