mirror of https://github.com/zulip/zulip.git
home: For web public realms, skip login for spectators.
To provide a smoother experience of accessing a web public stream, we don't ask user to login unless user directly requests a `/login` URL. Fixes #21690.
This commit is contained in:
parent
bd2dc7358b
commit
4e08c737ca
|
@ -34,7 +34,6 @@ page can be easily identified in it's respective JavaScript file. -->
|
||||||
<div class="login-social">
|
<div class="login-social">
|
||||||
<form class="anonymous_access_form form-inline" action="/" method="post">
|
<form class="anonymous_access_form form-inline" action="/" method="post">
|
||||||
{{ csrf_input }}
|
{{ csrf_input }}
|
||||||
<input type="hidden" name="prefers_web_public_view" value="true" />
|
|
||||||
<input type="hidden" name="next" value="{{ next }}" />
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
<button class="full-width">
|
<button class="full-width">
|
||||||
{{ _('Access without an account') }}
|
{{ _('Access without an account') }}
|
||||||
|
|
|
@ -1,23 +1,9 @@
|
||||||
HTTP/2 302
|
HTTP/2 404
|
||||||
server: {nginx_version_string}
|
server: {nginx_version_string}
|
||||||
content-type: text/html; charset=utf-8
|
content-type: text/html; charset=utf-8
|
||||||
location: /login/
|
vary: Accept-Encoding
|
||||||
vary: Accept-Language, Cookie
|
vary: Accept-Language, Cookie
|
||||||
content-language: en
|
content-language: en
|
||||||
strict-transport-security: max-age=15768000
|
strict-transport-security: max-age=15768000
|
||||||
x-frame-options: DENY
|
x-frame-options: DENY
|
||||||
x-content-type-options: nosniff
|
|
||||||
x-xss-protection: 1; mode=block
|
|
||||||
|
|
||||||
HTTP/2 200
|
|
||||||
server: {nginx_version_string}
|
|
||||||
content-type: text/html; charset=utf-8
|
|
||||||
vary: Accept-Encoding
|
|
||||||
cache-control: max-age=0, no-cache, no-store, must-revalidate, private
|
|
||||||
vary: Cookie, Accept-Language
|
|
||||||
content-language: en
|
|
||||||
strict-transport-security: max-age=15768000
|
|
||||||
x-frame-options: DENY
|
|
||||||
x-content-type-options: nosniff
|
|
||||||
x-xss-protection: 1; mode=block
|
|
||||||
|
|
||||||
|
|
|
@ -2170,7 +2170,7 @@ class TestRequestNotes(ZulipTestCase):
|
||||||
# no realm can be set on the request notes.
|
# no realm can be set on the request notes.
|
||||||
with mock.patch("zerver.views.home.zulip_login_required", lambda f: mock_home(None)):
|
with mock.patch("zerver.views.home.zulip_login_required", lambda f: mock_home(None)):
|
||||||
result = self.client_get("/", subdomain="")
|
result = self.client_get("/", subdomain="")
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 404)
|
||||||
|
|
||||||
root_subdomain_realm = do_create_realm("", "Root Domain")
|
root_subdomain_realm = do_create_realm("", "Root Domain")
|
||||||
# Now test that that realm does get set, if it exists, for requests
|
# Now test that that realm does get set, if it exists, for requests
|
||||||
|
|
|
@ -14,7 +14,7 @@ from django.utils.timezone import now as timezone_now
|
||||||
|
|
||||||
from corporate.models import Customer, CustomerPlan
|
from corporate.models import Customer, CustomerPlan
|
||||||
from zerver.actions.create_user import do_create_user
|
from zerver.actions.create_user import do_create_user
|
||||||
from zerver.actions.realm_settings import do_change_realm_plan_type
|
from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_realm_property
|
||||||
from zerver.actions.users import change_user_is_active
|
from zerver.actions.users import change_user_is_active
|
||||||
from zerver.lib.compatibility import LAST_SERVER_UPGRADE_TIME, is_outdated_server
|
from zerver.lib.compatibility import LAST_SERVER_UPGRADE_TIME, is_outdated_server
|
||||||
from zerver.lib.home import (
|
from zerver.lib.home import (
|
||||||
|
@ -248,7 +248,7 @@ class HomeTest(ZulipTestCase):
|
||||||
set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"}
|
set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assert_length(queries, 45)
|
self.assert_length(queries, 46)
|
||||||
self.assert_length(cache_mock.call_args_list, 5)
|
self.assert_length(cache_mock.call_args_list, 5)
|
||||||
|
|
||||||
html = result.content.decode()
|
html = result.content.decode()
|
||||||
|
@ -311,46 +311,22 @@ class HomeTest(ZulipTestCase):
|
||||||
self.assertEqual(set(actual_keys), set(expected_keys))
|
self.assertEqual(set(actual_keys), set(expected_keys))
|
||||||
|
|
||||||
def test_logged_out_home(self) -> None:
|
def test_logged_out_home(self) -> None:
|
||||||
# Redirect to login on first request.
|
realm = get_realm("zulip")
|
||||||
|
do_set_realm_property(realm, "enable_spectator_access", False, acting_user=None)
|
||||||
|
|
||||||
|
# Redirect to login if spectator access is disabled.
|
||||||
result = self.client_get("/")
|
result = self.client_get("/")
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
self.assertEqual(result.url, "/login/")
|
self.assertEqual(result.url, "/login/")
|
||||||
|
|
||||||
# Tell server that user wants to log in anonymously
|
# Load webapp directly if spectator access is enabled.
|
||||||
# Redirects to load webapp.
|
do_set_realm_property(realm, "enable_spectator_access", True, acting_user=None)
|
||||||
realm = get_realm("zulip")
|
|
||||||
result = self.client_post("/", {"prefers_web_public_view": "true"})
|
|
||||||
self.assertEqual(self.client.session.get("prefers_web_public_view"), True)
|
|
||||||
self.assertEqual(realm.enable_spectator_access, True)
|
|
||||||
self.assertEqual(result.status_code, 302)
|
|
||||||
self.assertEqual(result.url, "http://zulip.testserver")
|
|
||||||
|
|
||||||
# Disable spectator login. Since Realm.enable_spectator_access
|
|
||||||
# is False, the login should fail.
|
|
||||||
realm.enable_spectator_access = False
|
|
||||||
realm.save()
|
|
||||||
|
|
||||||
result = self.client_post("/", {"prefers_web_public_view": "true"})
|
|
||||||
self.assertEqual(self.client.session.get("prefers_web_public_view"), True)
|
|
||||||
self.assertEqual(realm.enable_spectator_access, False)
|
|
||||||
self.assertEqual(result.status_code, 302)
|
|
||||||
self.assertEqual(result.url, "/login/")
|
|
||||||
|
|
||||||
# Enable spectator login.
|
|
||||||
realm.enable_spectator_access = True
|
|
||||||
realm.save()
|
|
||||||
|
|
||||||
result = self.client_post("/", {"prefers_web_public_view": "true"})
|
|
||||||
self.assertEqual(self.client.session.get("prefers_web_public_view"), True)
|
|
||||||
self.assertEqual(realm.enable_spectator_access, True)
|
|
||||||
self.assertEqual(result.status_code, 302)
|
|
||||||
self.assertEqual(result.url, "http://zulip.testserver")
|
|
||||||
|
|
||||||
# Always load the web app from then on directly
|
|
||||||
result = self.client_get("/")
|
result = self.client_get("/")
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
# Check no unnecessary params are passed to spectators.
|
||||||
page_params = self._get_page_params(result)
|
page_params = self._get_page_params(result)
|
||||||
|
self.assertEqual(page_params["is_spectator"], True)
|
||||||
actual_keys = sorted(str(k) for k in page_params.keys())
|
actual_keys = sorted(str(k) for k in page_params.keys())
|
||||||
removed_keys = [
|
removed_keys = [
|
||||||
"custom_profile_field_types",
|
"custom_profile_field_types",
|
||||||
|
@ -361,7 +337,6 @@ class HomeTest(ZulipTestCase):
|
||||||
]
|
]
|
||||||
expected_keys = [i for i in self.expected_page_params_keys if i not in removed_keys]
|
expected_keys = [i for i in self.expected_page_params_keys if i not in removed_keys]
|
||||||
self.assertEqual(actual_keys, expected_keys)
|
self.assertEqual(actual_keys, expected_keys)
|
||||||
self.assertEqual(self.client.session.get("prefers_web_public_view"), True)
|
|
||||||
|
|
||||||
# Test information passed to client about users.
|
# Test information passed to client about users.
|
||||||
page_params = self._get_page_params(result)
|
page_params = self._get_page_params(result)
|
||||||
|
@ -384,11 +359,6 @@ class HomeTest(ZulipTestCase):
|
||||||
date_length = len("YYYY-MM-DD")
|
date_length = len("YYYY-MM-DD")
|
||||||
self.assert_length(page_params["realm_users"][0]["date_joined"], date_length)
|
self.assert_length(page_params["realm_users"][0]["date_joined"], date_length)
|
||||||
|
|
||||||
# Web-public session key should clear once user is logged in
|
|
||||||
self.login("hamlet")
|
|
||||||
self.client_get("/")
|
|
||||||
self.assertEqual(self.client.session.get("prefers_web_public_view"), None)
|
|
||||||
|
|
||||||
def test_home_under_2fa_without_otp_device(self) -> None:
|
def test_home_under_2fa_without_otp_device(self) -> None:
|
||||||
with self.settings(TWO_FACTOR_AUTHENTICATION_ENABLED=True):
|
with self.settings(TWO_FACTOR_AUTHENTICATION_ENABLED=True):
|
||||||
self.login("iago")
|
self.login("iago")
|
||||||
|
@ -421,7 +391,7 @@ class HomeTest(ZulipTestCase):
|
||||||
result = self._get_home_page()
|
result = self._get_home_page()
|
||||||
self.check_rendered_logged_in_app(result)
|
self.check_rendered_logged_in_app(result)
|
||||||
self.assert_length(cache_mock.call_args_list, 6)
|
self.assert_length(cache_mock.call_args_list, 6)
|
||||||
self.assert_length(queries, 42)
|
self.assert_length(queries, 43)
|
||||||
|
|
||||||
def test_num_queries_with_streams(self) -> None:
|
def test_num_queries_with_streams(self) -> None:
|
||||||
main_user = self.example_user("hamlet")
|
main_user = self.example_user("hamlet")
|
||||||
|
@ -452,7 +422,7 @@ class HomeTest(ZulipTestCase):
|
||||||
with queries_captured() as queries2:
|
with queries_captured() as queries2:
|
||||||
result = self._get_home_page()
|
result = self._get_home_page()
|
||||||
|
|
||||||
self.assert_length(queries2, 40)
|
self.assert_length(queries2, 41)
|
||||||
|
|
||||||
# Do a sanity check that our new streams were in the payload.
|
# Do a sanity check that our new streams were in the payload.
|
||||||
html = result.content.decode()
|
html = result.content.decode()
|
||||||
|
|
|
@ -4,6 +4,7 @@ from unittest import mock
|
||||||
|
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
|
|
||||||
|
from zerver.actions.realm_settings import do_set_realm_property
|
||||||
from zerver.actions.users import change_user_is_active
|
from zerver.actions.users import change_user_is_active
|
||||||
from zerver.lib.sessions import (
|
from zerver.lib.sessions import (
|
||||||
delete_all_deactivated_user_sessions,
|
delete_all_deactivated_user_sessions,
|
||||||
|
@ -28,8 +29,8 @@ class TestSessions(ZulipTestCase):
|
||||||
action()
|
action()
|
||||||
if expected_result:
|
if expected_result:
|
||||||
result = self.client_get("/", subdomain=realm.subdomain)
|
result = self.client_get("/", subdomain=realm.subdomain)
|
||||||
self.assertEqual(302, result.status_code)
|
self.assertEqual(200, result.status_code)
|
||||||
self.assertEqual("/login/", result.url)
|
self.assertTrue('is_spectator":true' in str(result.content))
|
||||||
else:
|
else:
|
||||||
self.assertIn("_auth_user_id", self.client.session)
|
self.assertIn("_auth_user_id", self.client.session)
|
||||||
|
|
||||||
|
@ -40,8 +41,8 @@ class TestSessions(ZulipTestCase):
|
||||||
for session in user_sessions(user_profile):
|
for session in user_sessions(user_profile):
|
||||||
delete_session(session)
|
delete_session(session)
|
||||||
result = self.client_get("/")
|
result = self.client_get("/")
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 200)
|
||||||
self.assertEqual(result.url, "/login/")
|
self.assertTrue('is_spectator":true' in str(result.content))
|
||||||
|
|
||||||
def test_delete_user_sessions(self) -> None:
|
def test_delete_user_sessions(self) -> None:
|
||||||
user_profile = self.example_user("hamlet")
|
user_profile = self.example_user("hamlet")
|
||||||
|
@ -77,8 +78,19 @@ class TestSessions(ZulipTestCase):
|
||||||
get_realm("zulip"),
|
get_realm("zulip"),
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
lear_realm = get_realm("lear")
|
||||||
|
do_set_realm_property(lear_realm, "enable_spectator_access", True, acting_user=None)
|
||||||
|
self.make_stream(
|
||||||
|
"web_public_stream",
|
||||||
|
realm=lear_realm,
|
||||||
|
is_web_public=True,
|
||||||
|
)
|
||||||
self.do_test_session(
|
self.do_test_session(
|
||||||
self.mit_user("sipbtest"), lambda: delete_all_user_sessions(), get_realm("zephyr"), True
|
self.lear_user("cordelia"),
|
||||||
|
lambda: delete_all_user_sessions(),
|
||||||
|
lear_realm,
|
||||||
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_delete_all_deactivated_user_sessions(self) -> None:
|
def test_delete_all_deactivated_user_sessions(self) -> None:
|
||||||
|
@ -89,8 +101,8 @@ class TestSessions(ZulipTestCase):
|
||||||
self.client_post("/accounts/logout/")
|
self.client_post("/accounts/logout/")
|
||||||
delete_all_deactivated_user_sessions()
|
delete_all_deactivated_user_sessions()
|
||||||
result = self.client_get("/")
|
result = self.client_get("/")
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 200)
|
||||||
self.assertEqual(result.url, "/login/")
|
self.assertTrue('is_spectator":true' in str(result.content))
|
||||||
|
|
||||||
# Test nothing happens to an active user's session
|
# Test nothing happens to an active user's session
|
||||||
self.login("othello")
|
self.login("othello")
|
||||||
|
@ -110,8 +122,8 @@ class TestSessions(ZulipTestCase):
|
||||||
[f"INFO:root:Deactivating session for deactivated user {user_profile_3.id}"],
|
[f"INFO:root:Deactivating session for deactivated user {user_profile_3.id}"],
|
||||||
)
|
)
|
||||||
result = self.client_get("/")
|
result = self.client_get("/")
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 200)
|
||||||
self.assertEqual(result.url, "/login/")
|
self.assertTrue('is_spectator":true' in str(result.content))
|
||||||
|
|
||||||
|
|
||||||
class TestExpirableSessionVars(ZulipTestCase):
|
class TestExpirableSessionVars(ZulipTestCase):
|
||||||
|
|
|
@ -43,9 +43,8 @@ class PublicURLTest(ZulipTestCase):
|
||||||
"/en/accounts/login/",
|
"/en/accounts/login/",
|
||||||
"/ru/accounts/login/",
|
"/ru/accounts/login/",
|
||||||
"/help/",
|
"/help/",
|
||||||
],
|
# Since web-public streams are enabled in this `zulip`
|
||||||
302: [
|
# instance, the public access experience is loaded directly.
|
||||||
# These 302 because they redirect to the spectator experience.
|
|
||||||
"/",
|
"/",
|
||||||
"/en/",
|
"/en/",
|
||||||
"/ru/",
|
"/ru/",
|
||||||
|
|
|
@ -79,7 +79,6 @@ def dev_direct_login(
|
||||||
realm = get_realm(subdomain)
|
realm = get_realm(subdomain)
|
||||||
|
|
||||||
if request.POST.get("prefers_web_public_view") == "Anonymous login":
|
if request.POST.get("prefers_web_public_view") == "Anonymous login":
|
||||||
request.session["prefers_web_public_view"] = True
|
|
||||||
redirect_to = get_safe_redirect_to(next, realm.uri)
|
redirect_to = get_safe_redirect_to(next, realm.uri)
|
||||||
return HttpResponseRedirect(redirect_to)
|
return HttpResponseRedirect(redirect_to)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.shortcuts import redirect, render
|
||||||
from django.utils.cache import patch_cache_control
|
from django.utils.cache import patch_cache_control
|
||||||
|
|
||||||
from zerver.actions.user_settings import do_change_tos_version
|
from zerver.actions.user_settings import do_change_tos_version
|
||||||
from zerver.context_processors import get_valid_realm_from_request
|
from zerver.context_processors import get_realm_from_request, get_valid_realm_from_request
|
||||||
from zerver.decorator import web_public_view, zulip_login_required
|
from zerver.decorator import web_public_view, zulip_login_required
|
||||||
from zerver.forms import ToSForm
|
from zerver.forms import ToSForm
|
||||||
from zerver.lib.compatibility import is_outdated_desktop_app, is_unsupported_browser
|
from zerver.lib.compatibility import is_outdated_desktop_app, is_unsupported_browser
|
||||||
|
@ -19,7 +19,6 @@ from zerver.lib.subdomains import get_subdomain
|
||||||
from zerver.lib.user_counts import realm_user_count
|
from zerver.lib.user_counts import realm_user_count
|
||||||
from zerver.lib.utils import statsd
|
from zerver.lib.utils import statsd
|
||||||
from zerver.models import PreregistrationUser, Realm, Stream, UserProfile
|
from zerver.models import PreregistrationUser, Realm, Stream, UserProfile
|
||||||
from zerver.views.auth import get_safe_redirect_to
|
|
||||||
from zerver.views.portico import hello_view
|
from zerver.views.portico import hello_view
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,14 +116,10 @@ def home(request: HttpRequest) -> HttpResponse:
|
||||||
if settings.ROOT_DOMAIN_LANDING_PAGE and subdomain == Realm.SUBDOMAIN_FOR_ROOT_DOMAIN:
|
if settings.ROOT_DOMAIN_LANDING_PAGE and subdomain == Realm.SUBDOMAIN_FOR_ROOT_DOMAIN:
|
||||||
return hello_view(request)
|
return hello_view(request)
|
||||||
|
|
||||||
# TODO: The following logic is a bit hard to read. We save a
|
realm = get_realm_from_request(request)
|
||||||
# database query in the common case by avoiding the call to
|
if realm is None:
|
||||||
# `get_valid_realm_from_request` if user hasn't requested
|
return render(request, "zerver/invalid_realm.html", status=404)
|
||||||
# web-public access.
|
if realm.allow_web_public_streams_access():
|
||||||
if (
|
|
||||||
request.POST.get("prefers_web_public_view") == "true"
|
|
||||||
or request.session.get("prefers_web_public_view")
|
|
||||||
) and get_valid_realm_from_request(request).allow_web_public_streams_access():
|
|
||||||
return web_public_view(home_real)(request)
|
return web_public_view(home_real)(request)
|
||||||
return zulip_login_required(home_real)(request)
|
return zulip_login_required(home_real)(request)
|
||||||
|
|
||||||
|
@ -161,32 +156,9 @@ def home_real(request: HttpRequest) -> HttpResponse:
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
user_profile = request.user
|
user_profile = request.user
|
||||||
realm = user_profile.realm
|
realm = user_profile.realm
|
||||||
|
|
||||||
# User is logged in and hence no longer `prefers_web_public_view`.
|
|
||||||
if "prefers_web_public_view" in request.session.keys():
|
|
||||||
del request.session["prefers_web_public_view"]
|
|
||||||
else:
|
else:
|
||||||
realm = get_valid_realm_from_request(request)
|
realm = get_valid_realm_from_request(request)
|
||||||
|
# We load the spectator experience. We fall through to the shared code
|
||||||
# TODO: Ideally, we'd open Zulip directly as a spectator if
|
|
||||||
# the URL had clicked a link to content on a web-public
|
|
||||||
# stream. We could maybe do this by parsing `next`, but it's
|
|
||||||
# not super convenient with Zulip's hash-based URL scheme.
|
|
||||||
|
|
||||||
# The "Access without an account" button on the login page
|
|
||||||
# submits a POST to this page with this hidden field set.
|
|
||||||
if request.POST.get("prefers_web_public_view") == "true":
|
|
||||||
request.session["prefers_web_public_view"] = True
|
|
||||||
# We serve a redirect here, rather than serving a page, to
|
|
||||||
# avoid browser "Confirm form resubmission" prompts on reload.
|
|
||||||
redirect_to = get_safe_redirect_to(request.POST.get("next"), realm.uri)
|
|
||||||
return redirect(redirect_to)
|
|
||||||
|
|
||||||
# See the assert in `home` above for why this must be true.
|
|
||||||
assert request.session.get("prefers_web_public_view")
|
|
||||||
|
|
||||||
# For users who have selected public access, we load the
|
|
||||||
# spectator experience. We fall through to the shared code
|
|
||||||
# for loading the application, with user_profile=None encoding
|
# for loading the application, with user_profile=None encoding
|
||||||
# that we're a spectator, not a logged-in user.
|
# that we're a spectator, not a logged-in user.
|
||||||
user_profile = None
|
user_profile = None
|
||||||
|
|
Loading…
Reference in New Issue