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 ed4f9f97d..a91ead016 100644
--- a/test/test_api_usage.py
+++ b/test/test_api_usage.py
@@ -30,7 +30,7 @@ from test.helpers import assert_action_logged
from util.secscan.fake import fake_security_scanner
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):