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 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 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 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_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 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 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 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 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 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 load_current_user(identity): """Load User object for JWTManager, using Token's identity""" return User.retrieve(identity)