Support invite codes for verification of email
Also changes the system so we don't apply the invite until it is called explicitly from the frontend Fixes #241
This commit is contained in:
parent
5d86fa80e7
commit
687bab1c05
7 changed files with 3185 additions and 35 deletions
3041
data/model/legacy.py
Normal file
3041
data/model/legacy.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -253,9 +253,26 @@ def delete_team_invite(code, user_obj=None):
|
|||
return (team, inviter)
|
||||
|
||||
|
||||
def confirm_team_invite(code, user_obj):
|
||||
def find_matching_team_invite(code, user_obj):
|
||||
""" Finds a team invite with the given code that applies to the given user and returns it or
|
||||
raises a DataModelException if not found. """
|
||||
found = lookup_team_invite(code)
|
||||
|
||||
# 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)
|
||||
|
||||
return found
|
||||
|
||||
|
||||
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. """
|
||||
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".
|
||||
|
|
|
@ -19,7 +19,7 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques
|
|||
from endpoints.api.subscribe import subscribe
|
||||
from endpoints.common import common_login
|
||||
from endpoints.decorators import anon_allowed
|
||||
from endpoints.api.team import try_accept_invite
|
||||
|
||||
from data import model
|
||||
from data.billing import get_plan
|
||||
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
|
||||
|
@ -33,6 +33,33 @@ from util.names import parse_single_urn
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def handle_invite_code(invite_code, user):
|
||||
""" Checks that the given invite code matches the specified user's e-mail address. If so, the
|
||||
user is marked as having a verified e-mail address and this method returns True.
|
||||
"""
|
||||
parsed_invite = parse_single_urn(invite_code)
|
||||
if parsed_invite is None:
|
||||
return False
|
||||
|
||||
if parsed_invite[0] != 'teaminvite':
|
||||
return False
|
||||
|
||||
# Check to see if the team invite is valid. If so, then we know the user has
|
||||
# a possible matching email address.
|
||||
try:
|
||||
found = model.team.find_matching_team_invite(invite_code, user)
|
||||
except model.DataModelException:
|
||||
return False
|
||||
|
||||
# Since we sent the invite code via email, mark the user as having a verified
|
||||
# email address.
|
||||
if found.email != user.email:
|
||||
return False
|
||||
|
||||
user.verified = True
|
||||
user.save()
|
||||
return True
|
||||
|
||||
|
||||
def user_view(user):
|
||||
def org_view(o):
|
||||
|
@ -105,7 +132,6 @@ class User(ApiResource):
|
|||
""" Operations related to users. """
|
||||
schemas = {
|
||||
'NewUser': {
|
||||
|
||||
'id': 'NewUser',
|
||||
'type': 'object',
|
||||
'description': 'Fields which must be specified for a new user.',
|
||||
|
@ -299,18 +325,8 @@ class User(ApiResource):
|
|||
new_user = model.user.create_user(user_data['username'], user_data['password'],
|
||||
user_data['email'], auto_verify=not features.MAILING)
|
||||
|
||||
# Handle any invite codes.
|
||||
parsed_invite = parse_single_urn(invite_code)
|
||||
if parsed_invite is not None:
|
||||
if parsed_invite[0] == 'teaminvite':
|
||||
# Add the user to the team.
|
||||
try:
|
||||
try_accept_invite(invite_code, new_user)
|
||||
except model.user.DataModelException:
|
||||
pass
|
||||
|
||||
|
||||
if features.MAILING:
|
||||
email_address_confirmed = handle_invite_code(invite_code, new_user)
|
||||
if features.MAILING and not email_address_confirmed:
|
||||
code = model.user.create_confirm_email_code(new_user)
|
||||
send_confirmation_email(new_user.username, new_user.email, code.code)
|
||||
return {
|
||||
|
@ -389,18 +405,23 @@ class ClientKey(ApiResource):
|
|||
}
|
||||
|
||||
|
||||
def conduct_signin(username_or_email, password):
|
||||
def conduct_signin(username_or_email, password, invite_code=None):
|
||||
needs_email_verification = False
|
||||
invalid_credentials = False
|
||||
found_user = None
|
||||
|
||||
verified = None
|
||||
try:
|
||||
(verified, error_message) = authentication.verify_and_link_user(username_or_email, password)
|
||||
(found_user, error_message) = authentication.verify_and_link_user(username_or_email, password)
|
||||
except model.user.TooManyUsersException as ex:
|
||||
raise license_error(exception=ex)
|
||||
|
||||
if verified:
|
||||
if common_login(verified):
|
||||
# If there is an attached invitation code, handle it here. This will mark the
|
||||
# user as verified if the code is valid.
|
||||
if invite_code:
|
||||
handle_invite_code(invite_code, found_user)
|
||||
|
||||
if found_user:
|
||||
if common_login(found_user):
|
||||
return {'success': True}
|
||||
else:
|
||||
needs_email_verification = True
|
||||
|
@ -501,6 +522,10 @@ class Signin(ApiResource):
|
|||
'type': 'string',
|
||||
'description': 'The user\'s password',
|
||||
},
|
||||
'invite_code': {
|
||||
'type': 'string',
|
||||
'description': 'The optional invite code'
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -516,8 +541,9 @@ class Signin(ApiResource):
|
|||
|
||||
username = signin_data['username']
|
||||
password = signin_data['password']
|
||||
invite_code = signin_data.get('invite_code', '')
|
||||
|
||||
return conduct_signin(username, password)
|
||||
return conduct_signin(username, password, invite_code=invite_code)
|
||||
|
||||
|
||||
@resource('/v1/signin/verify')
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</div>
|
||||
<div id="collapseSignin" class="panel-collapse collapse" ng-class="hasSignedIn() ? 'in' : 'out'">
|
||||
<div class="panel-body">
|
||||
<div class="signin-form" signed-in="signedIn()" sign-in-started="signInStarted()" redirect-url="redirectUrl"></div>
|
||||
<div class="signin-form" signed-in="signedIn()" sign-in-started="signInStarted()" redirect-url="redirectUrl" invite-code="inviteCode"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@ angular.module('quay').directive('signinForm', function () {
|
|||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'inviteCode': '=inviteCode',
|
||||
'redirectUrl': '=redirectUrl',
|
||||
'signInStarted': '&signInStarted',
|
||||
'signedIn': '&signedIn'
|
||||
|
@ -49,6 +50,10 @@ angular.module('quay').directive('signinForm', function () {
|
|||
$scope.markStarted();
|
||||
$scope.cancelInterval();
|
||||
|
||||
if ($scope.inviteCode) {
|
||||
$scope.user['invite_code'] = $scope.inviteCode;
|
||||
}
|
||||
|
||||
ApiService.signinUser($scope.user).then(function() {
|
||||
$scope.signingIn = false;
|
||||
$scope.needsEmailVerification = false;
|
||||
|
|
|
@ -35,11 +35,13 @@ angular.module('quay').directive('signupForm', function () {
|
|||
mixpanel.alias($scope.newUser.username);
|
||||
}
|
||||
|
||||
if (!$scope.awaitingConfirmation) {
|
||||
$scope.userRegistered({'username': $scope.newUser.username});
|
||||
|
||||
if (!$scope.awaitingConfirmation && !$scope.inviteCode) {
|
||||
document.location = '/';
|
||||
}
|
||||
|
||||
$scope.userRegistered({'username': $scope.newUser.username});
|
||||
UserService.load();
|
||||
}, function(result) {
|
||||
$scope.registering = false;
|
||||
UIService.showFormError('#signupButton', result);
|
||||
|
|
|
@ -182,6 +182,12 @@ class ApiTestCase(unittest.TestCase):
|
|||
parsed = py_json.loads(data)
|
||||
return parsed
|
||||
|
||||
|
||||
def assertNotInTeam(self, data, membername):
|
||||
for memberData in data['members']:
|
||||
if memberData['name'] == membername:
|
||||
self.fail(membername + ' found in team: ' + json.dumps(data))
|
||||
|
||||
def assertInTeam(self, data, membername):
|
||||
for member_data in data['members']:
|
||||
if member_data['name'] == membername:
|
||||
|
@ -469,7 +475,7 @@ class TestCreateNewUser(ApiTestCase):
|
|||
def test_createuser_withteaminvite(self):
|
||||
inviter = model.user.get_user(ADMIN_ACCESS_USER)
|
||||
team = model.team.get_organization_team(ORGANIZATION, 'owners')
|
||||
invite = model.team.add_or_invite_to_team(inviter, team, None, 'foo@example.com')
|
||||
invite = model.team.add_or_invite_to_team(inviter, team, None, NEW_USER_DETAILS['email'])
|
||||
|
||||
details = {
|
||||
'invite_code': invite.invite_token
|
||||
|
@ -477,14 +483,42 @@ class TestCreateNewUser(ApiTestCase):
|
|||
details.update(NEW_USER_DETAILS)
|
||||
|
||||
data = self.postJsonResponse(User, data=details, expected_code=200)
|
||||
self.assertEquals(True, data['awaiting_verification'])
|
||||
|
||||
# Make sure the user was added to the team.
|
||||
# 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 team.
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
json = self.getJsonResponse(TeamMemberList,
|
||||
params=dict(orgname=ORGANIZATION,
|
||||
teamname='owners'))
|
||||
self.assertInTeam(json, NEW_USER_DETAILS['username'])
|
||||
self.assertNotInTeam(json, NEW_USER_DETAILS['username'])
|
||||
|
||||
|
||||
def test_createuser_withteaminvite_differentemails(self):
|
||||
inviter = model.user.get_user(ADMIN_ACCESS_USER)
|
||||
team = model.team.get_organization_team(ORGANIZATION, 'owners')
|
||||
invite = model.team.add_or_invite_to_team(inviter, team, None, 'differentemail@example.com')
|
||||
|
||||
details = {
|
||||
'invite_code': invite.invite_token
|
||||
}
|
||||
details.update(NEW_USER_DETAILS)
|
||||
|
||||
data = self.postJsonResponse(User, data=details, expected_code=200)
|
||||
|
||||
# Make sure the user is *not* verified since the email address of the user
|
||||
# does not match that of the team invite.
|
||||
self.assertTrue(data['awaiting_verification'])
|
||||
|
||||
# Make sure the user was not (yet) added to the team.
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
json = self.getJsonResponse(TeamMemberList,
|
||||
params=dict(orgname=ORGANIZATION,
|
||||
teamname='owners'))
|
||||
self.assertNotInTeam(json, NEW_USER_DETAILS['username'])
|
||||
|
||||
|
||||
|
||||
class TestSignin(ApiTestCase):
|
||||
|
@ -492,6 +526,38 @@ class TestSignin(ApiTestCase):
|
|||
self.postResponse(Signin, data=dict(username=u'\xe5\x8c\x97\xe4\xba\xac\xe5\xb8\x82',
|
||||
password='password'), expected_code=403)
|
||||
|
||||
def test_signin_invitecode(self):
|
||||
# Create a new user (unverified)
|
||||
data = self.postJsonResponse(User, data=NEW_USER_DETAILS, expected_code=200)
|
||||
self.assertTrue(data['awaiting_verification'])
|
||||
|
||||
# Try to sign in without an invite code.
|
||||
data = self.postJsonResponse(Signin, data=NEW_USER_DETAILS, expected_code=403)
|
||||
self.assertTrue(data['needsEmailVerification'])
|
||||
|
||||
# Try to sign in with an invalid invite code.
|
||||
details = {
|
||||
'invite_code': 'someinvalidcode'
|
||||
}
|
||||
details.update(NEW_USER_DETAILS)
|
||||
|
||||
data = self.postJsonResponse(Signin, data=details, expected_code=403)
|
||||
self.assertTrue(data['needsEmailVerification'])
|
||||
|
||||
# Sign in with an invite code and ensure the user becomes verified.
|
||||
inviter = model.user.get_user(ADMIN_ACCESS_USER)
|
||||
team = model.team.get_organization_team(ORGANIZATION, 'owners')
|
||||
invite = model.team.add_or_invite_to_team(inviter, team, None, NEW_USER_DETAILS['email'])
|
||||
|
||||
details = {
|
||||
'invite_code': invite.invite_token
|
||||
}
|
||||
details.update(NEW_USER_DETAILS)
|
||||
|
||||
data = self.postJsonResponse(Signin, data=details, expected_code=200)
|
||||
self.assertFalse('needsEmailVerification' in data)
|
||||
|
||||
|
||||
|
||||
class TestSignout(ApiTestCase):
|
||||
def test_signout(self):
|
||||
|
@ -1050,13 +1116,6 @@ class TestUpdateOrganizationTeamMember(ApiTestCase):
|
|||
|
||||
|
||||
class TestAcceptTeamMemberInvite(ApiTestCase):
|
||||
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))
|
||||
|
||||
def test_accept(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
|
|
Reference in a new issue