From 1029e46a68c5013ae160f69d58f3ae69e371a42f Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 26 May 2022 21:56:55 +0200 Subject: [PATCH 1/2] feat: added new methods for client scopes --- src/keycloak/keycloak_admin.py | 62 +++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 025d9e8..fba28c2 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2108,6 +2108,22 @@ class KeycloakAdmin: data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_client_scope_by_name(self, client_scope_name): + """ + Get representation of the client scope identified by the client scope name. + + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes + :param client_scope_name: (str) Name of the client scope + :returns: ClientScopeRepresentation or None + """ + + client_scopes = self.get_client_scopes() + for client_scope in client_scopes: + if client_scope["name"] == client_scope_name: + return client_scope + + return None + def create_client_scope(self, payload, skip_exists=False): """ Create a client scope @@ -2117,16 +2133,24 @@ class KeycloakAdmin: :param payload: ClientScopeRepresentation :param skip_exists: If true then do not raise an error if client scope already exists - :return: Keycloak server response (ClientScopeRepresentation) + :return: Client scope id """ + if skip_exists: + exists = self.get_client_scope_by_name(client_scope_name=payload["name"]) + + if exists is not None: + return exists["id"] + params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( urls_patterns.URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response( + raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def update_client_scope(self, client_scope_id, payload): """ @@ -2146,6 +2170,34 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) + def delete_client_scope(self, client_scope_id): + """ + Delete existing client scope. + + ClientScopeRepresentation: + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_client_scopes_resource + + :param client_scope_id: The id of the client scope + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) + + def get_mappers_from_client_scope(self, client_scope_id): + """ + Get a list of all mappers connected to the client scope + + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocol_mappers_resource + :param client_scope_id: Client scope id + :returns: Keycloak server response (ProtocolMapperRepresentation) + """ + params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope @@ -2165,20 +2217,20 @@ class KeycloakAdmin: return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) - def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): + def delete_mapper_from_client_scope(self, client_scope_id, protocol_mapper_id): """ Delete a mapper from a client scope https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_delete_mapper :param client_scope_id: The id of the client scope - :param payload: ProtocolMapperRepresentation + :param protocol_mapper_id: Protocol mapper id :return: Keycloak server Response """ params_path = { "realm-name": self.realm_name, "scope-id": client_scope_id, - "protocol-mapper-id": protocol_mppaer_id, + "protocol-mapper-id": protocol_mapper_id, } data_raw = self.raw_delete( From 2e5d75198557b4c6658d1e2ad7e5c23970d9f0d6 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 26 May 2022 21:57:08 +0200 Subject: [PATCH 2/2] test: added tests for client scopes --- tests/test_keycloak_admin.py | 139 +++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index b0f1948..58d298f 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1243,3 +1243,142 @@ def test_sync_users(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakPostError) as err: admin.sync_users(storage_id="does-not-exist", action="triggerFullSync") assert err.match('404: b\'{"error":"Could not find component"}\'') + + +def test_client_scopes(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Test get client scopes + res = admin.get_client_scopes() + scope_names = {x["name"] for x in res} + assert len(res) == 10 + assert "email" in scope_names + assert "profile" in scope_names + assert "offline_access" in scope_names + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_scope(client_scope_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client scope"}\'') + + scope = admin.get_client_scope(client_scope_id=res[0]["id"]) + assert res[0] == scope + + scope = admin.get_client_scope_by_name(client_scope_name=res[0]["name"]) + assert res[0] == scope + + # Test create client scope + res = admin.create_client_scope(payload={"name": "test-scope"}, skip_exists=True) + assert res + res2 = admin.create_client_scope(payload={"name": "test-scope"}, skip_exists=True) + assert res == res2 + with pytest.raises(KeycloakPostError) as err: + admin.create_client_scope(payload={"name": "test-scope"}, skip_exists=False) + assert err.match('409: b\'{"errorMessage":"Client Scope test-scope already exists"}\'') + + # Test update client scope + with pytest.raises(KeycloakPutError) as err: + admin.update_client_scope(client_scope_id="does-not-exist", payload=dict()) + assert err.match('404: b\'{"error":"Could not find client scope"}\'') + + res_update = admin.update_client_scope( + client_scope_id=res, payload={"name": "test-scope-update"} + ) + assert res_update == dict() + admin.get_client_scope(client_scope_id=res)["name"] == "test-scope-update" + + # Test get mappers + mappers = admin.get_mappers_from_client_scope(client_scope_id=res) + assert mappers == list() + + # Test add mapper + with pytest.raises(KeycloakPostError) as err: + admin.add_mapper_to_client_scope(client_scope_id=res, payload=dict()) + assert err.match('404: b\'{"error":"ProtocolMapper provider not found"}\'') + + res_add = admin.add_mapper_to_client_scope( + client_scope_id=res, + payload={ + "name": "test-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + }, + ) + assert res_add == b"" + assert len(admin.get_mappers_from_client_scope(client_scope_id=res)) == 1 + + # Test update mapper + test_mapper = admin.get_mappers_from_client_scope(client_scope_id=res)[0] + with pytest.raises(KeycloakPutError) as err: + admin.update_mapper_in_client_scope( + client_scope_id="does-not-exist", protocol_mapper_id=test_mapper["id"], payload=dict() + ) + assert err.match('404: b\'{"error":"Could not find client scope"}\'') + test_mapper["config"]["user.attribute"] = "test" + res_update = admin.update_mapper_in_client_scope( + client_scope_id=res, + protocol_mapper_id=test_mapper["id"], + payload=test_mapper, + ) + assert res_update == dict() + assert ( + admin.get_mappers_from_client_scope(client_scope_id=res)[0]["config"]["user.attribute"] + == "test" + ) + + # Test delete mapper + res_del = admin.delete_mapper_from_client_scope( + client_scope_id=res, protocol_mapper_id=test_mapper["id"] + ) + assert res_del == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_mapper_from_client_scope( + client_scope_id=res, protocol_mapper_id=test_mapper["id"] + ) + assert err.match('404: b\'{"error":"Model not found"}\'') + + # Test default default scopes + res_defaults = admin.get_default_default_client_scopes() + assert len(res_defaults) == 6 + + with pytest.raises(KeycloakPutError) as err: + admin.add_default_default_client_scope(scope_id="does-not-exist") + assert err.match('404: b\'{"error":"Client scope not found"}\'') + + res_add = admin.add_default_default_client_scope(scope_id=res) + assert res_add == dict() + assert len(admin.get_default_default_client_scopes()) == 7 + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_default_default_client_scope(scope_id="does-not-exist") + assert err.match('404: b\'{"error":"Client scope not found"}\'') + + res_del = admin.delete_default_default_client_scope(scope_id=res) + assert res_del == dict() + assert len(admin.get_default_default_client_scopes()) == 6 + + # Test default optional scopes + res_defaults = admin.get_default_optional_client_scopes() + assert len(res_defaults) == 4 + + with pytest.raises(KeycloakPutError) as err: + admin.add_default_optional_client_scope(scope_id="does-not-exist") + assert err.match('404: b\'{"error":"Client scope not found"}\'') + + res_add = admin.add_default_optional_client_scope(scope_id=res) + assert res_add == dict() + assert len(admin.get_default_optional_client_scopes()) == 5 + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_default_optional_client_scope(scope_id="does-not-exist") + assert err.match('404: b\'{"error":"Client scope not found"}\'') + + res_del = admin.delete_default_optional_client_scope(scope_id=res) + assert res_del == dict() + assert len(admin.get_default_optional_client_scopes()) == 4 + + # Test client scope delete + res_del = admin.delete_client_scope(client_scope_id=res) + assert res_del == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_client_scope(client_scope_id=res) + assert err.match('404: b\'{"error":"Could not find client scope"}\'')