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(sign-in): Fix redirect correct org after accepting invite #82005

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a38c8fb
fix(sign-in): Fix redirect correct org after accepting invite
priscilawebdev Dec 12, 2024
e3400ba
Merge branch 'master' into priscila/fix/sign-in/after-accepting-not-r…
priscilawebdev Dec 12, 2024
8917d0d
fix test
priscilawebdev Dec 12, 2024
01ae5c6
Merge branch 'priscila/fix/sign-in/after-accepting-not-redirected-to-…
priscilawebdev Dec 12, 2024
2498903
update test name
priscilawebdev Dec 12, 2024
1bb57c9
Merge branch 'master' into priscila/fix/sign-in/after-accepting-not-r…
priscilawebdev Dec 12, 2024
cb987bc
move code
priscilawebdev Dec 13, 2024
a662a8c
fix test
priscilawebdev Dec 13, 2024
7e3fb23
test that the user sees the 2FA view after accepting invite with exis…
mifu67 Dec 16, 2024
3f809cd
Merge branch 'master' into priscila/fix/sign-in/after-accepting-not-r…
priscilawebdev Dec 17, 2024
c1e2f11
Merge branch 'master' into priscila/fix/sign-in/after-accepting-not-r…
priscilawebdev Jan 8, 2025
0194109
Merge branch 'master' into priscila/fix/sign-in/after-accepting-not-r…
priscilawebdev Jan 8, 2025
7278406
Merge branch 'priscila/fix/sign-in/after-accepting-not-redirected-to-…
priscilawebdev Jan 10, 2025
a2d2f4e
Merge branch 'master' into priscila/fix/sign-in/after-accepting-not-r…
priscilawebdev Jan 10, 2025
e103635
Merge branch 'master' into priscila/fix/sign-in/after-accepting-not-r…
priscilawebdev Jan 10, 2025
7846496
users have to explicitly accept org invitation
priscilawebdev Jan 13, 2025
a6553f6
Merge branch 'priscila/fix/sign-in/after-accepting-not-redirected-to-…
priscilawebdev Jan 13, 2025
aa1ed41
Merge branch 'master' into priscila/fix/sign-in/after-accepting-not-r…
priscilawebdev Jan 13, 2025
a8edc8e
update comment
priscilawebdev Jan 13, 2025
a34916e
Merge branch 'priscila/fix/sign-in/after-accepting-not-redirected-to-…
priscilawebdev Jan 13, 2025
638f481
fix test
priscilawebdev Jan 13, 2025
5653939
Merge branch 'master' into priscila/fix/sign-in/after-accepting-not-r…
priscilawebdev Jan 16, 2025
7350150
feedback
priscilawebdev Jan 16, 2025
8badf87
Update src/sentry/web/frontend/auth_login.py
priscilawebdev Jan 16, 2025
d36abe7
add import
priscilawebdev Jan 16, 2025
5639efa
Merge branch 'master' into priscila/fix/sign-in/after-accepting-not-r…
priscilawebdev Jan 16, 2025
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
1 change: 1 addition & 0 deletions src/sentry/organizations/services/organization/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ class RpcOrganizationMember(RpcOrganizationMemberSummary):
token_expired: bool = False
legacy_token: str = ""
email: str = ""
invitation_link: str | None = None

def get_audit_log_metadata(self, user_email: str | None = None) -> Mapping[str, Any]:
from sentry.models.organizationmember import invite_status_names
Expand Down
1 change: 1 addition & 0 deletions src/sentry/organizations/services/organization/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def serialize_member(member: OrganizationMember) -> RpcOrganizationMember:
token_expired=member.token_expired,
legacy_token=member.legacy_token,
email=member.get_email(),
invitation_link=member.get_invite_link(),
)

omts = OrganizationMemberTeam.objects.filter(
Expand Down
19 changes: 19 additions & 0 deletions src/sentry/web/frontend/auth_login.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import logging
import urllib
from typing import Any

from django.conf import settings
Expand Down Expand Up @@ -668,6 +669,24 @@ def handle_basic_auth(self, request: Request, **kwargs) -> HttpResponseBase:
if not user.is_active:
return self.redirect(reverse("sentry-reactivate-account"))
if organization:
# Check if the user is a member of the provided organization based on their email
membership = organization_service.check_membership_by_email(
email=user.email, organization_id=organization.id
)

invitation_link = getattr(membership, "invitation_link", None)

# If the user is a member, the user_id is None, and they are in a "pending invite acceptance" state with a valid invitation link,
# we redirect them to the invitation page to explicitly accept the invite
if (
membership
and membership.user_id is None
and membership.is_pending
and invitation_link
):
accept_path = urllib.parse.urlparse(invitation_link).path
return self.redirect(accept_path)

# Refresh the organization we fetched prior to login in order to check its login state.
org_context = organization_service.get_organization_by_slug(
user_id=request.user.id,
Expand Down
128 changes: 128 additions & 0 deletions tests/acceptance/test_accept_organization_invite.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.db.models import F
from django.test import override_settings
from selenium.webdriver.common.by import By

from sentry.models.authprovider import AuthProvider
from sentry.models.organization import Organization
Expand All @@ -23,6 +25,14 @@ def setUp(self):
teams=[self.team],
)

def _sign_in_user(self, email, password):
"""
Helper method to sign in a user with given email and password.
"""
self.browser.find_element(By.ID, "id_username").send_keys(email)
self.browser.find_element(By.ID, "id_password").send_keys(password)
self.browser.find_element(By.XPATH, "//button[contains(text(), 'Sign In')]").click()

def test_invite_simple(self):
self.login_as(self.user)
self.browser.get(self.member.get_invite_link().split("/", 3)[-1])
Expand Down Expand Up @@ -52,3 +62,121 @@ def test_invite_sso_org(self):
self.browser.wait_until('[data-test-id="accept-invite"]')
assert self.browser.element_exists_by_test_id("action-info-sso")
assert self.browser.element_exists('[data-test-id="sso-login"]')

@override_settings(SENTRY_SINGLE_ORGANIZATION=True)
def test_authenticated_user_already_member_of_an_org_accept_invite_other_org(self):
"""
Test that an authenticated user already part of an organization can accept an invite to another organization.
"""

# Setup: Create a second user and make them a member of an organization
email = "[email protected]"
password = "dummy"
user2 = self.create_user(email=email)
user2.set_password(password)
user2.save()
self.create_organization(name="Second Org", owner=user2)

# Action: Invite User2 to the first organization
new_member = self.create_member(
user=None,
email=user2.email,
organization=self.org,
role="owner",
teams=[self.team],
)

self.login_as(user2)

# Simulate the user accessing the invite link
self.browser.get(new_member.get_invite_link().split("/", 3)[-1])
self.browser.wait_until('[data-test-id="accept-invite"]')

self.browser.click('button[data-test-id="join-organization"]')
assert self.browser.wait_until('[aria-label="Create project"]')

@override_settings(SENTRY_SINGLE_ORGANIZATION=True)
def test_not_authenticated_user_already_member_of_an_org_accept_invite_other_org(self):
"""
Test that a not authenticated user already part of an organization can accept an invite to another organization.
"""

# Setup: Create a second user and make them a member of an organization
email = "[email protected]"
password = "dummy"
user2 = self.create_user(email=email)
user2.set_password(password)
user2.save()
self.create_organization(name="Second Org", owner=user2)

# Action: Invite User2 to the first organization
new_member = self.create_member(
user=None,
email=user2.email,
organization=self.org,
role="member",
teams=[self.team],
)

# Simulate the user accessing the invite link
self.browser.get(new_member.get_invite_link().split("/", 3)[-1])
self.browser.wait_until('[data-test-id="accept-invite"]')

# Choose to login with existing account
self.browser.click('a[data-test-id="link-with-existing"]')
self.browser.wait_until_not('[data-test-id="loading-indicator"]')

# Handle form validation: Prevent default invalid event blocking
self.browser.driver.execute_script(
"document.addEventListener('invalid', function(e) { e.preventDefault(); }, true);"
)

# Login
self._sign_in_user(email, password)
self.browser.wait_until('[data-test-id="join-organization"]')

# Display the acceptance view for the invitation to join a new organization
assert self.browser.element_exists(f"[aria-label='Join the {self.org.slug} organization']")

@override_settings(SENTRY_SINGLE_ORGANIZATION=True)
def test_existing_user_invite_2fa_enforced_org(self):
"""
Test that a user who has an existing Sentry account can accept an invite to another organization
and is required to go through the 2FA configuration view.
"""
self.org.update(flags=F("flags").bitor(Organization.flags.require_2fa))
# Setup: Create a second user and make them a member of an organization
email = "[email protected]"
password = "dummy"
user2 = self.create_user(email=email)
user2.set_password(password)
user2.save()
self.create_organization(name="Second Org", owner=user2)

# Action: Invite User2 to the first organization
new_member = self.create_member(
user=None,
email=user2.email,
organization=self.org,
role="owner",
teams=[self.team],
)
# Simulate the user accessing the invite link
self.browser.get(new_member.get_invite_link().split("/", 3)[-1])
self.browser.wait_until('[data-test-id="accept-invite"]')

# Accept the invitation using the existing account
self.browser.click('a[data-test-id="link-with-existing"]')
self.browser.wait_until_not('[data-test-id="loading-indicator"]')

# Handle form validation: Prevent default invalid event blocking
self.browser.driver.execute_script(
"document.addEventListener('invalid', function(e) { e.preventDefault(); }, true);"
)

# Login using existing credentials
self._sign_in_user(email, password)
self.browser.wait_until('[data-test-id="2fa-warning"]')

# Display the 2FA configuration view
assert self.browser.element_exists("[aria-label='Configure Two-Factor Auth']")
12 changes: 6 additions & 6 deletions tests/sentry/web/frontend/test_auth_organization_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,9 +880,8 @@ def test_flow_as_authenticated_user_with_invite_joining(self):

@override_settings(SENTRY_SINGLE_ORGANIZATION=True)
@with_feature({"organizations:create": False})
def test_basic_auth_flow_as_invited_user(self):
def test_basic_auth_flow_as_not_invited_user(self):
user = self.create_user("[email protected]")
self.create_member(organization=self.organization, email="[email protected]")

self.session["_next"] = reverse(
"sentry-organization-settings", args=[self.organization.slug]
Expand All @@ -896,9 +895,8 @@ def test_basic_auth_flow_as_invited_user(self):
assert resp.status_code == 403
self.assertTemplateUsed(resp, "sentry/no-organization-access.html")

def test_basic_auth_flow_as_invited_user_not_single_org_mode(self):
def test_basic_auth_flow_as_not_invited_user_not_single_org_mode(self):
user = self.create_user("[email protected]")
self.create_member(organization=self.organization, email="[email protected]")
resp = self.client.post(
self.path, {"username": user, "password": "admin", "op": "login"}, follow=True
)
Expand Down Expand Up @@ -993,7 +991,8 @@ def test_correct_redirect_as_2fa_user_single_org_invited(self):
self.path, {"username": user, "password": "admin", "op": "login"}, follow=True
)

assert resp.redirect_chain == [("/auth/2fa/", 302)]
invitation_link = "/" + member.get_invite_link().split("/", 3)[-1]
assert resp.redirect_chain == [(invitation_link, 302)]

def test_correct_redirect_as_2fa_user_invited(self):
user = self.create_user("[email protected]")
Expand All @@ -1012,7 +1011,8 @@ def test_correct_redirect_as_2fa_user_invited(self):
self.path, {"username": user, "password": "admin", "op": "login"}, follow=True
)

assert resp.redirect_chain == [("/auth/2fa/", 302)]
invitation_link = "/" + member.get_invite_link().split("/", 3)[-1]
assert resp.redirect_chain == [(invitation_link, 302)]

@override_settings(SENTRY_SINGLE_ORGANIZATION=True)
@with_feature({"organizations:create": False})
Expand Down
Loading