From 5905cc5071052f5c105234509234be1f1d83e579 Mon Sep 17 00:00:00 2001 From: Eunseok Kang <6640728+ethan-k@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:48:57 +0900 Subject: [PATCH] feat: add get_role_composites_by_id method --- src/keycloak/keycloak_admin.py | 40 ++++++++++++++++ tests/test_keycloak_admin.py | 85 ++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index fab1a71..31118e7 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -3475,6 +3475,26 @@ class KeycloakAdmin: expected_codes=[HTTP_NO_CONTENT], ) + def get_role_composites_by_id(self, role_id: str, query: dict | None = None) -> list: + """ + Get all composite roles by role id. + + :param role_id: id of role + :type role_id: str + :param query: Query parameters (optional). Supported keys: 'first', 'max', 'search' + :type query: dict + :return: Keycloak server response (RoleRepresentation) + :rtype: list + """ + query = query or {} + params_path = {"realm-name": self.connection.realm_name, "role-id": role_id} + url = urls_patterns.URL_ADMIN_REALM_ROLE_COMPOSITES.format(**params_path) + + if "first" in query or "max" in query: + return self.__fetch_paginated(url, query) + + return self.__fetch_all(url, query) + def create_realm_role(self, payload: dict, skip_exists: bool = False) -> str: """ Create a new role for the realm or client. @@ -8773,6 +8793,26 @@ class KeycloakAdmin: expected_codes=[HTTP_NO_CONTENT], ) + async def a_get_role_composites_by_id(self, role_id: str, query: dict | None = None) -> list: + """ + Get all composite roles by role id asynchronously. + + :param role_id: id of role + :type role_id: str + :param query: Query parameters (optional). Supported keys: 'first', 'max', 'search' + :type query: dict + :return: Keycloak server response (RoleRepresentation) + :rtype: list + """ + query = query or {} + params_path = {"realm-name": self.connection.realm_name, "role-id": role_id} + url = urls_patterns.URL_ADMIN_REALM_ROLE_COMPOSITES.format(**params_path) + + if "first" in query or "max" in query: + return await self.a___fetch_paginated(url, query) + + return await self.a___fetch_all(url, query) + async def a_create_realm_role(self, payload: dict, skip_exists: bool = False) -> str: """ Create a new role for the realm or client asynchronously. diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index a90b6d5..be58f2f 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -3285,6 +3285,48 @@ def test_get_role_client_level_children( assert child["id"] in [x["id"] for x in res] +def test_get_role_composites_by_id( + admin: KeycloakAdmin, + realm: str, + client: str, + composite_client_role: str, + client_role: str, +) -> None: + """ + Test get role's children by role ID. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :param client: Keycloak client + :type client: str + :param composite_client_role: Composite client role + :type composite_client_role: str + :param client_role: Client role + :type client_role: str + """ + admin.change_current_realm(realm) + + parent_role = admin.get_client_role(client, composite_client_role) + child_role = admin.get_client_role(client, client_role) + + composites = admin.get_role_composites_by_id(parent_role["id"]) + assert len(composites) > 0 + assert child_role["id"] in [x["id"] for x in composites] + + composites_paginated = admin.get_role_composites_by_id( + parent_role["id"], query={"first": 0, "max": 10} + ) + assert len(composites_paginated) > 0 + assert child_role["id"] in [x["id"] for x in composites_paginated] + + composites_searched = admin.get_role_composites_by_id( + parent_role["id"], query={"search": client_role[:3]} + ) + assert len(composites_searched) > 0 + + def test_upload_certificate( admin: KeycloakAdmin, realm: str, @@ -7017,6 +7059,49 @@ async def test_a_get_role_client_level_children( assert child["id"] in [x["id"] for x in res] +@pytest.mark.asyncio +async def test_a_get_role_composites_by_id( + admin: KeycloakAdmin, + realm: str, + client: str, + composite_client_role: str, + client_role: str, +) -> None: + """ + Test get all composite roles by role id asynchronously. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :param client: Keycloak client + :type client: str + :param composite_client_role: Composite client role + :type composite_client_role: str + :param client_role: Client role + :type client_role: str + """ + await admin.a_change_current_realm(realm) + + parent_role = await admin.a_get_client_role(client, composite_client_role) + child_role = await admin.a_get_client_role(client, client_role) + + composites = await admin.a_get_role_composites_by_id(parent_role["id"]) + assert len(composites) > 0 + assert child_role["id"] in [x["id"] for x in composites] + + composites_paginated = await admin.a_get_role_composites_by_id( + parent_role["id"], query={"first": 0, "max": 10} + ) + assert len(composites_paginated) > 0 + assert child_role["id"] in [x["id"] for x in composites_paginated] + + composites_searched = await admin.a_get_role_composites_by_id( + parent_role["id"], query={"search": client_role[:3]} + ) + assert len(composites_searched) > 0 + + @pytest.mark.asyncio async def test_a_upload_certificate( admin: KeycloakAdmin,