diff --git a/src/sentry/web/frontend/accept_organization_invite_redirect.py b/src/sentry/web/frontend/accept_organization_invite_redirect.py new file mode 100644 index 00000000000000..cd92b6c6f51c47 --- /dev/null +++ b/src/sentry/web/frontend/accept_organization_invite_redirect.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from django.http import HttpRequest, HttpResponse, HttpResponseRedirect +from django.urls import reverse + +from sentry.api.endpoints.accept_organization_invite import get_invite_state +from sentry.api.invite_helper import ApiInviteHelper +from sentry.demo_mode.utils import is_demo_user +from sentry.utils.http import query_string +from sentry.web.frontend.react_page import GenericReactPageView + + +# TODO(cells): Temporary redirect to support previous invitations. Remove after May 8th +class AcceptOrganizationInviteRedirectView(GenericReactPageView): + auth_required = False + + def handle(self, request: HttpRequest, **kwargs) -> HttpResponse: + member_id: str = kwargs["member_id"] + token: str = kwargs["token"] + if request.user.is_authenticated and not is_demo_user(request.user): + user_id: int | None = request.user.id + else: + user_id = None + + invite_context = get_invite_state( + member_id=int(member_id), + organization_id_or_slug=None, + user_id=user_id, + request=request, + ) + if invite_context is None: + return self.handle_react(request, **kwargs) + + helper = ApiInviteHelper(request=request, token=token, invite_context=invite_context) + if not helper.valid_token: + return self.handle_react(request, **kwargs) + + redirect_url = reverse( + "sentry-organization-accept-invite", + kwargs={ + "organization_slug": invite_context.organization.slug, + "member_id": member_id, + "token": token, + }, + ) + return HttpResponseRedirect(f"{redirect_url}{query_string(request)}") diff --git a/src/sentry/web/urls.py b/src/sentry/web/urls.py index 3790497227bd18..e118becbffd191 100644 --- a/src/sentry/web/urls.py +++ b/src/sentry/web/urls.py @@ -23,6 +23,9 @@ from sentry.users.web.user_avatar import UserAvatarPhotoView from sentry.web import api from sentry.web.frontend import csrf_failure, generic +from sentry.web.frontend.accept_organization_invite_redirect import ( + AcceptOrganizationInviteRedirectView, +) from sentry.web.frontend.auth_channel_login import AuthChannelLoginView from sentry.web.frontend.auth_close import AuthCloseView from sentry.web.frontend.auth_login import AuthLoginView @@ -554,7 +557,7 @@ ), re_path( r"^accept/(?P\d+)/(?P\w+)/$", - GenericReactPageView.as_view(auth_required=False), + AcceptOrganizationInviteRedirectView.as_view(), name="sentry-accept-invite", ), re_path( diff --git a/tests/sentry/web/frontend/test_accept_organization_invite_redirect.py b/tests/sentry/web/frontend/test_accept_organization_invite_redirect.py new file mode 100644 index 00000000000000..16945ab37c8712 --- /dev/null +++ b/tests/sentry/web/frontend/test_accept_organization_invite_redirect.py @@ -0,0 +1,47 @@ +from django.urls import reverse + +from sentry.testutils.cases import TestCase +from sentry.testutils.silo import control_silo_test + + +@control_silo_test +class AcceptOrganizationInviteRedirectViewTest(TestCase): + def test_redirects_legacy_invite_to_org_scoped_route(self) -> None: + organization = self.create_organization() + member = self.create_member( + organization=organization, email="newuser@example.com", token="abc" + ) + + response = self.client.get( + reverse("sentry-accept-invite", args=[member.id, member.token]) + "?referrer=email" + ) + + assert response.status_code == 302 + assert response["Location"] == ( + reverse( + "sentry-organization-accept-invite", + kwargs={ + "organization_slug": organization.slug, + "member_id": member.id, + "token": member.token, + }, + ) + + "?referrer=email" + ) + + def test_invalid_token_does_not_leak_org_slug(self) -> None: + organization = self.create_organization() + member = self.create_member(organization=organization, email="newuser@example.com") + + response = self.client.get( + reverse("sentry-accept-invite", args=[member.id, "invalidtoken"]) + ) + + assert response.status_code == 200 + self.assertTemplateUsed(response, "sentry/base-react.html") + + def test_unresolved_legacy_invite_falls_back_to_react_page(self) -> None: + response = self.client.get(reverse("sentry-accept-invite", args=[123456, "invalidtoken"])) + + assert response.status_code == 200 + self.assertTemplateUsed(response, "sentry/base-react.html")