diff --git a/src/sentry/web/frontend/base.py b/src/sentry/web/frontend/base.py index 01ae22c75c0746..729d048d846d5e 100644 --- a/src/sentry/web/frontend/base.py +++ b/src/sentry/web/frontend/base.py @@ -18,11 +18,12 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry import options from sentry.api.serializers import serialize from sentry.api.utils import generate_organization_url, is_member_disabled_from_limit from sentry.auth import access from sentry.auth.superuser import is_active_superuser -from sentry.models import Organization, Project, ProjectStatus, Team, TeamStatus +from sentry.models import Organization, OrganizationStatus, Project, ProjectStatus, Team, TeamStatus from sentry.models.avatars.base import AvatarBase from sentry.models.user import User from sentry.services.hybrid_cloud.organization import ( @@ -215,28 +216,28 @@ def redirect_to_org(self: _HasRespond, request: Request) -> HttpResponse: elif not features.has("organizations:create"): return self.respond("sentry/no-organization-access.html", status=403) else: - org_exists = False - url = "/organizations/new/" + url = reverse("sentry-organization-create") if using_customer_domain: url = absolute_uri(url) if using_customer_domain and request.user and request.user.is_authenticated: - organizations = organization_service.get_organizations( - user_id=request.user.id, scope=None, only_visible=True - ) requesting_org_slug = request.subdomain - org_exists = ( - organization_service.check_organization_by_slug( - slug=requesting_org_slug, only_visible=True - ) - is not None + org_context = organization_service.get_organization_by_slug( + slug=requesting_org_slug, only_visible=False, user_id=request.user.id ) - if org_exists and organizations: - # If the user is a superuser, redirect them to the org's landing page (e.g. issues page) - if request.user.is_superuser: - url = Organization.get_url(requesting_org_slug) + if org_context and org_context.organization: + if org_context.organization.status == OrganizationStatus.PENDING_DELETION: + url = reverse("sentry-customer-domain-restore-organization") + elif org_context.organization.status == OrganizationStatus.DELETION_IN_PROGRESS: + url_prefix = options.get("system.url-prefix") + url = reverse("sentry-organization-create") + return HttpResponseRedirect(absolute_uri(url, url_prefix=url_prefix)) else: - url = reverse("sentry-auth-organization", args=[requesting_org_slug]) + # If the user is a superuser, redirect them to the org's landing page (e.g. issues page) + if request.user.is_superuser: + url = Organization.get_url(requesting_org_slug) + else: + url = reverse("sentry-auth-organization", args=[requesting_org_slug]) url_prefix = generate_organization_url(requesting_org_slug) url = absolute_uri(url, url_prefix=url_prefix) @@ -487,8 +488,8 @@ def is_auth_required( return False - def handle_permission_required(self, request: Request, organization: Organization | RpcOrganization, *args: Any, **kwargs: Any) -> HttpResponse: # type: ignore[override] - if self.needs_sso(request, organization): + def handle_permission_required(self, request: Request, organization: Organization | RpcOrganization | None, *args: Any, **kwargs: Any) -> HttpResponse: # type: ignore[override] + if organization and self.needs_sso(request, organization): logger.info( "access.must-sso", extra={"organization_id": organization.id, "user_id": request.user.id}, @@ -506,6 +507,21 @@ def handle_permission_required(self, request: Request, organization: Organizatio redirect_uri = make_login_link_with_redirect(path, after_login_redirect) else: + if is_using_customer_domain(request): + # In the customer domain world, if an organziation is pending deletion, we redirect the user to the + # organization restoration page. + org_context = organization_service.get_organization_by_slug( + slug=request.subdomain, only_visible=False, user_id=request.user.id + ) + if org_context and org_context.member: + if org_context.organization.status == OrganizationStatus.PENDING_DELETION: + url_base = generate_organization_url(org_context.organization.slug) + restore_org_path = reverse("sentry-customer-domain-restore-organization") + return self.redirect(f"{url_base}{restore_org_path}") + elif org_context.organization.status == OrganizationStatus.DELETION_IN_PROGRESS: + url_base = options.get("system.url-prefix") + create_org_path = reverse("sentry-organization-create") + return self.redirect(f"{url_base}{create_org_path}") redirect_uri = self.get_no_permission_url(request, *args, **kwargs) return self.redirect(redirect_uri) diff --git a/tests/sentry/web/frontend/test_auth_login.py b/tests/sentry/web/frontend/test_auth_login.py index db2cc4ce5760c2..31ec1f1630f166 100644 --- a/tests/sentry/web/frontend/test_auth_login.py +++ b/tests/sentry/web/frontend/test_auth_login.py @@ -610,6 +610,25 @@ def test_login_valid_credentials_orgless(self): follow=True, ) + assert resp.status_code == 200 + assert resp.redirect_chain == [ + ("http://albertos-apples.testserver/auth/login/", 302), + ("http://albertos-apples.testserver/auth/login/albertos-apples/", 302), + ] + + def test_login_valid_credentials_org_does_not_exist(self): + user = self.create_user() + with override_settings(MIDDLEWARE=tuple(provision_middleware())): + # load it once for test cookie + self.client.get(self.path) + + resp = self.client.post( + self.path, + {"username": user.username, "password": "admin", "op": "login"}, + SERVER_NAME="albertos-apples.testserver", + follow=True, + ) + assert resp.status_code == 200 assert resp.redirect_chain == [ ("http://albertos-apples.testserver/auth/login/", 302), diff --git a/tests/sentry/web/frontend/test_home.py b/tests/sentry/web/frontend/test_home.py index a79742966b427f..fea2f0daceb7de 100644 --- a/tests/sentry/web/frontend/test_home.py +++ b/tests/sentry/web/frontend/test_home.py @@ -2,6 +2,7 @@ from django.urls import reverse +from sentry.models import OrganizationStatus from sentry.testutils import TestCase from sentry.utils import json from sentry.utils.client_state import get_client_state_key, get_redis_client @@ -51,3 +52,56 @@ def test_redirect_to_onboarding(self): get_redis_client().set(key, json.dumps({"state": "started", "url": "select-platform/"})) resp = self.client.get(self.path) self.assertRedirects(resp, f"/onboarding/{org.slug}/select-platform/") + + def test_customer_domain(self): + org = self.create_organization(owner=self.user) + + self.login_as(self.user) + + with self.feature({"organizations:customer-domains": [org.slug]}): + response = self.client.get( + "/", + SERVER_NAME=f"{org.slug}.testserver", + follow=True, + ) + assert response.status_code == 200 + assert response.redirect_chain == [ + (f"http://{org.slug}.testserver/issues/", 302), + ] + assert self.client.session["activeorg"] == org.slug + + def test_customer_domain_org_pending_deletion(self): + org = self.create_organization(owner=self.user, status=OrganizationStatus.PENDING_DELETION) + + self.login_as(self.user) + + with self.feature({"organizations:customer-domains": [org.slug]}): + response = self.client.get( + "/", + SERVER_NAME=f"{org.slug}.testserver", + follow=True, + ) + assert response.status_code == 200 + assert response.redirect_chain == [ + (f"http://{org.slug}.testserver/restore/", 302), + ] + assert "activeorg" not in self.client.session + + def test_customer_domain_org_deletion_in_progress(self): + org = self.create_organization( + owner=self.user, status=OrganizationStatus.DELETION_IN_PROGRESS + ) + + self.login_as(self.user) + + with self.feature({"organizations:customer-domains": [org.slug]}): + response = self.client.get( + "/", + SERVER_NAME=f"{org.slug}.testserver", + follow=True, + ) + assert response.status_code == 200 + assert response.redirect_chain == [ + ("http://testserver/organizations/new/", 302), + ] + assert "activeorg" not in self.client.session diff --git a/tests/sentry/web/frontend/test_react_page.py b/tests/sentry/web/frontend/test_react_page.py index e763b9be2310a9..c00e00c8f5d80b 100644 --- a/tests/sentry/web/frontend/test_react_page.py +++ b/tests/sentry/web/frontend/test_react_page.py @@ -2,6 +2,7 @@ from django.urls import URLResolver, get_resolver, reverse +from sentry.models import OrganizationStatus from sentry.testutils import TestCase from sentry.web.frontend.react_page import NON_CUSTOMER_DOMAIN_URL_NAMES, ReactMixin @@ -288,3 +289,54 @@ def test_customer_domain_superuser(self): assert response.redirect_chain == [ (f"http://{other_org.slug}.testserver/issues/", 302), ] + + def test_customer_domain_loads(self): + org = self.create_organization(owner=self.user, status=OrganizationStatus.ACTIVE) + + self.login_as(self.user) + + with self.feature({"organizations:customer-domains": [org.slug]}): + response = self.client.get( + "/issues/", + SERVER_NAME=f"{org.slug}.testserver", + ) + assert response.status_code == 200 + self.assertTemplateUsed(response, "sentry/base-react.html") + assert response.context["request"] + assert self.client.session["activeorg"] == org.slug + + def test_customer_domain_org_pending_deletion(self): + org = self.create_organization(owner=self.user, status=OrganizationStatus.PENDING_DELETION) + + self.login_as(self.user) + + with self.feature({"organizations:customer-domains": [org.slug]}): + response = self.client.get( + "/issues/", + SERVER_NAME=f"{org.slug}.testserver", + follow=True, + ) + assert response.status_code == 200 + assert response.redirect_chain == [ + (f"http://{org.slug}.testserver/restore/", 302), + ] + assert "activeorg" not in self.client.session + + def test_customer_domain_org_deletion_in_progress(self): + org = self.create_organization( + owner=self.user, status=OrganizationStatus.DELETION_IN_PROGRESS + ) + + self.login_as(self.user) + + with self.feature({"organizations:customer-domains": [org.slug]}): + response = self.client.get( + "/issues/", + SERVER_NAME=f"{org.slug}.testserver", + follow=True, + ) + assert response.status_code == 200 + assert response.redirect_chain == [ + ("http://testserver/organizations/new/", 302), + ] + assert "activeorg" not in self.client.session