def post(self): """ Authenticate the user, logout the current user, login the new user :return: dict containing success message """ try: data = get_post_data(request) email = data['username'] password = data['password'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 u = user_manipulator.first(email=email) if u is None or not u.validate_password(password): abort(401) if u.confirmed_at is None: return {"error": "account has not been verified"}, 403 # Logout of previous user (may have been bumblebee) if current_user.is_authenticated(): logout_user() login_user(u) # Login to real user user_manipulator.update( u, last_login_at=datetime.datetime.now(), login_count=u.login_count + 1 if u.login_count else 1, ) return {"message": "success"}, 200
def test_user_ratelimit(self): """ Set the user's ratelimit_level to a value and confirm that """ go = lambda: self.open('GET', '/test_webservice/LOW_RATE_LIMIT') # 3 per 5 second user_manipulator.update(self.user, ratelimit_level=2) # 2*3 per 5 second r = go() self.assertStatus(r, 200) r = go() self.assertStatus(r, 200) r = go() self.assertStatus(r, 200) r = go() self.assertStatus(r, 200) r = go() self.assertStatus(r, 200) r = go() self.assertStatus(r, 200) r = go() time.sleep(0.2) # Make sure cache is caught up self.assertStatus(r, 429) # Wait until the ratelimit has expired, then try again. time.sleep(5) r = go() self.assertStatus(r, 200)
def test_user_ratelimit(self): """ Set the user's ratelimit_level to a value and confirm that """ go = lambda: self.open('GET', '/test_webservice/LOW_RATE_LIMIT' ) # 3 per 5 second user_manipulator.update(self.user, ratelimit_level=2) # 2*3 per 5 second r = go() self.assertStatus(r, 200) r = go() self.assertStatus(r, 200) r = go() self.assertStatus(r, 200) r = go() self.assertStatus(r, 200) r = go() self.assertStatus(r, 200) r = go() self.assertStatus(r, 200) r = go() time.sleep(0.2) # Make sure cache is caught up self.assertStatus(r, 429) # Wait until the ratelimit has expired, then try again. time.sleep(5) r = go() self.assertStatus(r, 200)
def post(self): """ Authenticate the user, logout the current user, login the new user :return: dict containing success message """ try: data = get_post_data(request) email = data['username'] password = data['password'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 u = user_manipulator.first(email=email) if u is None or not u.validate_password(password): abort(401) if u.confirmed_at is None: return {"error": "account has not been verified"}, 403 # Logout of previous user (may have been bumblebee) if current_user.is_authenticated(): logout_user() login_user(u) # Login to real user user_manipulator.update( u, last_login_at=datetime.datetime.now(), login_count=u.login_count+1 if u.login_count else 1, ) return {"message": "success"}, 200
def test_login_and_logout(self): """ tests a login and logout pattern, including incorrect login """ url = url_for('userauthview') payload = {'username': '******', 'password': '******'} r = self.client.post(url, data=json.dumps(payload), headers={'content-type': 'application/json'}) self.assertStatus(r, 400) # No csrf token = 400 with self.client as c: csrf = self.get_csrf() # Incorrect login should return 401 payload = {'username': '******', 'password': '******'} r = c.post(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 401) # A correct login, but unverified account should return an error payload = { 'username': self.real_user.email, 'password': self.passwords[self.real_user] } r = c.post(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 403) self.assertEqual(r.json['error'], 'account has not been verified') # Correct login on a verified account user_manipulator.update(self.real_user, confirmed_at=datetime.datetime.now()) r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}, ) self.assertStatus(r, 200) self.assertEqual(current_user.email, self.real_user.email) self.assertEqual(current_user.login_count, 1) self.assertIsInstance(current_user.last_login_at, datetime.date) self.assertAlmostEqual( current_user.last_login_at, datetime.datetime.now(), delta=datetime.timedelta(seconds=1), ) # Test logout r = c.post(url_for('logoutview'), headers={'X-CSRFToken': csrf}) self.assertRaises(AttributeError, lambda: current_user.email)
def test_adsws_user_ratelimit_header(self): """ Test that the correct 'adsws-user_ratelimit_level' header is passed to remote services """ r = self.open('GET', '/test_webservice/ECHO_HEADERS') self.assertNotIn('X-Adsws-Ratelimit-Level', r.json) user_manipulator.update(self.user, ratelimit_level=10) r = self.open('GET', '/test_webservice/ECHO_HEADERS') self.assertEqual('10', r.json['X-Adsws-Ratelimit-Level'])
def test_user_ratelimit(self): """ Set the user's ratelimit_level to a value and confirm that """ user_manipulator.update(self.user, ratelimit_level=10) r = self.open('GET', '/test_webservice/LOW_RATE_LIMIT') self.assertStatus(r, 200) self.assertEqual( g._rate_limit_info.remaining, self.user.ratelimit_level*3 - 1, )
class ChangePasswordView(Resource): """ Implements change password functionality """ decorators = [login_required] def post(self): """ Verify that the current user's password is correct, that the desired new password is valid, and finally update the password in the User object """ try: data = get_post_data(request) old_password = data['old_password'] new_password1 = data['new_password1'] new_password2 = data['new_password2'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 if not current_user.validate_password(old_password): return {'error': 'please verify your current password'}, 401 if new_password1 != new_password2: return {'error': 'passwords do not match'}, 400 try: validate_password(new_password1) except ValidationError, e: return {'error': 'validation error'}, 400 u = user_manipulator.first(email=current_user.email) user_manipulator.update(u, password=new_password1) return {'message': 'success'}, 200
def get(self, token): try: email = current_app.ts.loads(token, max_age=86400, salt=VerificationEmail.salt) except: current_app.logger.warning( "{0} verification token not validated".format(token)) return {"error": "unknown verification token"}, 404 # This logic is necessary to de-activate accounts via the change-email # workflow. This strong coupling should be deprecated by using signals. previous_uid = None if " " in email: email, previous_uid = email.split() u = user_manipulator.first(email=email) if u is None: return { "error": "no user associated " "with that verification token" }, 404 if u.confirmed_at is not None: return { "error": "this user and email " "has already been validated" }, 400 if previous_uid: # De-activate previous accounts by deleting the account associated # with the new email address, then update the old account with the # new email address. Again, this should be deprecated with signals. user_manipulator.delete(u) u = user_manipulator.first(id=previous_uid) user_manipulator.update(u, email=email, confirmed_at=datetime.datetime.now()) else: user_manipulator.update(u, confirmed_at=datetime.datetime.now()) login_user(u) return {"message": "success", "email": email}
def get(self, token): try: email = current_app.ts.loads(token, max_age=86400, salt=VerificationEmail.salt) except: current_app.logger.warning( "{0} verification token not validated".format(token) ) return {"error": "unknown verification token"}, 404 # This logic is necessary to de-activate accounts via the change-email # workflow. This strong coupling should be deprecated by using signals. previous_uid = None if " " in email: email, previous_uid = email.split() u = user_manipulator.first(email=email) if u is None: return {"error": "no user associated " "with that verification token"}, 404 if u.confirmed_at is not None: return {"error": "this user and email " "has already been validated"}, 400 if previous_uid: # De-activate previous accounts by deleting the account associated # with the new email address, then update the old account with the # new email address. Again, this should be deprecated with signals. user_manipulator.delete(u) u = user_manipulator.first(id=previous_uid) user_manipulator.update( u, email=email, confirmed_at=datetime.datetime.now() ) else: user_manipulator.update(u, confirmed_at=datetime.datetime.now()) login_user(u) return {"message": "success", "email": email}
def test_users_crud_operations(self): """ perform and test create, read, update, and delete patterns on user models using the `user_manipulator` service """ # .new() should not save the User to the database joe = user_manipulator.new(email='joe') self.assertIsNone(user_manipulator.first(email='joe')) # .save() should save the User to the database user_manipulator.save(joe) u = user_manipulator.first(email='joe') self.assertIsNotNone(u) self.assertEqual(u.email, 'joe') # .create() should create immediately elias = user_manipulator.create(email='elias') u = user_manipulator.first(email='elias') self.assertIsNotNone(u) self.assertEqual(elias, u) # .update() should update immediately user_manipulator.update(elias, confirmed_at=datetime(2000, 1, 1)) u = user_manipulator.first(email='elias') self.assertEqual(u.confirmed_at, datetime(2000, 1, 1)) self.assertEqual(elias, u) # .delete() should delete immediately user_manipulator.delete(elias) u = user_manipulator.first(email='elias') self.assertIsNone(u) # even though this object was deleted in the db, we still should # have a reference to the python object self.assertIsNotNone(elias) self.assertEqual(elias.confirmed_at, datetime(2000, 1, 1))
def login(self, user, client, csrf): """ Perform the necessary steps required to sucessfully login Assumes that the resuling cookie will be saved, i.e. we are in a flask.client session context. :param user: User object whose confirmed_at attribute to set, and then logged in :param client: flask.client instance :param csrf: csrf token """ user_manipulator.update( user, confirmed_at=datetime.datetime.now() ) passwd = self.passwords[user] payload = {'username': user.email,'password': passwd} r = client.post( url_for('userauthview'), data=json.dumps(payload), headers={'content-type': 'application/json', 'X-CSRFToken': csrf}, ) self.assertStatus(r, 200)
def login(self, user, client, csrf): """ Perform the necessary steps required to sucessfully login Assumes that the resuling cookie will be saved, i.e. we are in a flask.client session context. :param user: User object whose confirmed_at attribute to set, and then logged in :param client: flask.client instance :param csrf: csrf token """ user_manipulator.update(user, confirmed_at=datetime.datetime.now()) passwd = self.passwords[user] payload = {'username': user.email, 'password': passwd} r = client.post( url_for('userauthview'), data=json.dumps(payload), headers={ 'content-type': 'application/json', 'X-CSRFToken': csrf }, ) self.assertStatus(r, 200)
class ForgotPasswordView(Resource): """ Implements "reset password" functionality """ def get(self, token): """ Attempts to decode a verification token into an email. Responds with the resulting decoded email :param token: HMAC encoded string :type token: basestring """ try: email = current_app.ts.loads(token, max_age=86400, salt=PasswordResetEmail.salt) except: current_app.logger.warning( "Invalid Token {0} in ForgotPasswordView".format(token)) return {"error": "unknown verification token"}, 404 # Check that the user still exists u = user_manipulator.first(email=email) if u is None: current_app.logger.error( "[GET] Reset password validated link," " but no user exists for {email}".format(email=email)) abort(400) return {"email": email}, 200 def post(self, token): """ Send the password reset email to the specified email address (recaptcha protected) Note that param "token" represents the raw email address of the recipient, and it is not expected to be encoded. :param token: email address of the recipient :type token: basestring """ if token == current_app.config['BOOTSTRAP_USER_EMAIL']: abort(403) try: data = get_post_data(request) reset_url = data['reset_url'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 if not verify_recaptcha(request): return {'error': 'captcha was not verified'}, 403 u = user_manipulator.first(email=token) if u is None: return {'error': 'no such user exists'}, 404 if not u.confirmed_at: return { 'error': 'This email was never verified. It will be ' 'deleted from out database within a day' }, 403 send_email(email_addr=token, email_template=PasswordResetEmail, base_url=reset_url, payload=token) return {"message": "success"}, 200 def put(self, token): """ Check if the current user has the same email as the decoded token, and, if so, change that user's password to that specified in the PUT body :param token: HMAC encoded email string :type token: basestring """ try: email = current_app.ts.loads(token, max_age=86400, salt=PasswordResetEmail.salt) except: current_app.logger.critical( "PUT on reset-password with invalid token. " "This may indicate a brute-force attack!") return {"error": "unknown verification token"}, 404 try: data = get_post_data(request) new_password1 = data['password1'] new_password2 = data['password2'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 if new_password1 != new_password2: return {'error': 'passwords do not match'}, 400 try: validate_password(new_password1) except ValidationError, e: return {'error': 'validation error'}, 400 u = user_manipulator.first(email=email) if u is None: current_app.logger.error( "[PUT] Reset password validated link," " but no user exists for {email}".format(email=email)) abort(500) user_manipulator.update(u, password=new_password1) logout_user() login_user(u) return {"message": "success"}, 200
def test_reset_password(self): """ test the reset password workflow """ with self.client as c: csrf = self.get_csrf() self.setup_google_recaptcha_response() user_manipulator.update(self.real_user,confirmed_at=datetime.datetime.now()) url = url_for( 'forgotpasswordview', token="this_email_wasnt@registered" ) payload = { 'g-recaptcha-response': 'correct_response', 'reset_url':'http://not_relevant.com', } # Attempt to reset the password for an unregistered email address r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken':csrf} ) self.assertStatus(r, 404) self.assertEqual(r.json['error'], 'no such user exists') # Resetting password for the default user should not be permitted url = url_for( 'forgotpasswordview', token=self.bootstrap_user.email ) r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf} ) self.assertStatus(r, 403) # Test a proper change-email request url = url_for('forgotpasswordview', token=self.real_user.email) r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf} ) self.assertStatus(r, 200) # Now let's test GET and PUT requests with the encoded token msg, token = utils.send_email( email_addr=self.real_user.email, email_template=PasswordResetEmail, payload=self.real_user.email ) url = url_for('forgotpasswordview', token=token) # Test de-coding and verifying of the token r = c.get(url) self.assertStatus(r, 200) self.assertEqual(r.json['email'], self.real_user.email) # Change the password, then attempt to log-in with the new password payload = {'password1': '123Abc', 'password2': '123Abc'} r = c.put( url, data=json.dumps(payload), headers={'X-CSRFToken':csrf} ) self.assertStatus(r, 200) url = url_for('userauthview') payload = {'username': self.real_user.email,'password':'******'} r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf} ) self.assertStatus(r, 200) self.assertEqual(current_user.email, self.real_user.email)
def test_reset_password(self): """ test the reset password workflow """ with self.client as c: csrf = self.get_csrf() self.setup_google_recaptcha_response() user_manipulator.update(self.real_user, confirmed_at=datetime.datetime.now()) url = url_for('forgotpasswordview', token="this_email_wasnt@registered") payload = { 'g-recaptcha-response': 'correct_response', 'reset_url': 'http://not_relevant.com', } # Attempt to reset the password for an unregistered email address r = c.post(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 404) self.assertEqual(r.json['error'], 'no such user exists') # Resetting password for the default user should not be permitted url = url_for('forgotpasswordview', token=self.bootstrap_user.email) r = c.post(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 403) # Test a proper change-email request url = url_for('forgotpasswordview', token=self.real_user.email) r = c.post(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 200) # Now let's test GET and PUT requests with the encoded token msg, token = utils.send_email(email_addr=self.real_user.email, email_template=PasswordResetEmail, payload=self.real_user.email) url = url_for('forgotpasswordview', token=token) # Test de-coding and verifying of the token r = c.get(url) self.assertStatus(r, 200) self.assertEqual(r.json['email'], self.real_user.email) # Change the password, then attempt to log-in with the new password payload = {'password1': '123Abc', 'password2': '123Abc'} r = c.put(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 200) url = url_for('userauthview') payload = {'username': self.real_user.email, 'password': '******'} r = c.post(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 200) self.assertEqual(current_user.email, self.real_user.email)
def test_login_and_logout(self): """ tests a login and logout pattern, including incorrect login """ url = url_for('userauthview') payload = {'username': '******', 'password': '******'} r = self.client.post( url, data=json.dumps(payload), headers={'content-type': 'application/json'} ) self.assertStatus(r, 400) # No csrf token = 400 with self.client as c: csrf = self.get_csrf() # Incorrect login should return 401 payload = {'username': '******', 'password': '******'} r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf} ) self.assertStatus(r, 401) # A correct login, but unverified account should return an error payload = { 'username': self.real_user.email, 'password': self.passwords[self.real_user]} r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf} ) self.assertStatus(r, 403) self.assertEqual(r.json['error'], 'account has not been verified') # Correct login on a verified account user_manipulator.update( self.real_user, confirmed_at=datetime.datetime.now() ) r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}, ) self.assertStatus(r, 200) self.assertEqual(current_user.email, self.real_user.email) self.assertEqual(current_user.login_count, 1) self.assertIsInstance(current_user.last_login_at, datetime.date) self.assertAlmostEqual( current_user.last_login_at, datetime.datetime.now(), delta=datetime.timedelta(seconds=1), ) # Test logout r = c.post(url_for('logoutview'),headers={'X-CSRFToken': csrf}) self.assertRaises(AttributeError, lambda: current_user.email)