diff --git a/hypha/apply/users/tests/test_email_change.py b/hypha/apply/users/tests/test_email_change.py index 320b89f996..5f185a5325 100644 --- a/hypha/apply/users/tests/test_email_change.py +++ b/hypha/apply/users/tests/test_email_change.py @@ -28,6 +28,17 @@ def url_with_value(signed_value): return f"{EMAIL_CHANGE_URL}?{urlencode({'value': signed_value})}" +class ElevatedSessionMixin: + """Treat the session as elevated for the duration of the test.""" + + def elevate_session(self): + patcher = patch( + "hypha.elevate.middleware.has_elevated_privileges", return_value=True + ) + patcher.start() + self.addCleanup(patcher.stop) + + class TestEmailChangeRequiresLogin(TestCase): def test_unauthenticated_user_redirected_to_login(self): from django.conf import settings @@ -38,7 +49,7 @@ def test_unauthenticated_user_redirected_to_login(self): class TestEmailChangeElevationCheck(TestCase): - """Users with a usable password must re-authenticate (elevate) before proceeding.""" + """Every user must re-authenticate (elevate) before proceeding.""" def setUp(self): self.user = UserFactory() # has a usable password @@ -65,25 +76,35 @@ def test_elevated_user_is_not_redirected_to_elevate(self): self.assertNotEqual(response["Location"], ELEVATE_URL) -class TestEmailChangeOAuthUserSkipsElevation(TestCase): - """OAuth users have no usable password — the elevation gate must be skipped.""" +class TestEmailChangeOAuthUserRequiresElevation(TestCase): + """OAuth users have no usable password, but still have to confirm access. + + The elevate page offers them an emailed confirmation code instead of a + password prompt. + """ def setUp(self): self.user = OAuthUserFactory() self.client.force_login(self.user) - def test_oauth_user_not_redirected_to_elevate(self): + def test_oauth_user_redirected_to_elevate(self): signed = make_signed_value(self.user.email) response = self.client.get(url_with_value(signed), follow=False) - self.assertNotIn(ELEVATE_URL, response.get("Location", "")) + self.assertEqual(response.status_code, 302) + self.assertIn(ELEVATE_URL, response["Location"]) + def test_elevate_page_offers_confirmation_code(self): + response = self.client.get(ELEVATE_URL, follow=False) + self.assertContains(response, "Send a confirmation code to your email") -class TestEmailChangeTokenValidation(TestCase): + +class TestEmailChangeTokenValidation(ElevatedSessionMixin, TestCase): """The signed token in the query string must be valid, unexpired and requested by the same user that the change is being executed for.""" def setUp(self): - self.user = OAuthUserFactory() # skip elevation + self.user = OAuthUserFactory() self.client.force_login(self.user) + self.elevate_session() def test_missing_value_param_redirects_to_account(self): response = self.client.get(EMAIL_CHANGE_URL, follow=False) @@ -104,12 +125,13 @@ def test_tampered_value_shows_error_message(self): self.assertContains(response, "timed out") -class TestEmailChangeSuccess(TestCase): +class TestEmailChangeSuccess(ElevatedSessionMixin, TestCase): """With a valid elevated session and correct token, the view updates the user.""" def setUp(self): - self.user = OAuthUserFactory() # skip elevation + self.user = OAuthUserFactory() self.client.force_login(self.user) + self.elevate_session() def test_valid_token_redirects_to_confirm_link_sent(self): signed = make_signed_value(self.user.email, name="New Name") diff --git a/hypha/apply/users/views.py b/hypha/apply/users/views.py index 81e471b32a..c62f8c1bd0 100644 --- a/hypha/apply/users/views.py +++ b/hypha/apply/users/views.py @@ -179,7 +179,7 @@ def hijack_view(request): @login_required def account_email_change(request): - if request.user.has_usable_password() and not request.is_elevated(): + if not request.is_elevated(): return redirect_to_elevate(request.get_full_path()) signer = TimestampSigner() @@ -195,7 +195,7 @@ def account_email_change(request): return redirect("users:account") value = loads(unsigned_value) - if slack := value["slack"] is not None: + if slack := value["slack"]: request.user.slack = slack request.user.full_name = value["name"] diff --git a/hypha/settings/base.py b/hypha/settings/base.py index b0c9114aeb..28c4ad24af 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -270,8 +270,8 @@ # @deprecated: This setting is deprecated and will be removed in a future release. FORCE_LOGIN_FOR_APPLICATION = env.bool("FORCE_LOGIN_FOR_APPLICATION", True) -# Seconds to enter password on password page while email change/2FA change (default 120, 2 minutes). -PASSWORD_PAGE_TIMEOUT = env.int("PASSWORD_PAGE_TIMEOUT", 120) +# Seconds to enter password on password page while email change/2FA change (default 300, 5 minutes). +PASSWORD_PAGE_TIMEOUT = env.int("PASSWORD_PAGE_TIMEOUT", 300) # Template engines and options to be used with Django. TEMPLATES = [