auth: Only automatically redirect for same domain redirects.

If the `deactivated_redirect` belongs to the same domain as
`EXTERNAL_HOST`, automatically redirect, otherwise just point
user to the new URL.
This commit is contained in:
Aman Agrawal
2025-06-18 19:38:48 +05:30
committed by Tim Abbott
parent ba32e732c7
commit 9b15dce1b2
6 changed files with 71 additions and 19 deletions

View File

@@ -605,6 +605,7 @@ def support(
if parse_result.port:
hostname = f"{hostname}:{parse_result.port}"
subdomain = get_subdomain_from_hostname(hostname)
assert subdomain is not None
with suppress(Realm.DoesNotExist):
realms.add(get_realm(subdomain))
except ValidationError:

View File

@@ -16,12 +16,25 @@
<div class="inline-block">
<div class="get-started">
<h1>{{ _("Deactivated organization") }}</h1>
{% if deactivated_redirect %}
<h1>{{ _("Organization moved") }}</h1>
{% else %}
<h1>{{ _("Deactivated organization") }}</h1>
{% endif %}
</div>
<div class="white-box deactivated-realm-container">
<p>
{% if realm_data_deleted %}
{% if deactivated_redirect %}
{% trans %}
This organization has moved to <a href="{{ deactivated_redirect }}">{{ deactivated_redirect }}</a>.
{% endtrans %}
{% if auto_redirect_to %}
{% trans %}
This page will automatically redirect to the <a href="{{ auto_redirect_to }}" id="deactivated-org-auto-redirect">new URL</a> in <span id="deactivated-org-auto-redirect-countdown">5</span> seconds.
{% endtrans %}
{% endif %}
{% elif realm_data_deleted %}
{{ _("This organization has been deactivated, and all organization data has been deleted.") }}
{% if corporate_enabled %}
{% trans %}

View File

@@ -493,4 +493,19 @@ $(() => {
$("#slack-access-token").on("input", () => {
$("#update-slack-access-token").show();
});
if ($("a#deactivated-org-auto-redirect").length > 0) {
// This is a special case for the deactivated organization page,
// where we want to redirect to the login page after 5 seconds.
const interval_id = setInterval(() => {
const $countdown_elt = $("#deactivated-org-auto-redirect-countdown");
const current_countdown = Number($countdown_elt.text());
if (current_countdown > 0) {
$countdown_elt.text((current_countdown - 1).toString());
} else {
window.location.href = $("a#deactivated-org-auto-redirect").attr("href")!;
clearInterval(interval_id);
}
}, 1000);
}
});

View File

@@ -22,22 +22,27 @@ def get_subdomain(request: HttpRequest) -> str:
# compatibility with older versions of Zulip, so that's a start.
host = request.get_host().lower()
return get_subdomain_from_hostname(host)
subdomain = get_subdomain_from_hostname(host)
assert subdomain is not None
return subdomain
def get_subdomain_from_hostname(host: str) -> str:
def get_subdomain_from_hostname(
host: str, default_subdomain: str | None = Realm.SUBDOMAIN_FOR_ROOT_DOMAIN
) -> str | None:
# Set `default_subdomain` as None to check if a valid subdomain was found.
m = re.search(rf"\.{settings.EXTERNAL_HOST}(:\d+)?$", host)
if m:
subdomain = host[: m.start()]
if subdomain in settings.ROOT_SUBDOMAIN_ALIASES:
return Realm.SUBDOMAIN_FOR_ROOT_DOMAIN
return default_subdomain
return subdomain
for subdomain, realm_host in settings.REALM_HOSTS.items():
if re.search(rf"^{realm_host}(:\d+)?$", host):
return subdomain
return Realm.SUBDOMAIN_FOR_ROOT_DOMAIN
return default_subdomain
def is_subdomain_root_or_alias(request: HttpRequest) -> bool:

View File

@@ -191,14 +191,19 @@ class DeactivationNoticeTestCase(ZulipTestCase):
realm.save(update_fields=["deactivated", "deactivated_redirect"])
result = self.client_get("/login/", follow=True)
self.assertIn(result.request.get("SERVER_NAME"), ["example.zulipchat.com"])
self.assert_in_success_response(
['href="http://example.zulipchat.com/" id="deactivated-org-auto-redirect"'], result
)
def test_deactivation_notice_when_realm_subdomain_is_changed(self) -> None:
realm = get_realm("zulip")
do_change_realm_subdomain(realm, "new-subdomain-name", acting_user=None)
result = self.client_get("/login/", follow=True)
self.assertIn(result.request.get("SERVER_NAME"), ["new-subdomain-name.testserver"])
self.assert_in_success_response(
['href="http://new-subdomain-name.testserver/" id="deactivated-org-auto-redirect"'],
result,
)
def test_no_deactivation_notice_with_no_redirect(self) -> None:
realm = get_realm("zulip")
@@ -220,12 +225,16 @@ class DeactivationNoticeTestCase(ZulipTestCase):
do_change_realm_subdomain(realm, "new-name-1", acting_user=None)
result = self.client_get("/login/", follow=True)
self.assertIn(result.request.get("SERVER_NAME"), ["new-name-1.testserver"])
self.assert_in_success_response(
['href="http://new-name-1.testserver/" id="deactivated-org-auto-redirect"'], result
)
realm = get_realm("new-name-1")
do_change_realm_subdomain(realm, "new-name-2", acting_user=None)
result = self.client_get("/login/", follow=True)
self.assertIn(result.request.get("SERVER_NAME"), ["new-name-2.testserver"])
self.assert_in_success_response(
['href="http://new-name-2.testserver/" id="deactivated-org-auto-redirect"'], result
)
def test_deactivation_notice_when_deactivated_and_scrubbed(self) -> None:
# We expect system bot messages when scrubbing a realm.

View File

@@ -3,7 +3,7 @@ import secrets
from collections.abc import Callable, Mapping
from functools import wraps
from typing import TYPE_CHECKING, Any, Concatenate, TypeAlias, cast
from urllib.parse import urlencode, urljoin
from urllib.parse import urlencode, urljoin, urlsplit
import jwt
import orjson
@@ -63,7 +63,11 @@ from zerver.lib.realm_icon import realm_icon_url
from zerver.lib.request import RequestNotes
from zerver.lib.response import json_success
from zerver.lib.sessions import set_expirable_session_var
from zerver.lib.subdomains import get_subdomain, is_subdomain_root_or_alias
from zerver.lib.subdomains import (
get_subdomain,
get_subdomain_from_hostname,
is_subdomain_root_or_alias,
)
from zerver.lib.typed_endpoint import typed_endpoint
from zerver.lib.url_encoding import append_url_query_string
from zerver.lib.user_agent import parse_user_agent
@@ -820,16 +824,21 @@ def redirect_to_misconfigured_ldap_notice(request: HttpRequest, error_type: int)
def show_deactivation_notice(request: HttpRequest, next: str = "/") -> HttpResponse:
realm = get_realm_from_request(request)
if realm and realm.deactivated:
if realm.deactivated_redirect is not None:
# URL hash is automatically preserved by the browser.
# See https://stackoverflow.com/a/5283739
redirect_to = get_safe_redirect_to(next, realm.deactivated_redirect)
return HttpResponseRedirect(redirect_to)
realm_data_scrubbed = RealmAuditLog.objects.filter(
realm=realm, event_type=AuditLogEventType.REALM_SCRUBBED
).exists()
context = {"realm_data_deleted": realm_data_scrubbed}
context = {
"realm_data_deleted": realm_data_scrubbed,
"deactivated_redirect": realm.deactivated_redirect,
}
if realm.deactivated_redirect is not None:
split = urlsplit(realm.deactivated_redirect)
host = f"{split.scheme}://{split.netloc}"
# If the redirect is in the same domain, do an automatic redirect.
if get_subdomain_from_hostname(host) is not None:
redirect_to = get_safe_redirect_to(next, realm.deactivated_redirect)
context["auto_redirect_to"] = redirect_to
return render(request, "zerver/deactivated.html", context=context)
return HttpResponseRedirect(reverse("login_page"))