def update_user(userid, data): """ Update User Resource Representation Args: userid: Path Parameter - Unique ID of User Resource (int) data - dictionary with partial User Resource attributes, loaded from Request body JSON and validated with models.user_schema JWT Baerer Authorization in request.headers - account owner or admin privilege required Returns: Confirmation or Error Message """ user = User.retrieve(userid) if not user: return make_response('Not found', 404) if 'username' in data and User.get_list({'username': data['username']}): current_app.logger.warning( f'create_user() failed. Username={data["username"]} already exists' ) return make_response('Bad request', 400) if current_user.userid != userid and not current_user.get_admin(): current_app.logger.warning( f'update_user(userid={userid}) failed. Userid={current_user.userid} not authorized' ) return make_response('Unauthorized', 401) user.update(**data) return make_response('OK', 200)
def create_user(data): """ Create User Resource Args: data - dictionary with all User Resource attributes, loaded from Request body JSON and validated with models.user_schema JWT Baerer Authorization in request.headers - admin privilege required Returns: Confirmation or Error Message 'Location' Response Header """ if User.get_list({'username': data['username']}): current_app.logger.warning( f'create_user() failed. Username={data["username"]} already exists' ) return make_response('Bad request', 400) # Ignore 'userid' if present in request data # if 'userid' in data: # del(data['userid']) # new_user = User(**data) response = make_response('Created', 201) response.headers['Location'] = url_for( 'users.retrieve_user', userid=new_user.userid, _external=True ) return response
def test_08_remove_user(self): """Test User.remove() method""" with self.app.app_context(): filters = {'username': '******'} users = User.get_list(filters) self.assertEqual(len(users), 1) users[0].remove() users = User.get_list(filters) self.assertEqual(len(users), 0)
def test_09_set_password(self): """Test User.set_password() method""" with self.app.app_context(): filters = {'username': '******'} users = User.get_list(filters) self.assertEqual(len(users), 1) users[0].set_password('pass') user = User.retrieve(users[0].userid) self.assertEqual(user.password, 'pass')
def test_05_user_retrieve(self): """Test User.retrieve() method""" with self.app.app_context(): filters = {'username': '******'} users = User.get_list(filters) self.assertEqual(len(users), 1) user = User.retrieve(users[0].userid) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(users[0].username, user.username)
def test_11_admin(self): """Test User admin methods""" with self.app.app_context(): filters = {'username': '******'} users = User.get_list(filters) self.assertEqual(len(users), 1) users[0].grant_admin() user = User.retrieve(users[0].userid) self.assertTrue(user.get_admin()) user.revoke_admin() user = User.retrieve(users[0].userid) self.assertFalse(user.get_admin())
def test_07_update_user(self): """Test User.retrieve() method""" with self.app.app_context(): filters = {'username': '******'} users = User.get_list(filters) self.assertEqual(len(users), 1) user = User.retrieve(users[0].userid) user.update(lastname='example') user = User.retrieve(users[0].userid) self.assertEqual(users[0].username, user.username) self.assertEqual(user.lastname, 'example') user.update(lastname='Example')
def test_10_lock(self): """Test User lock methods""" with self.app.app_context(): filters = {'username': '******'} users = User.get_list(filters) self.assertEqual(len(users), 1) users[0].set_lock() user = User.retrieve(users[0].userid) self.assertTrue(user.get_lock()) user.unlock() user = User.retrieve(users[0].userid) self.assertFalse(user.get_lock()) self.assertEqual(user.failed_logins, 0) self.assertIsNone(user.last_failed_login)
def list_users(): """ List and filter Users Collection Args: request.args - Query String parameters: filtering, sorting and pagination X-API-Key in request.headers Returns: JSON array of User Resource Representations """ try: filters = users_filters_schema.load(request.args) except ValidationError as e: current_app.logger.warning( f'list_group() Query String validation failed.\nValidationError: {e}' ) return make_response('Bad request', 400) filtered_list = User.get_list(filters) if 'return_fields' in filters: return_fields = filters['return_fields'].split(',') + ['href'] users = UserListSchema(many=True, only=return_fields).dump(filtered_list) else: users = user_list_schema.dump(filtered_list) return jsonify(users)
def set_password(userid, data): """ Set new password for User account Args: userid: Path Parameter - Unique ID of User Resource (int) data - dictionary with Set new password for User attributes, loaded from Request body JSON and validated with models.set_password_body_schema JWT Baerer Authorization in request.headers - account owner or admin privilege required Returns: Confirmation or Error Message """ user = User.retrieve(userid) if not user: return('Not Found', 404) if current_user.userid != userid and not current_user.get_admin(): current_app.logger.warning( f'set_password(userid={userid}) failed. Userid={current_user.userid} not authorized' ) return make_response('Unauthorized', 401) user.set_password(data['password']) return('OK', 200)
def test_02_user_get_list_empty_filters(self): """Test User.get_list() method with empty filters dictionary""" with self.app.app_context(): filters = {} users = User.get_list(filters) self.assertEqual(len(users), 4) self.assertIsInstance(users[0], User)
def test_03_user_get_list_by_username(self): """Test User.get_list() method with username=johne""" with self.app.app_context(): filters = {'username': '******'} users = User.get_list(filters) self.assertEqual(len(users), 1) self.assertEqual(users[0].username, 'johne')
def test_04_user_get_list_by_firstname_lastname(self): """Test User.get_list() method with firstname and lastname""" with self.app.app_context(): filters = {'firstname': 'Linda', 'lastname': 'Sample'} users = User.get_list(filters) self.assertEqual(len(users), 1) self.assertEqual(users[0].username, 'lindas')
def add_user_to_group(groupid, userid): """ Add User to Group Args: groupid: Path Parameter - Unique ID of Group Resource (int) userid: Path Parameter - Unique ID of User Resource (int) JWT Baerer Authorization in request.headers - admin privilege required Returns: Confirmation or Error Message """ group = Group.retrieve(groupid) if group == None: current_app.logger.warning( f'add_user_to_group() Group with id={groupid} not found') return make_response('Group or User not found', 404) user = User.retrieve(userid) if user == None: current_app.logger.warning( f'add_user_to_group() User with id={userid} not found') return make_response('Group or User not found', 404) if user in group.users: return 'User already in the Group', 200 else: group.add_member(user) return 'User added to the Group', 201
def test_12_user_get_list_pagination(self): """Test User.get_list() pagination filters""" with self.app.app_context(): # Database should contain johne, lindas and lin filters = {'sortBy': '-lastname', 'offset': 1, 'limit': 2} users = User.get_list(filters) self.assertEqual(len(users), 2) self.assertEqual(users[0].username, 'lin')
def test_06_create_user_with_duplicate_name(self): """Test User create with duplicate name""" with self.app.app_context(): with self.assertRaises(Exception): duplicate = User(username='******', firstname='John', lastname='Example', email='*****@*****.**', phone='123-444-5555')
def test_01_users_create(self): """Create test Users in database""" with self.app.app_context(): johne = User(username='******', firstname='John', lastname='Example', email='*****@*****.**', phone='123-444-5555') self.assertIsNotNone(johne) self.assertGreater(johne.userid, 0) self.johne_userid = johne.userid lindas = User(username='******', firstname='Linda', lastname='Sample', email='*****@*****.**', phone='123-444-6666') self.assertIsNotNone(lindas) self.assertGreater(lindas.userid, 0) self.assertNotEqual(johne.userid, lindas.userid) lin = User(username='******', firstname='Li', lastname='Nerd', email='*****@*****.**', phone='123-444-7777') self.assertIsNotNone(lin) self.assertGreater(lin.userid, 0) removeme = User(username='******', firstname='Remove', lastname='Me', email='*****@*****.**', phone='123-444-8888') self.assertIsNotNone(removeme) self.assertGreater(removeme.userid, 0)
def read_admin_status(userid): """ Read User account admin privilege status Args: userid: Path Parameter - Unique ID of User Resource (int) JWT Baerer Authorization in request.headers - admin privilege required Returns: JSON Object with admin status or Error Message """ user = User.retrieve(userid) if not user: return('Not Found', 404) return jsonify({'isAdmin': user.get_admin()})
def retrieve_user(userid): """ Retrieve User Resource Representation Args: userid: Path Parameter - Unique ID of User Resource (int) X-API-Key in request.headers Returns: JSON Object with User Resource Representation or Error Message """ user = User.retrieve(userid) if user: return jsonify(user_schema.dump(user)) else: return("Not Found", 404)
def clear_lock(userid): """ Clear User lock status Args: userid: Path Parameter - Unique ID of User Resource (int) JWT Baerer Authorization in request.headers - admin privilege required Returns: Confirmation or Error Message """ user = User.retrieve(userid) if not user: return('Not Found', 404) user.unlock() return('OK', 200)
def remoke_admin_status(userid): """ Revoke admin privilege from User Args: userid: Path Parameter - Unique ID of User Resource (int) JWT Baerer Authorization in request.headers - admin privilege required Returns: Confirmation or Error Message """ user = User.retrieve(userid) if not user: return('Not Found', 404) user.revoke_admin() return('OK', 200)
def delete_user(userid): """ Delete User Resource Args: userid: Path Parameter - Unique ID of User Resource (int) JWT Baerer Authorization in request.headers - admin privilege required Returns: Confirmation or Error Message """ user = User.retrieve(userid) if user: if userid == current_user.userid: current_app.logger.warning( f'delete_user(userid={userid}) failed. Cannot delete self' ) return make_response('Unauthorized', 401) else: user.remove() return make_response('OK', 200) else: return make_response('Not found', 404)
def delete_user_from_group(groupid, userid): """ Delete User from Group Args: groupid: Path Parameter - Unique ID of Group Resource (int) userid: Path Parameter - Unique ID of User Resource (int) JWT Baerer Authorization in request.headers - admin privilege required Returns: Confirmation or Error Message """ group = Group.retrieve(groupid) if group == None: current_app.logger.warning( f'add_user_to_group() Group with id={groupid} not found') return make_response('Group or User not found', 404) user = User.retrieve(userid) if user == None: current_app.logger.warning( f'add_user_to_group() User with id={userid} not found') return make_response('Group or User not found', 404) group.remove_member(user) return 'User deleted from Group', 200
def test_1_login(self): """Test Login operation""" # This test assumes that 'admin' and 'lindas' are in Users, # both have password 'pass', are unlocked and can login # 'admin' must have following values: # username : admin # firstname : Admin # lastname : User # email: [email protected] # phone : 123-444-5555 # Test assumes user 'locked' is in Database with password 'pass', # is locked and unable to login # Test assumes there is no user 'admin1' in Database # Test assumes application configuration parameters # MAX_FAILED_LOGIN_ATTEMPTS and JWT_ACCESS_TOKEN_EXPIRES are set # # Test successful login operation - 200 admin_login_data = {'username': '******', 'password': '******'} resp = self.client.post('/login', json=admin_login_data) self.assertEqual(resp.status_code, 200) # Assert response data is JSON self.assertTrue(resp.is_json) resp_data = resp.get_json() # Assert Response JSON has expected fields self.assertSetEqual({'jwtToken', 'userHref'}, set(resp_data.keys())) # Decode and check returned JWT with self.app.app_context(): decoded_token = flask_jwt_extended.decode_token( resp_data['jwtToken']) # Assert JWT has 'identity', 'iat' and 'exp' fields self.assertIn('identity', decoded_token) self.assertIn('iat', decoded_token) self.assertIn('exp', decoded_token) # Assert DB object with userid==decoded_token['identity'] is in fact 'admin' with self.app.app_context(): user = User.retrieve(decoded_token['identity']) self.assertEqual(user.username, 'admin') # Assert JWT expiration 'exp' is set to Application Config value config_jwt_exp = self.app.config['JWT_ACCESS_TOKEN_EXPIRES'] if type(config_jwt_exp) is int: self.assertEqual(decoded_token['exp'] - decoded_token['iat'], config_jwt_exp) elif type(config_jwt_exp) is datetime.timedelta: self.assertEqual( datetime.timedelta(seconds=decoded_token['exp'] - decoded_token['iat']), config_jwt_exp) else: # We have unsupported type of JWT_ACCESS_TOKEN_EXPIRES. # flask_jwt_extended.create_token() should fail in this case, # resulting in failure of first Login operation. # Code below should not get reached self.fail( msg= f'Config parameter JWT_ACCESS_TOKEN_EXPIRES has unexpected type {type(config_jwt_exp)}' ) # Assert 'userHref' is 'admin' User URI resp1 = self.client.get( resp_data['userHref'], headers={'X-API-Key': self.app.config['API_KEY']}) self.assertEqual(resp1.status_code, 200) resp1_data = resp1.get_json() expected_admin_data = { 'userid': user.userid, 'username': '******', 'firstname': 'Admin', 'lastname': 'User', 'contactInfo': { 'email': '*****@*****.**', 'phone': '123-444-5555' } } self.assertDictEqual(resp1_data, expected_admin_data) # Test correct username and incorrect password - 401 bad_pass_login_data = {'username': '******', 'password': '******'} resp = self.client.post('/login', json=bad_pass_login_data) self.assertEqual(resp.status_code, 401) # Assert no JSON in response data self.assertFalse(resp.is_json) self.assertIsNone(resp.get_json()) # Test incorrect username - 401 bad_username_login_data = {'username': '******', 'password': '******'} resp = self.client.post('/login', json=bad_username_login_data) self.assertEqual(resp.status_code, 401) # Assert no JSON in response data self.assertFalse(resp.is_json) self.assertIsNone(resp.get_json()) # Test locked account - 401 locked_login_data = {'username': '******', 'password': '******'} resp = self.client.post('/login', json=locked_login_data) self.assertEqual(resp.status_code, 401) # Assert no JSON in response data self.assertFalse(resp.is_json) self.assertIsNone(resp.get_json()) # Test incorrect Request body data - 400 bad_login_data = [{ 'user': '******', 'password': '******' }, { 'username': '******' }, { 'username': '******', 'password': 5 }, { 'username': "******", 'password': '' }] for data in bad_login_data: resp = self.client.post('/login', json=data) self.assertEqual(resp.status_code, 400, msg=f'bad_login_data={bad_login_data}') # Assert no JSON in response data self.assertFalse(resp.is_json, msg=f'bad_login_data={bad_login_data}') self.assertIsNone(resp.get_json(), msg=f'bad_login_data={bad_login_data}') # Test invalid JSON format data bad_json = '{"username": "******", "password": "******"' resp = self.client.post('/login', data=bad_json, headers={'Content-Type': 'application/json'}) self.assertEqual(resp.status_code, 400) # Assert no JSON in response data self.assertFalse(resp.is_json) self.assertIsNone(resp.get_json()) # Test 'text/plain' Request body format - 415 text_data = 'random text' resp = self.client.post('/login', data=text_data) self.assertEqual(resp.status_code, 415) # Assert no JSON in response data self.assertFalse(resp.is_json) self.assertIsNone(resp.get_json()) # Test User account lock due to unsuccessful logins # Reset LOCK_TIMEOUT to very short period lock_timeout_config = self.app.config['LOCK_TIMEOUT'] temporary_lock_timeout = 1 self.app.config['LOCK_TIMEOUT'] = datetime.timedelta( seconds=temporary_lock_timeout) # Confirm User lindas is unlocked and can log in lindas_login_data = {'username': '******', 'password': '******'} resp = self.client.post('/login', json=lindas_login_data) self.assertEqual(resp.status_code, 200) # Make MAX_FAILED_LOGIN_ATTEMPTS failed login attempts # User account should not get locked yet incorrect_data = {'username': '******', 'password': '******'} for i in range(self.app.config['MAX_FAILED_LOGIN_ATTEMPTS']): resp = self.client.post('/login', json=incorrect_data) self.assertEqual(resp.status_code, 401, msg=f'i={i}') # Make correct login to verify that account is not locked # and to clear failed logins counter resp = self.client.post('/login', json=lindas_login_data) self.assertEqual(resp.status_code, 200) # Make MAX_FAILED_LOGIN_ATTEMPTS + 1 failed login attempts # User account should get locked in the end for i in range(self.app.config['MAX_FAILED_LOGIN_ATTEMPTS'] + 1): resp = self.client.post('/login', json=incorrect_data) self.assertEqual(resp.status_code, 401, msg=f'i={i}') # Assert User cannot login now resp = self.client.post('/login', json=lindas_login_data) self.assertEqual(resp.status_code, 401) # Sleep for temporary lock timeout (1 sec) time.sleep(temporary_lock_timeout) # Assert lindas can login again now resp = self.client.post('/login', json=lindas_login_data) self.assertEqual(resp.status_code, 200) # Restore LOCK_TIMEOUT self.app.config['LOCK_TIMEOUT'] = lock_timeout_config
def test_3_retrieve_user(self): """Test Retrieve User Resource Representation operation""" # This test assumes 'admin' is in Database with password 'pass', # is unlocked and can log in, has admin privilege # Test expects User 'johne' in Database with following values: # username : johne # firstname : John # lastname : Example # email: [email protected] # phone: 123-444-6666 # # Test correct response - 200 # Get 'johne' userid directly from Database with self.app.app_context(): johne_userid = User.get_list({'username': '******'})[0].userid # Retrieve User Representation of 'johne' resp = self.client.get( f'/users/{johne_userid}', headers={'X-API-Key': self.app.config['API_KEY']}) self.assertEqual(resp.status_code, 200) # Assert response data is JSON self.assertTrue(resp.is_json) user_data = resp.get_json() # Assert response data is exactly what's expected johne = { 'userid': johne_userid, 'username': '******', 'firstname': 'John', 'lastname': 'Example', 'contactInfo': { 'email': '*****@*****.**', 'phone': '123-444-6666' } } self.assertDictEqual(johne, user_data) # Test Unauthorized response - 401 jwt_token = self.login('admin', 'pass') bad_headers = [ # Missing Authorization header {}, # Invalid X-API-Key { 'X-API-Key': self.app.config['API_KEY'] + 'incorrect' }, # valid JWT Bearer token instead of X-API-Key { 'Authorization': f'Bearer {jwt_token}' } ] # Test bad authorization headers for headers in bad_headers: resp = self.client.get(f'/users/{johne_userid}', headers=headers) # Assert Status Code is 401 self.assertEqual(resp.status_code, 401, msg=f'headers={headers}') # Assert no JSON in response data self.assertFalse(resp.is_json, msg=f'headers={headers}') self.assertIsNone(resp.get_json(), msg=f'headers={headers}') # Test response Not Found - 404 # Find largest userid in Database and add 1 with self.app.app_context(): non_existent_userid = User.get_list({'sortBy': '-userid' })[0].userid + 1 resp = self.client.get( f'/users/{non_existent_userid}', headers={'X-API-Key': self.app.config['API_KEY']}) self.assertEqual(resp.status_code, 404) # Assert no JSON in response data self.assertFalse(resp.is_json) self.assertIsNone(resp.get_json())
def setUpClass(cls): """Initialize app and create test_client""" if 'APPUSERS_CONFIG' not in os.environ: os.environ['APPUSERS_CONFIG'] = 'test_config.py' cls.app = create_app() cls.client = cls.app.test_client() with cls.app.app_context(): # Clear existing data in test database meta = db.metadata for table in reversed(meta.sorted_tables): db.session.execute(table.delete()) db.session.commit() # Create user with admin privilege admin_user = User(username='******', firstname='Admin', lastname='User', email='*****@*****.**', phone='123-444-5555') admin_user.set_password('pass') admin_user.grant_admin() # Create other Users and Groups johne = User(username='******', firstname='John', lastname='Example', email='*****@*****.**', phone='123-444-6666') lindas = User(username='******', firstname='Linda', lastname='Sample', email='*****@*****.**', phone='123-444-7777') lindas.set_password('pass') lin = User(username='******', firstname='Li', lastname='Nerd', email='*****@*****.**', phone='123-444-8888') locked = User(username='******', firstname='Locked', lastname='Account', email='*****@*****.**', phone='123-444-9999') locked.set_password('pass') locked.set_lock() devs = Group(groupname='devs', description='Developers') testers = Group(groupname='testers', description='Testers')
# Module level set-up for all unit tests in this file if 'APPUSERS_CONFIG' not in os.environ: os.environ['APPUSERS_CONFIG'] = 'test_config.py' app = create_app() with app.app_context(): # Clear existing data in test database meta = db.metadata for table in reversed(meta.sorted_tables): db.session.execute(table.delete()) db.session.commit() # Create user with admin privilege admin_user = User(username='******', firstname='Admin', lastname='User', email='*****@*****.**', phone='123-444-5555') admin_user.set_password('pass') admin_user.grant_admin() # End of module level set-up def login(client, username, password): """Login helper function User login with application test client. Arguments: client - Flask Test Client
def load_current_user(identity): """Load User object for JWTManager, using Token's identity""" return User.retrieve(identity)
def login(data): """ Login operation Args: data - dictionary with all Login operation attributes, loaded from Request body JSON and validated with models.login_body_schema Returns: JSON with JWT Token and User link or Error Message. Token contains 'identity' field set to userid of authenticated User. Token expires after JWT_ACCESS_TOKEN_EXPIRES. """ user_list = User.get_list({'username': data['username']}) if len(user_list) == 0: current_app.logger.warning( f'authenticate_user() failed. No such user: {data["username"]}' ) return make_response('Unathorized', 401) else: user = user_list[0] # check if User account is locked, lift lock if lock interval passed if user.last_failed_login and datetime.now() > user.last_failed_login + current_app.config['LOCK_TIMEOUT']: user.unlock() current_app.logger.info( f'authenticate_user() - userid={user.userid} unlocked due to lock timeout' ) if user.get_lock(): current_app.logger.warning( f'authenticate_user() failed. Userid={user.userid} is locked' ) return make_response('Unathorized', 401) if safe_str_cmp(user.password.encode('utf-8'), data['password'].encode('utf-8')): # clear lock info on successful login user.unlock() access_token = create_access_token(identity=user.userid) response = {'jwtToken': access_token, 'userHref': url_for( 'users.retrieve_user', userid=user.userid, _external=True )} current_app.logger.info( f'authenticate_user() successful. {user.username} logged in' ) return(jsonify(response), 200) else: current_app.logger.warning( f'authenticate_user() failed. Incorrect password for userid={user.userid}' ) # update lock status user.failed_logins = user.failed_logins + 1 user.last_failed_login = datetime.now() # consider datetime.utcnow() if user.failed_logins > current_app.config['MAX_FAILED_LOGIN_ATTEMPTS']: user.set_lock() current_app.logger.warning( f'Too many failed logins for userid={user.userid}, account locked' ) user.update() return make_response('Unauthorized', 401)