From 7df92332fd12fe5e6bba0ceb80b0cb4beda58f10 Mon Sep 17 00:00:00 2001 From: Geoff Owen Date: Sun, 11 Feb 2024 15:06:38 +0000 Subject: [PATCH] feat: Adding additional methods to support roles-by-id api calls Most of the methods rely on the role name within python keycloak, which for the vast majority is fine, however there are some role names which cannot be used by the API endpoint as they contain characters that cannot be encoded properly. Therefore this change is to allow the use of the role's id to get, update and delete roles by their id instead.' --- src/keycloak/keycloak_admin.py | 54 +++++++++++++++++++++++++++++++++ src/keycloak/keycloak_openid.py | 4 +-- src/keycloak/urls_patterns.py | 1 + tests/test_keycloak_admin.py | 29 ++++++++++++++++++ tests/test_keycloak_openid.py | 4 +-- tox.ini | 3 ++ 6 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 86649fc..07790b2 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2569,6 +2569,60 @@ class KeycloakAdmin: urls_patterns.URL_ADMIN_CLIENT_ROLE_GROUPS.format(**params_path), query ) + def get_role_by_id(self, role_id): + """Get a specific role’s representation. + + RoleRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + + :param role_id: id of role + :type role_id: str + :return: Keycloak server response (RoleRepresentation) + :rtype: bytes + """ + params_path = {"realm-name": self.connection.realm_name, "role-id": role_id} + data_raw = self.connection.raw_get( + urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_ID.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + + def update_role_by_id(self, role_id, payload): + """Update the role. + + RoleRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + + :param payload: RoleRepresentation + :type payload: dict + :param role_id: id of role + :type role_id: str + :returns: Keycloak server response + :rtype: bytes + """ + params_path = {"realm-name": self.connection.realm_name, "role-id": role_id} + data_raw = self.connection.raw_put( + urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_ID.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) + + def delete_role_by_id(self, role_id): + """Delete a role by its id. + + RoleRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + + :param role_id: id of role + :type role_id: str + :returns: Keycloak server response + :rtype: bytes + """ + params_path = {"realm-name": self.connection.realm_name, "role-id": role_id} + data_raw = self.connection.raw_delete( + urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_ID.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) + def create_realm_role(self, payload, skip_exists=False): """Create a new role for the realm or client. diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index bb52ea2..1bcc7f7 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -736,9 +736,7 @@ class KeycloakOpenID: :rtype: dict """ params_path = {"realm-name": self.realm_name} - payload = { - "client_id": self.client_id, - } + payload = {"client_id": self.client_id} payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_DEVICE.format(**params_path), data=payload) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 0531bd8..9db374f 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -43,6 +43,7 @@ URL_CLIENT_REGISTRATION = URL_REALM + "/clients-registrations/default" URL_CLIENT_UPDATE = URL_CLIENT_REGISTRATION + "/{client-id}" # ADMIN URLS +URL_ADMIN_ROOT = "admin/realms/{realm-name}" URL_ADMIN_USERS = "admin/realms/{realm-name}/users" URL_ADMIN_USERS_COUNT = "admin/realms/{realm-name}/users/count" URL_ADMIN_USER = "admin/realms/{realm-name}/users/{id}" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 7e6aa6a..a9a5904 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1763,6 +1763,35 @@ def test_client_roles(admin: KeycloakAdmin, client: str): admin.delete_client_role(client_role_id=client, role_name="client-role-test-update") assert err.match('404: b\'{"error":"Could not find role"}\'') + # Test of roles by id - Get role + admin.create_client_role( + client_role_id=client, payload={"name": "client-role-by-id-test"}, skip_exists=True + ) + role = admin.get_client_role(client_id=client, role_name="client-role-by-id-test") + res = admin.get_role_by_id(role_id=role["id"]) + assert res["name"] == "client-role-by-id-test" + with pytest.raises(KeycloakGetError) as err: + admin.get_role_by_id(role_id="bad") + assert err.match('404: b\'{"error":"Could not find role with id"}\'') + + # Test of roles by id - Update role + res = admin.update_role_by_id( + role_id=role["id"], payload={"name": "client-role-by-id-test-update"} + ) + assert res == dict() + with pytest.raises(KeycloakPutError) as err: + res = admin.update_role_by_id( + role_id="bad", payload={"name": "client-role-by-id-test-update"} + ) + assert err.match('404: b\'{"error":"Could not find role with id"}\'') + + # Test of roles by id - Delete role + res = admin.delete_role_by_id(role_id=role["id"]) + assert res == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_role_by_id(role_id="bad") + assert err.match('404: b\'{"error":"Could not find role with id"}\'') + def test_enable_token_exchange(admin: KeycloakAdmin, realm: str): """Test enable token exchange. diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 171d70d..76e7264 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -208,9 +208,7 @@ def test_exchange_token( # Exchange token with the new user new_token = oid.exchange_token( - token=token["access_token"], - audience=oid.client_id, - subject=username, + token=token["access_token"], audience=oid.client_id, subject=username ) assert oid.userinfo(token=new_token["access_token"]) == { "email": f"{username}@test.test", diff --git a/tox.ini b/tox.ini index 419dc81..503ec2c 100644 --- a/tox.ini +++ b/tox.ini @@ -14,18 +14,21 @@ commands = isort -c --df src/keycloak tests docs flake8 src/keycloak tests docs codespell src tests docs +allowlist_externals = black, poetry [testenv:apply-check] commands = black -C src/keycloak tests docs black src/keycloak tests docs isort src/keycloak tests docs +allowlist_externals = black, poetry, isort [testenv:docs] commands_pre = poetry install --no-root --sync -E docs commands = sphinx-build -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html +allowlist_externals = sphinx-build, poetry [testenv:tests] setenv = file|tox.env