Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(hybrid-cloud): Redirect to org restoration page for customer domains #45159

Merged
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
52 changes: 34 additions & 18 deletions src/sentry/web/frontend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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]
Copy link
Member Author

Choose a reason for hiding this comment

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

I've adjusted the organization type here, since handle_permission_required() will be called when an organization is not visible.

if organization and self.needs_sso(request, organization):
logger.info(
"access.must-sso",
extra={"organization_id": organization.id, "user_id": request.user.id},
Expand All @@ -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)

Expand Down
19 changes: 19 additions & 0 deletions tests/sentry/web/frontend/test_auth_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
54 changes: 54 additions & 0 deletions tests/sentry/web/frontend/test_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
52 changes: 52 additions & 0 deletions tests/sentry/web/frontend/test_react_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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