Skip to content
Open
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
40 changes: 31 additions & 9 deletions hypha/apply/users/tests/test_email_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions hypha/apply/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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"]
Expand Down
4 changes: 2 additions & 2 deletions hypha/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down