def post(self, request, user_name): """ Create a new user Args: request: Django rest framework request user_name: User name of the user to create Returns: None Note: User's data is passed as json data in the request """ user_data = request.data.copy() # Keep track of what has been created, so in the catch block we can # delete them when there is an error in another step of create user user_created = False try: with KeyCloakClient('BOSS') as kc: # DP NOTE: email also has to be unique, in the current configuration of Keycloak data = { "username": user_name, "firstName": user_data.get('first_name'), "lastName": user_data.get('last_name'), "email": user_data.get('email'), "enabled": True } data = json.dumps(data) response = kc.create_user(data) user_created = True data = { "type": "password", "temporary": False, "value": user_data.get('password') } kc.reset_password(user_name, data) return Response(status=201) except KeyCloakError: # cleanup created objects if True in [user_created]: try: with KeyCloakClient('BOSS') as kc: try: if user_created: kc.delete_user(user_name) except: LOG.exception( "Error deleting user '{}'".format(user_name)) except: LOG.exception( "Error communicating with Keycloak to delete created user and primary group" ) msg = "Error addng user '{}' to Keycloak".format(user_name) return BossKeycloakError(msg)
def post(self, request, user_name): """ Create a new user if the user does not exist Args: request: Django rest framework request user_name: User name from the request Returns: Http status of the request """ user_data = request.data.copy() # Keep track of what has been created, so in the catch block we can # delete them when there is an error in another step of create user user_created = False try: with KeyCloakClient('BOSS') as kc: # Create the user account, attached to the default groups # DP NOTE: email also has to be unique, in the current configuration of Keycloak data = { "username": user_name, "firstName": user_data.get('first_name'), "lastName": user_data.get('last_name'), "email": user_data.get('email'), "enabled": True } data = json.dumps(data) response = kc.create_user(data) user_create = True data = { "type": "password", "temporary": False, "value": user_data.get('password') } kc.reset_password(user_name, data) return Response(response, status=201) except Exception as e: # cleanup created objects if True in [user_created]: try: with KeyCloakClient('BOSS') as kc: try: if user_created: kc.delete_user(user_name) except: LOG.exception( "Error deleting user '{}'".format(user_name)) except: LOG.exception( "Error communicating with Keycloak to delete created user and primary group" ) msg = "Error addng user '{}' to Keycloak".format(user_name) return BossHTTPError.from_exception(e, 404, msg, 30000)
def post(self, request, user_name, role_name): """ Assign a role to a user Args: request: Django rest framework request user_name: User name role_name : Role name Returns: Http status of the request """ try: if role_name not in ['admin', 'user-manager', 'resource-manager']: return BossHTTPError(404, "Invalid role name {}".format(role_name), 30000) with KeyCloakClient('BOSS') as kc: response = kc.map_role_to_user(user_name, role_name) return Response(serializer.data, status=201) except Exception as e: return BossHTTPError( 404, "Unable to map role {} to user {} in keycloak. {}".format( role_name, user_name, e), 30000)
def delete(self, request, user_name, role_name): """ Delete a user Args: request: Django rest framework request user_name: User name from the request role_name: Role name from the request Returns: Http status of the request """ try: if role_name not in ['admin', 'user-manager', 'resource-manager']: return BossHTTPError(404, "Invalid role name {}".format(role_name), 30000) with KeyCloakClient('BOSS') as kc: response = kc.remove_role_from_user(user_name, role_name) return Response(status=204) except Exception as e: return BossHTTPError( 404, "Unable to remove role {} from user {} in keycloak. {}".format( role_name, user_name, e), 30000)
def get(self, request, user_name, role_name=None): """ Multi-function method 1) If role_name is None, return all roles assigned to the user 2) If role_name is not None, return True/False if the user is assigned the given role Args: request: Django rest framework request user_name: User name of the user to check role_name: Name of the role to check, or None to return all roles Returns: True if the user has the role or a list of all assigned roles """ try: with KeyCloakClient('BOSS') as kc: resp = kc.get_realm_roles(user_name) roles = [r['name'] for r in resp] roles = filter_roles(roles) if role_name is None: return Response(roles, status=200) else: exists = role_name in roles return Response(exists, status=200) except KeyCloakError: msg = "Error getting user '{}' role's from Keycloak".format( user_name) return BossKeycloakError(msg)
def get(self, request, user_name, role_name=None): """ Check if the user has a specific role Args: request: Django rest framework request user_name: User name role_name: Returns: True if the user has the role """ try: with KeyCloakClient('BOSS') as kc: resp = kc.get_realm_roles(user_name) roles = [r['name'] for r in resp] # DP TODO: filter roles array to limit to valid roles?? if role_name is None: return Response(roles, status=200) else: valid = ['admin', 'user-manager', 'resource-manager'] if role_name not in valid: return BossHTTPError( 404, "Invalid role name {}".format(role_name), 30000) exists = role_name in roles return Response(exists, status=200) except Exception as e: return BossHTTPError( 404, "Error getting user's {} roles from keycloak. {}".format( user_name, e), 30000)
def delete(self, request, user_name, role_name): """ Unasign a role from a user Args: request: Django rest framework request user_name: User name of user to unassign role from role_name : Role name of role to unassign from user Returns: None """ # DP NOTE: admin role has to be removed manually in Keycloak if role_name == 'admin': return BossHTTPError("Cannot remove 'admin' role", ErrorCodes.INVALID_ROLE) # DP NOTE: user-manager role can only be modified by an admin if role_name == 'user-manager': resp = check_for_admin(request.user) if resp is not None: return resp try: with KeyCloakClient('BOSS') as kc: response = kc.remove_role_from_user(user_name, role_name) return Response(status=204) except KeyCloakError: msg = "Unable to remove role '{}' from user '{}' in Keycloak".format( role_name, user_name) return BossKeycloakError(msg)
def get(self, request, user_name=None): """ Get information about a user Args: request: Django rest framework request user_name: User name to get information about Returns: JSON dictionary of user data """ try: with KeyCloakClient('BOSS') as kc: if user_name is None: # Get all users search = request.GET.get('search') response = kc.get_all_users(search) return Response(response, status=200) else: response = kc.get_userdata(user_name) roles = kc.get_realm_roles(user_name) response["realmRoles"] = filter_roles( [r['name'] for r in roles]) return Response(response, status=200) except KeyCloakError: msg = "Error getting user '{}' from Keycloak".format(user_name) return BossKeycloakError(msg)
def delete(self, request, user_name): """ Delete a user Args: request: Django rest framework request user_name: User name of user to delete Returns: None """ # DP TODO: verify user_name is not an admin if user_name == 'bossadmin': msg = "Cannot delete user bossadmin from Keycloak".format( user_name) return BossKeycloakError(msg) else: try: with KeyCloakClient('BOSS') as kc: kc.delete_user(user_name) return Response(status=204) except KeyCloakError: msg = "Error deleting user '{}' from Keycloak".format( user_name) return BossKeycloakError(msg)
def setUp(self): """Create the KeyCloakClient object and override login/logout with mock versions""" self.client = KeyCloakClient(REALM, BASE) # Have to use MethodType to bind the mock method to the specific instance # of the KeyCloakClient instead of binding it to all instances of the # class # See http://stackoverflow.com/questions/972/adding-a-method-to-an-existing-object-instance self.client.login = MethodType(mock_login, self.client) self.client.logout = MethodType(mock_logout, self.client)
def user_exist(self, uid): """Cache the results of looking up the user in Keycloak""" if not LOCAL_KEYCLOAK_TESTING: with KeyCloakClient('BOSS') as kc: return kc.user_exist(uid) # Code for local testing with KeyCloak. try: kc = KeyCloakClient('BOSS', url_base='http://localhost:8080/auth', https=False) kc.login(username=KEYCLOAK_ADMIN_USER, password=KEYCLOAK_ADMIN_PASSWORD, client_id='admin-cli', login_realm='master') return kc.user_exist(uid) finally: kc.logout()
def delete(self, request, user_name): """ Delete a user Args: request: Django rest framework request user_name: User name from the request Returns: Http status of the request """ try: # Delete from Keycloak with KeyCloakClient('BOSS') as kc: kc.delete_user(user_name) return Response(status=204) except Exception as e: msg = "Error deleting user '{}' from Keycloak".format(user_name) return BossHTTPError.from_exception(e, 404, msg, 30000)
def get(self, request, user_name): """ Get the user information Args: request: Django rest framework request user_name: User name from the request Returns: User if the user exists """ try: with KeyCloakClient('BOSS') as kc: response = kc.get_userdata(user_name) roles = kc.get_realm_roles(user_name) response["realmRoles"] = [r['name'] for r in roles] return Response(response, status=200) except Exception as e: msg = "Error getting user '{}' from Keycloak".format(user_name) return BossHTTPError.from_exception(e, 404, msg, 30000)
def delete(self, request, user_name): """ Delete a user Args: request: Django rest framework request user_name: User name of user to delete Returns: None """ try: with KeyCloakClient('BOSS') as kc: kc.delete_user(user_name) return Response(status=204) except KeyCloakError: msg = "Error deleting user '{}' from Keycloak".format(user_name) return BossKeycloakError(msg)
def post(self, request, user_name, role_name): """ Assign a role to a user Args: request: Django rest framework request user_name: User name of user to assign role to role_name : Role name of role to assign to user Returns: None """ try: with KeyCloakClient('BOSS') as kc: response = kc.map_role_to_user(user_name, role_name) return Response(status=201) except KeyCloakError: msg = "Unable to map role '{}' to user '{}' in Keycloak".format( role_name, user_name) return BossKeycloakError(msg)
def delete(self, request, user_name, role_name): """ Unasign a role from a user Args: request: Django rest framework request user_name: User name of user to unassign role from role_name : Role name of role to unassign from user Returns: None """ try: with KeyCloakClient('BOSS') as kc: response = kc.remove_role_from_user(user_name, role_name) return Response(status=204) except KeyCloakError: msg = "Unable to remove role '{}' from user '{}' in Keycloak".format( role_name, user_name) return BossKeycloakError(msg)
def get_keycloak_client(self): """ Get the KeyCloak client. If LOCAL_KEYCLOAK_TESTING set in the Django settings, logs in using to the local test KeyCloak server. Returns: (KeyCloakClient) """ if LOCAL_KEYCLOAK_TESTING: kc = KeyCloakClient('BOSS', url_base='http://localhost:8080/auth', https=False) kc.login(username=KEYCLOAK_ADMIN_USER, password=KEYCLOAK_ADMIN_PASSWORD, client_id='admin-cli', login_realm='master') else: kc = KeyCloakClient('BOSS') return kc
def setUp(self): """Create the KeyCloakClient object and override previously tested internal methods with mock versions""" self.client = KeyCloakClient(REALM, BASE)
class TestAuthentication(unittest.TestCase): """Test the login/logout methods and the context manager that wraps login/logout""" def setUp(self): self.client = KeyCloakClient(REALM, BASE) @mock.patch(prefix + 'keycloak.Vault', autospec = True) @mock.patch(prefix + 'keycloak.requests.post', autospec = True) def test_login(self, mPost, mVault): """Test that login() make the correct POST call with data from Vault and created the internal token variable""" mPost.return_value = MockResponse(200, json.dumps(TOKEN)) mVault.return_value.read = lambda p, k: k self.assertIsNone(self.client.token) self.client.login() self.assertEqual(self.client.token, TOKEN) # DP NOTE: since the post call contains information read from Vault # I don't explicitly check for the calls to Vault, because # if they didn't happen, they the post call assert will fail url = BASE + '/realms/realm/protocol/openid-connect/token' data = {'grant_type': 'password', 'client_id': 'client_id', 'username': '******', 'password': '******' } call = mock.call(url, data = data, verify = True) self.assertEqual(mPost.mock_calls, [call]) @mock.patch(prefix + 'keycloak.Vault', autospec = True) @mock.patch(prefix + 'keycloak.requests.post', autospec = True) def test_failed_login(self, mPost, mVault): """Test that if there is an error from Keycloak when logging in that a KeyCloakError is raised""" mPost.return_value = MockResponse(500, '[]') mVault.return_value.read = lambda p, k: k with self.assertRaises(KeyCloakError): self.client.login() @mock.patch(prefix + 'keycloak.requests.post', autospec = True) def test_logout(self, mPost): """Test that logout() makes the correct POST call with data used in login and clears the token variable""" mPost.return_value = MockResponse(200, '[]') self.client.login = MethodType(mock_login, self.client) # See comment in TestRequests.setUp() self.client.login() self.assertEqual(self.client.token, TOKEN) self.client.logout() self.assertIsNone(self.client.token) url = BASE + '/realms/realm/protocol/openid-connect/logout' data = {'refresh_token': 'refresh_token', 'client_id': 'client_id' } call = mock.call(url, data = data, verify = True) self.assertEqual(mPost.mock_calls, [call]) @mock.patch(prefix + 'keycloak.requests.post', autospec = True) def test_failed_logout(self, mPost): """Test that if there is an error from Keycloak when logging out that a KeyCloakError is raised""" mPost.return_value = MockResponse(500, '[]') self.client.login = MethodType(mock_login, self.client) # See comment in TestRequests.setUp() self.client.login() with self.assertRaises(KeyCloakError): self.client.logout() def test_with(self): """Test that that when in a 'using' block that login and logout are called""" self.client.loginCalled = False self.client.logoutCalled = False def call_login(self): self.loginCalled = True def call_logout(self): self.logoutCalled = True self.client.login = MethodType(call_login, self.client) self.client.logout = MethodType(call_logout, self.client) with self.client as kc: pass self.assertTrue(self.client.loginCalled) self.assertTrue(self.client.logoutCalled) def test_failed_with(self): """Test that if there is an exception in a 'using' block that it is propogated correctly""" self.client.loginCalled = False self.client.logoutCalled = False def call_login(self): self.loginCalled = True def call_logout(self): self.logoutCalled = True self.client.login = MethodType(call_login, self.client) self.client.logout = MethodType(call_logout, self.client) with self.assertRaises(Exception): with self.client as kc: raise Exception() self.assertTrue(self.client.loginCalled) self.assertTrue(self.client.logoutCalled) def test_failed_with_login(self): """Test that when in a 'using' block if login() fails logout() is not called""" self.client.loginCalled = False self.client.logoutCalled = False def call_login(self): self.loginCalled = True raise Exception() def call_logout(self): self.logoutCalled = True self.client.login = MethodType(call_login, self.client) self.client.logout = MethodType(call_logout, self.client) with self.assertRaises(Exception): with self.client as kc: pass self.assertTrue(self.client.loginCalled) self.assertFalse(self.client.logoutCalled) def test_failed_with_logout(self): """Test that when in a 'using' block if logout() fails no exception is propogated""" self.client.loginCalled = False self.client.logoutCalled = False def call_login(self): self.loginCalled = True def call_logout(self): self.logoutCalled = True raise Exception() self.client.login = MethodType(call_login, self.client) self.client.logout = MethodType(call_logout, self.client) with self.client as kc: pass self.assertTrue(self.client.loginCalled) self.assertTrue(self.client.logoutCalled)
def setUp(self): self.client = KeyCloakClient(REALM, BASE)
class TestClient(unittest.TestCase): """Test the client methods used to interact with Keycloak""" def setUp(self): """Create the KeyCloakClient object and override previously tested internal methods with mock versions""" self.client = KeyCloakClient(REALM, BASE) # DP NOTE: login / logout are not mocked, because the methods are called without logging in or out @staticmethod def makeMM(arg): """Make a mock object and set the return value(s) Args: arg : Vale or list of Values for mock object to return when called Returns: mock.MagicMock : mock object """ mm = mock.MagicMock() if type(arg) == type([]): mm.side_effect = arg else: mm.return_value = arg return mm def setReturn(self, get=None, post=None, put=None, delete=None, get_user_id=False, get_role_by_name=False): """Mock up the return values for the different HTTP methods of KeyCloakClient Args: get : Value or Iterable of values to return for GETs post : Value or Iterable of values to return for POSTs put : Value or Iterable of values to return for PUTs delete : Value or Iterable of values to return for DELETEs get_user_id (bool) : If KeyCloakClient.get_user_id should be stubbed out get_role_by_name (bool) : If KeyCloakClient.get_role_by_name should be stubbed out """ make = lambda m: TestClient.makeMM(m) self.client._get = make(get) self.client._post = make(post) self.client._put = make(put) self.client._delete = make(delete) if get_user_id: self.client.get_user_id = make(0) if get_role_by_name: self.client.get_role_by_name = make({}) def test_get_userdata(self): username = '******' data = {'username': username} self.setReturn(get=MockResponse(200, json.dumps(data)), get_user_id=True) response = self.client.get_userdata(username) self.assertEqual(data, response) url = 'users/0' # user id set in setReturn call = mock.call(url) self.assertEqual(self.client._get.mock_calls, [call]) self.assertEqual(self.client.get_user_id.call_count, 1) def test_get_userinfo(self): data = {'test': 'test'} self.setReturn(get=MockResponse(200, json.dumps(data))) response = self.client.get_userinfo() self.assertEqual(data, response) url = 'protocol/openid-connect/userinfo' call = mock.call(url) self.assertEqual(self.client._get.mock_calls, [call]) def test_create_user(self): data = {'test': 'test'} self.setReturn() response = self.client.create_user(data) self.assertIsNone(response) call = mock.call('users', data) self.assertEqual(self.client._post.mock_calls, [call]) def test_reset_password(self): data = {'test': 'test'} self.setReturn(get_user_id = True) response = self.client.reset_password('test', data) self.assertIsNone(response) call = mock.call('users/0/reset-password', data) self.assertEqual(self.client._put.mock_calls, [call]) self.assertEqual(self.client.get_user_id.call_count, 1) def test_delete_user(self): self.setReturn(get_user_id = True) response = self.client.delete_user('test') self.assertIsNone(response) call = mock.call('users/0') self.assertEqual(self.client._delete.mock_calls, [call]) self.assertEqual(self.client.get_user_id.call_count, 1) def test_get_user_id(self): username = '******' params = {'username': username} self.setReturn(get=MockResponse(200, '[{"id": 0}]')) response = self.client.get_user_id(username) self.assertEqual(0, response) call = mock.call('users', params) self.assertEqual(self.client._get.mock_calls, [call]) def test_failed_get_user_id(self): username = '******' params = {'username': username} self.setReturn(get=MockResponse(500, '[]')) with self.assertRaises(KeyCloakError): self.client.get_user_id(username) call = mock.call('users', params) self.assertEqual(self.client._get.mock_calls, [call]) def test_get_realm_roles(self): data = {'test': 'test'} self.setReturn(get=MockResponse(200, json.dumps(data)), get_user_id=True) response = self.client.get_realm_roles('test') self.assertEqual(data, response) call = mock.call('users/0/role-mappings/realm') self.assertEqual(self.client._get.mock_calls, [call]) self.assertEqual(self.client.get_user_id.call_count, 1) def test_get_role_by_name(self): data = {'test': 'test'} self.setReturn(get=MockResponse(200, json.dumps(data))) response = self.client.get_role_by_name('test') self.assertEqual(data, response) call = mock.call('roles/test') self.assertEqual(self.client._get.mock_calls, [call]) def test_failed_get_role_by_name(self): self.setReturn(get=MockResponse(500, '[]')) with self.assertRaises(KeyCloakError): self.client.get_role_by_name('test') call = mock.call('roles/test') self.assertEqual(self.client._get.mock_calls, [call]) def test_map_role_to_user(self): self.setReturn(get_user_id = True, get_role_by_name = True) response = self.client.map_role_to_user('test', 'test') self.assertIsNone(response) call = mock.call('users/0/role-mappings/realm', '[{}]') self.assertEqual(self.client._post.mock_calls, [call]) def test_remove_role_from_user(self): self.setReturn(get_user_id = True, get_role_by_name = True) response = self.client.remove_role_from_user('test', 'test') self.assertIsNone(response) call = mock.call('users/0/role-mappings/realm', '[{}]') self.assertEqual(self.client._delete.mock_calls, [call])
def user_exist(self, uid): """Cache the results of looking up the user in Keycloak""" with KeyCloakClient('BOSS') as kc: return kc.user_exist(uid)