From 402ad256900fd57e97841cb4f733aee3d616f000 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 28 Nov 2016 18:39:28 -0500 Subject: [PATCH] Change team invitation acceptance to join all invited teams under the org Fixes #1989 --- data/model/team.py | 35 +++++++++++----- endpoints/api/team.py | 4 +- test/test_api_usage.py | 92 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 12 deletions(-) diff --git a/data/model/team.py b/data/model/team.py index a04b95483..69bc6fe44 100644 --- a/data/model/team.py +++ b/data/model/team.py @@ -312,10 +312,24 @@ def find_matching_team_invite(code, user_obj): return found +def find_organization_invites(organization, user_obj): + """ Finds all organization team invites for the given user under the given organization. """ + invite_check = (TeamMemberInvite.user == user_obj) + if user_obj.verified: + invite_check = invite_check | (TeamMemberInvite.email == user_obj.email) + + query = (TeamMemberInvite + .select() + .join(Team) + .where(invite_check, Team.organization == organization)) + return query + + def confirm_team_invite(code, user_obj): """ Confirms the given team invite code for the given user by adding the user to the team and deleting the code. Raises a DataModelException if the code was not found or does - not apply to the given user. """ + not apply to the given user. If the user is invited to two or more teams under the + 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. @@ -324,15 +338,18 @@ def confirm_team_invite(code, user_obj): Please login to that account and try again.""" % found.user.username raise DataModelException(message) - # Add the user to the team. - try: - add_user_to_team(user_obj, found.team) - except UserAlreadyInTeam: - # Ignore. - pass + # Find all matching invitations for the user under the organization. + for invite in find_organization_invites(found.team.organization, user_obj): + # Add the user to the team. + try: + add_user_to_team(user_obj, invite.team) + except UserAlreadyInTeam: + # Ignore. + pass + + # Delete the invite and return the team. + invite.delete_instance() - # Delete the invite and return the team. team = found.team inviter = found.inviter - found.delete_instance() return (team, inviter) diff --git a/endpoints/api/team.py b/endpoints/api/team.py index d1ef774ac..3fdb5bd5f 100644 --- a/endpoints/api/team.py +++ b/endpoints/api/team.py @@ -28,7 +28,8 @@ def permission_view(permission): def try_accept_invite(code, user): (team, inviter) = model.team.confirm_team_invite(code, user) - model.notification.delete_matching_notifications(user, 'org_team_invite', code=code) + model.notification.delete_matching_notifications(user, 'org_team_invite', + org=team.organization.username) orgname = team.organization.username log_action('org_team_member_invite_accepted', orgname, { @@ -208,6 +209,7 @@ class TeamMemberList(ApiResource): invites = model.team.get_organization_team_member_invites(team.id) data = { + 'name': teamname, 'members': [member_view(m) for m in members] + [invite_view(i) for i in invites], 'can_edit': edit_permission.can() } diff --git a/test/test_api_usage.py b/test/test_api_usage.py index b356049a9..6426e5b0f 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -227,14 +227,14 @@ class ApiTestCase(unittest.TestCase): def assertNotInTeam(self, data, membername): for memberData in data['members']: if memberData['name'] == membername: - self.fail(membername + ' found in team: ' + json.dumps(data)) + self.fail(membername + ' found in team: ' + data['name']) def assertInTeam(self, data, membername): for member_data in data['members']: if member_data['name'] == membername: return - self.fail(membername + ' not found in team: ' + py_json.dumps(data)) + self.fail(membername + ' not found in team: ' + data['name']) def login(self, username, password='password'): return self.postJsonResponse(Signin, data=dict(username=username, password=password)) @@ -682,6 +682,94 @@ class TestCreateNewUser(ApiTestCase): teamname='owners')) self.assertNotInTeam(json, NEW_USER_DETAILS['username']) + def test_createuser_withmultipleteaminvites(self): + inviter = model.user.get_user(ADMIN_ACCESS_USER) + owners_team = model.team.get_organization_team(ORGANIZATION, 'owners') + readers_team = model.team.get_organization_team(ORGANIZATION, 'readers') + other_owners_team = model.team.get_organization_team('library', 'owners') + + owners_invite = model.team.add_or_invite_to_team(inviter, owners_team, None, + NEW_USER_DETAILS['email']) + + readers_invite = model.team.add_or_invite_to_team(inviter, readers_team, None, + NEW_USER_DETAILS['email']) + + other_owners_invite = model.team.add_or_invite_to_team(inviter, other_owners_team, None, + NEW_USER_DETAILS['email']) + + # Create the user and ensure they have a verified email address. + details = { + 'invite_code': owners_invite.invite_token + } + details.update(NEW_USER_DETAILS) + + data = self.postJsonResponse(User, data=details, expected_code=200) + + # Make sure the user is verified since the email address of the user matches + # that of the team invite. + self.assertFalse('awaiting_verification' in data) + + # Make sure the user was not (yet) added to the teams. + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse(TeamMemberList, + params=dict(orgname=ORGANIZATION, + teamname='owners')) + self.assertNotInTeam(json, NEW_USER_DETAILS['username']) + + json = self.getJsonResponse(TeamMemberList, + params=dict(orgname=ORGANIZATION, + teamname='readers')) + self.assertNotInTeam(json, NEW_USER_DETAILS['username']) + + json = self.getJsonResponse(TeamMemberList, + params=dict(orgname='library', + teamname='owners')) + self.assertNotInTeam(json, NEW_USER_DETAILS['username']) + + # Accept the first invitation. + self.login(NEW_USER_DETAILS['username']) + self.putJsonResponse(TeamMemberInvite, params=dict(code=owners_invite.invite_token)) + + # Make sure both codes are now invalid. + self.putResponse(TeamMemberInvite, params=dict(code=owners_invite.invite_token), + expected_code=400) + + self.putResponse(TeamMemberInvite, params=dict(code=readers_invite.invite_token), + expected_code=400) + + # Make sure the user is now in the two invited teams under the organization, but not + # in the other org's team. + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse(TeamMemberList, + params=dict(orgname=ORGANIZATION, + teamname='owners')) + self.assertInTeam(json, NEW_USER_DETAILS['username']) + + json = self.getJsonResponse(TeamMemberList, + params=dict(orgname=ORGANIZATION, + teamname='readers')) + self.assertInTeam(json, NEW_USER_DETAILS['username']) + + json = self.getJsonResponse(TeamMemberList, + params=dict(orgname='library', + teamname='owners')) + self.assertNotInTeam(json, NEW_USER_DETAILS['username']) + + # Accept the second invitation. + self.login(NEW_USER_DETAILS['username']) + self.putJsonResponse(TeamMemberInvite, params=dict(code=other_owners_invite.invite_token)) + + # Make sure the user was added to the other organization. + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse(TeamMemberList, + params=dict(orgname='library', + teamname='owners')) + self.assertInTeam(json, NEW_USER_DETAILS['username']) + + # Make sure the invitation codes are now invalid. + self.putResponse(TeamMemberInvite, params=dict(code=other_owners_invite.invite_token), + expected_code=400) + class TestDeleteNamespace(ApiTestCase): def test_deletenamespaces(self):