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:
Joseph Schorr 2015-07-16 15:00:51 +03:00
parent 5d86fa80e7
commit 687bab1c05
7 changed files with 3185 additions and 35 deletions

3041
data/model/legacy.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -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".

View file

@ -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')

View file

@ -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>

View file

@ -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;

View file

@ -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);

View file

@ -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)