From 785c74de52f8608fccd474bfdb4dcede9eef216a Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 15 Dec 2016 17:15:11 -0500 Subject: [PATCH] Fix attempts to confirm team invite for mismatched email address Currently, if a user tries to confirm an invite sent to them on an account with a mismatching email address, we simply redirect to the org (where they get a 403). This change ensures they get the proper error response message, and restyles the error page to be nicer. Fixes #2227 Fixes https://www.pivotaltracker.com/story/show/136088507 --- data/model/team.py | 22 +++++-- static/partials/confirm-invite.html | 9 ++- test/test_api_usage.py | 97 ++++++++++++++++++++++++++++- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/data/model/team.py b/data/model/team.py index 69bc6fe44..64d877c5b 100644 --- a/data/model/team.py +++ b/data/model/team.py @@ -270,6 +270,10 @@ def delete_team_user_invite(team, user_obj): return True +def lookup_team_invites_by_email(email): + return TeamMemberInvite.select().where(TeamMemberInvite.email == email) + + def lookup_team_invites(user_obj): return TeamMemberInvite.select().where(TeamMemberInvite.user == user_obj) @@ -332,16 +336,12 @@ def confirm_team_invite(code, user_obj): same organization, they are automatically confirmed for all of them. """ found = find_matching_team_invite(code, user_obj) - # If the invite is for a specific user, we have to confirm that here. - if found.user is not None and found.user != user_obj: - message = """This invite is intended for user "%s". - Please login to that account and try again.""" % found.user.username - raise DataModelException(message) - # Find all matching invitations for the user under the organization. + code_found = False for invite in find_organization_invites(found.team.organization, user_obj): # Add the user to the team. try: + code_found = True add_user_to_team(user_obj, invite.team) except UserAlreadyInTeam: # Ignore. @@ -350,6 +350,16 @@ def confirm_team_invite(code, user_obj): # Delete the invite and return the team. invite.delete_instance() + if not code_found: + if found.user: + message = """This invite is intended for user "%s". + Please login to that account and try again.""" % found.user.username + raise DataModelException(message) + else: + message = """This invite is intended for email "%s". + Please login to that account and try again.""" % found.email + raise DataModelException(message) + team = found.team inviter = found.inviter return (team, inviter) diff --git a/static/partials/confirm-invite.html b/static/partials/confirm-invite.html index 5bf2f97ad..8da6be2fe 100644 --- a/static/partials/confirm-invite.html +++ b/static/partials/confirm-invite.html @@ -5,9 +5,12 @@
-
-
- {{ invalid }} +
+ +
+

Confirmation Error

+

Confirmation code does not match this account

+
{{ invalid }}
diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 34817907f..4f62dbb51 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -30,7 +30,7 @@ from data.database import RepositoryActionCount, Repository as RepositoryTable from test.helpers import assert_action_logged from endpoints.api.team import (TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam, - TeamPermissions) + TeamPermissions, InviteTeamMember) from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RevertTag, ListRepositoryTags from endpoints.api.search import EntitySearch, ConductSearch from endpoints.api.image import RepositoryImage, RepositoryImageList @@ -1556,7 +1556,102 @@ class TestAcceptTeamMemberInvite(ApiTestCase): params=dict(code=invites[0].invite_token), expected_code=400) + def test_accept_via_email(self): + self.login(ADMIN_ACCESS_USER) + # Create the invite. + member = model.user.get_user(NO_ACCESS_USER) + response = self.putJsonResponse(InviteTeamMember, + params=dict(orgname=ORGANIZATION, teamname='owners', + email=member.email)) + + self.assertEquals(True, response['invited']) + + # Login as the user. + self.login(member.username) + + # Accept the invite. + invites = list(model.team.lookup_team_invites_by_email(member.email)) + self.assertEquals(1, len(invites)) + + self.putJsonResponse(TeamMemberInvite, params=dict(code=invites[0].invite_token)) + + # Verify the user is now on the team. + json = self.getJsonResponse(TeamMemberList, + params=dict(orgname=ORGANIZATION, + teamname='owners')) + + self.assertInTeam(json, member.username) + + # Verify the accept now fails. + self.putResponse(TeamMemberInvite, + params=dict(code=invites[0].invite_token), + expected_code=400) + + + def test_accept_invite_different_user(self): + self.login(ADMIN_ACCESS_USER) + + # Create the invite. + response = self.putJsonResponse(TeamMember, + params=dict(orgname=ORGANIZATION, teamname='owners', + membername=NO_ACCESS_USER)) + + self.assertEquals(True, response['invited']) + + # Login as a different user. + self.login(PUBLIC_USER) + + # Try to accept the invite. + user = model.user.get_user(NO_ACCESS_USER) + invites = list(model.team.lookup_team_invites(user)) + self.assertEquals(1, len(invites)) + + self.putResponse(TeamMemberInvite, params=dict(code=invites[0].invite_token), + expected_code=400) + + # Ensure the invite is still valid. + user = model.user.get_user(NO_ACCESS_USER) + invites = list(model.team.lookup_team_invites(user)) + self.assertEquals(1, len(invites)) + + # Ensure the user is *not* a member of the team. + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse(TeamMemberList, + params=dict(orgname=ORGANIZATION, + teamname='owners')) + self.assertNotInTeam(json, PUBLIC_USER) + + def test_accept_invite_different_email(self): + self.login(ADMIN_ACCESS_USER) + + # Create the invite. + response = self.putJsonResponse(InviteTeamMember, + params=dict(orgname=ORGANIZATION, teamname='owners', + email='someemail@example.com')) + + self.assertEquals(True, response['invited']) + + # Login as a different user. + self.login(PUBLIC_USER) + + # Try to accept the invite. + invites = list(model.team.lookup_team_invites_by_email('someemail@example.com')) + self.assertEquals(1, len(invites)) + + self.putResponse(TeamMemberInvite, params=dict(code=invites[0].invite_token), + expected_code=400) + + # Ensure the invite is still valid. + invites = list(model.team.lookup_team_invites_by_email('someemail@example.com')) + self.assertEquals(1, len(invites)) + + # Ensure the user is *not* a member of the team. + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse(TeamMemberList, + params=dict(orgname=ORGANIZATION, + teamname='owners')) + self.assertNotInTeam(json, PUBLIC_USER) class TestDeclineTeamMemberInvite(ApiTestCase): def test_decline_wronguser(self):