From a1591ba50ae3c0cedbec9c1f62cda993ddd1872e Mon Sep 17 00:00:00 2001 From: Romuald OUATTARA Date: Mon, 8 Aug 2022 19:28:53 +0200 Subject: [PATCH 01/10] docs: added change of realm example --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d300fa9..99eba7d 100644 --- a/README.md +++ b/README.md @@ -319,4 +319,10 @@ idps = keycloak_admin.get_idps() # Create a new Realm keycloak_admin.create_realm(payload={"realm": "demo"}, skip_exists=False) +# Changing Realm +keycloak_admin = KeycloakAdmin(realm_name="main", ...) +keycloak_admin.get_users() # Get user in main realm +keycloak_admin.realm_name = "demo" # Change realm to 'demo' +keycloak_admin.get_users() # Get users in realm 'demo' +keycloak_admin.create_user(...) # Creates a new user in 'demo' ``` From 0fb6c2058d96d2c1f194665243eef4e7f10c0ae8 Mon Sep 17 00:00:00 2001 From: Antonio Lucas Neres Date: Thu, 11 Aug 2022 10:35:53 -0300 Subject: [PATCH 02/10] feat: add client scope-mappings realm roles operations --- README.md | 56 ++++++++++++++++++-------------- src/keycloak/keycloak_admin.py | 42 ++++++++++++++++++++++++ src/keycloak/urls_patterns.py | 1 + tests/test_keycloak_admin.py | 58 ++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 99eba7d..9666ba9 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,9 @@ from keycloak import KeycloakOpenID # Configure client keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", - client_id="example_client", - realm_name="example_realm", - client_secret_key="secret") + client_id="example_client", + realm_name="example_realm", + client_secret_key="secret") # Get WellKnow config_well_known = keycloak_openid.well_known() @@ -110,7 +110,7 @@ rpt = keycloak_openid.entitlement(token['access_token'], "resource_id") # Instropect RPT token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['access_token'], rpt=rpt['rpt'], - token_type_hint="requesting_party_token")) + token_type_hint="requesting_party_token")) # Introspect Token token_info = keycloak_openid.introspect(token['access_token']) @@ -153,37 +153,37 @@ keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", # Add user new_user = keycloak_admin.create_user({"email": "example@example.com", - "username": "example@example.com", - "enabled": True, - "firstName": "Example", - "lastName": "Example"}) + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example"}) # Add user and raise exception if username already exists # exist_ok currently defaults to True for backwards compatibility reasons new_user = keycloak_admin.create_user({"email": "example@example.com", - "username": "example@example.com", - "enabled": True, - "firstName": "Example", - "lastName": "Example"}, - exist_ok=False) + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example"}, + exist_ok=False) # Add user and set password new_user = keycloak_admin.create_user({"email": "example@example.com", - "username": "example@example.com", - "enabled": True, - "firstName": "Example", - "lastName": "Example", + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example", "credentials": [{"value": "secret","type": "password",}]}) # Add user and specify a locale new_user = keycloak_admin.create_user({"email": "example@example.fr", - "username": "example@example.fr", - "enabled": True, - "firstName": "Example", - "lastName": "Example", - "attributes": { - "locale": ["fr"] - }}) + "username": "example@example.fr", + "enabled": True, + "firstName": "Example", + "lastName": "Example", + "attributes": { + "locale": ["fr"] + }}) # User counter count_users = keycloak_admin.users_count() @@ -312,6 +312,14 @@ keycloak_admin.assign_client_role(client_id=client_id, user_id=user_id, role_id= # Assign realm roles to user keycloak_admin.assign_realm_roles(user_id=user_id, roles=realm_roles) +# Assign realm roles to client's scope +keycloak_admin.assign_realm_roles_to_client_scope(client_id=client_id, roles=realm_roles) + +# Get realm roles assigned to client's scope +keycloak_admin.get_realm_roles_of_client_scope(client_id=client_id) + +# Remove realm roles assigned to client's scope +keycloak_admin.delete_realm_roles_of_client_scope(client_id=client_id, roles=realm_roles) # Get all ID Providers idps = keycloak_admin.get_idps() diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 7d16ad5..50299b6 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1597,6 +1597,48 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakGetError) + def assign_realm_roles_to_client_scope(self, client_id, roles): + """Assign realm roles to a client's scope. + + :param client_id: id of client (not client-id) + :param roles: roles list or role (use RoleRepresentation) + :return: Keycloak server response + """ + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_REALM_ROLES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) + + def delete_realm_roles_of_client_scope(self, client_id, roles): + """Delete realm roles of a client's scope. + + :param client_id: id of client (not client-id) + :param roles: roles list or role (use RoleRepresentation) + :return: Keycloak server response + """ + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_REALM_ROLES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) + + def get_realm_roles_of_client_scope(self, client_id): + """Get all realm roles for a client's scope. + + :param client_id: id of client (not client-id) + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_REALM_ROLES.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + def assign_realm_roles(self, user_id, roles): """Assign realm roles to a user. diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 3f4151e..f7d4f65 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -91,6 +91,7 @@ URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE = URL_ADMIN_CLIENT_ROLE + "/composi URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_ROLE_GROUPS = URL_ADMIN_CLIENT + "/roles/{role-name}/groups" URL_ADMIN_CLIENT_MANAGEMENT_PERMISSIONS = URL_ADMIN_CLIENT + "/management/permissions" +URL_ADMIN_CLIENT_SCOPE_MAPPINGS_REALM_ROLES = URL_ADMIN_CLIENT + "/scope-mappings/realm" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource?max=-1" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 61685af..e9b093c 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1030,6 +1030,64 @@ def test_realm_roles(admin: KeycloakAdmin, realm: str): assert err.match('404: b\'{"error":"Could not find role"}\'') +def test_client_scope_realm_roles(admin: KeycloakAdmin, realm: str): + """Test client realm roles.""" + admin.realm_name = realm + + # Test get realm roles + roles = admin.get_realm_roles() + assert len(roles) == 3, roles + role_names = [x["name"] for x in roles] + assert "uma_authorization" in role_names, role_names + assert "offline_access" in role_names, role_names + + # create realm role for test + role_id = admin.create_realm_role(payload={"name": "test-realm-role"}, skip_exists=True) + assert role_id, role_id + + # Test realm role client assignment + client_id = admin.create_client( + payload={"name": "role-testing-client", "clientId": "role-testing-client"} + ) + with pytest.raises(KeycloakPostError) as err: + admin.assign_realm_roles_to_client_scope(client_id=client_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_realm_roles_to_client_scope( + client_id=client_id, + roles=[ + admin.get_realm_role(role_name="offline_access"), + admin.get_realm_role(role_name="test-realm-role"), + ], + ) + assert res == dict(), res + + roles = admin.get_realm_roles_of_client_scope(client_id=client_id) + assert len(roles) == 2 + client_role_names = [x["name"] for x in roles] + assert "offline_access" in client_role_names, client_role_names + assert "test-realm-role" in client_role_names, client_role_names + assert "uma_authorization" not in client_role_names, client_role_names + + # Test remove realm role of client + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_realm_roles_of_client_scope(client_id=client_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.delete_realm_roles_of_client_scope( + client_id=client_id, roles=[admin.get_realm_role(role_name="offline_access")] + ) + assert res == dict(), res + roles = admin.get_realm_roles_of_client_scope(client_id=client_id) + assert len(roles) == 1 + assert "test-realm-role" in [x["name"] for x in roles] + + res = admin.delete_realm_roles_of_client_scope( + client_id=client_id, roles=[admin.get_realm_role(role_name="test-realm-role")] + ) + assert res == dict(), res + roles = admin.get_realm_roles_of_client_scope(client_id=client_id) + assert len(roles) == 0 + + def test_client_roles(admin: KeycloakAdmin, client: str): """Test client roles.""" # Test get client roles From 2e2578b1b425b5da34e84a6d6f77cbce6e7fd7e8 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Fri, 12 Aug 2022 06:55:00 +0000 Subject: [PATCH 03/10] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 251c6d5..958d8fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v2.2.0 (2022-08-12) + +### Feat + +- add client scope-mappings realm roles operations + ## v2.1.1 (2022-07-19) ### Refactor From d14fbd6b5dff94808b5f92db9539a16a53db6dce Mon Sep 17 00:00:00 2001 From: Subramaniam Ramasubramanian Date: Fri, 12 Aug 2022 10:46:15 +0000 Subject: [PATCH 04/10] feat: Add token_type/scope to token exchange api --- src/keycloak/keycloak_openid.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 82e980c..055085d 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -275,7 +275,15 @@ class KeycloakOpenID: data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakPostError) - def exchange_token(self, token: str, client_id: str, audience: str, subject: str) -> dict: + def exchange_token( + self, + token: str, + client_id: str, + audience: str, + subject: str, + requested_token_type: str = "urn:ietf:params:oauth:token-type:refresh_token", + scope: str = "", + ) -> dict: """Exchange user token. Use a token to obtain an entirely different token. See @@ -285,6 +293,8 @@ class KeycloakOpenID: :param client_id: :param audience: :param subject: + :param requested_token_type: + :param scope: :return: """ params_path = {"realm-name": self.realm_name} @@ -292,9 +302,10 @@ class KeycloakOpenID: "grant_type": ["urn:ietf:params:oauth:grant-type:token-exchange"], "client_id": client_id, "subject_token": token, - "requested_token_type": "urn:ietf:params:oauth:token-type:refresh_token", + "requested_token_type": requested_token_type, "audience": audience, "requested_subject": subject, + "scope": scope, } payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) From 0b93336f1582e17b105fbcf69899443268f1e11b Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Sat, 13 Aug 2022 00:39:02 +0000 Subject: [PATCH 05/10] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 958d8fa..cf8293b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v2.3.0 (2022-08-13) + +### Feat + +- Add token_type/scope to token exchange api + ## v2.2.0 (2022-08-12) ### Feat From 7c486ccb4fce28ff7e9fb5a5d4b6b95e94343926 Mon Sep 17 00:00:00 2001 From: Antonio Lucas Neres Date: Fri, 19 Aug 2022 14:13:27 -0300 Subject: [PATCH 06/10] feat: add client scope-mappings client roles operations --- README.md | 11 +++++++ src/keycloak/keycloak_admin.py | 57 ++++++++++++++++++++++++++++++++++ src/keycloak/urls_patterns.py | 3 ++ tests/test_keycloak_admin.py | 56 +++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+) diff --git a/README.md b/README.md index 9666ba9..3558d30 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,17 @@ keycloak_admin.get_realm_roles_of_client_scope(client_id=client_id) # Remove realm roles assigned to client's scope keycloak_admin.delete_realm_roles_of_client_scope(client_id=client_id, roles=realm_roles) +another_client_id = keycloak_admin.get_client_id("my-client-2") + +# Assign client roles to client's scope +keycloak_admin.assign_client_roles_to_client_scope(client_id=another_client_id, client_roles_owner_id=client_id, roles=client_roles) + +# Get client roles assigned to client's scope +keycloak_admin.get_client_roles_of_client_scope(client_id=another_client_id, client_roles_owner_id=client_id) + +# Remove client roles assigned to client's scope +keycloak_admin.delete_client_roles_of_client_scope(client_id=another_client_id, client_roles_owner_id=client_id, roles=client_roles) + # Get all ID Providers idps = keycloak_admin.get_idps() diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 50299b6..15423a1 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1639,6 +1639,63 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakGetError) + def assign_client_roles_to_client_scope(self, client_id, client_roles_owner_id, roles): + """Assign client roles to a client's scope. + + :param client_id: id of client (not client-id) who is assigned the roles + :param client_roles_owner_id: id of client (not client-id) who has the roles + :param roles: roles list or role (use RoleRepresentation) + :return: Keycloak server response + """ + payload = roles if isinstance(roles, list) else [roles] + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "client": client_roles_owner_id, + } + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) + + def delete_client_roles_of_client_scope(self, client_id, client_roles_owner_id, roles): + """Delete client roles of a client's scope. + + :param client_id: id of client (not client-id) who is assigned the roles + :param client_roles_owner_id: id of client (not client-id) who has the roles + :param roles: roles list or role (use RoleRepresentation) + :return: Keycloak server response + """ + payload = roles if isinstance(roles, list) else [roles] + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "client": client_roles_owner_id, + } + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) + + def get_client_roles_of_client_scope(self, client_id, client_roles_owner_id): + """Get all client roles for a client's scope. + + :param client_id: id of client (not client-id) + :param client_roles_owner_id: id of client (not client-id) who has the roles + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "client": client_roles_owner_id, + } + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_CLIENT_ROLES.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + def assign_realm_roles(self, user_id, roles): """Assign realm roles to a user. diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index f7d4f65..f2a2188 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -92,6 +92,9 @@ URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_ROLE_GROUPS = URL_ADMIN_CLIENT + "/roles/{role-name}/groups" URL_ADMIN_CLIENT_MANAGEMENT_PERMISSIONS = URL_ADMIN_CLIENT + "/management/permissions" URL_ADMIN_CLIENT_SCOPE_MAPPINGS_REALM_ROLES = URL_ADMIN_CLIENT + "/scope-mappings/realm" +URL_ADMIN_CLIENT_SCOPE_MAPPINGS_CLIENT_ROLES = ( + URL_ADMIN_CLIENT + "/scope-mappings/clients/{client}" +) URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource?max=-1" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index e9b093c..0f5af95 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1088,6 +1088,62 @@ def test_client_scope_realm_roles(admin: KeycloakAdmin, realm: str): assert len(roles) == 0 +def test_client_scope_client_roles(admin: KeycloakAdmin, realm: str, client: str): + """Test client assignment of other client roles.""" + admin.realm_name = realm + + client_id = admin.create_client( + payload={"name": "role-testing-client", "clientId": "role-testing-client"} + ) + + # Test get client roles + roles = admin.get_client_roles_of_client_scope(client_id, client) + assert len(roles) == 0, roles + + # create client role for test + client_role_id = admin.create_client_role( + client_role_id=client, payload={"name": "client-role-test"}, skip_exists=True + ) + assert client_role_id, client_role_id + + # Test client role assignment to other client + with pytest.raises(KeycloakPostError) as err: + admin.assign_client_roles_to_client_scope( + client_id=client_id, client_roles_owner_id=client, roles=["bad"] + ) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_client_roles_to_client_scope( + client_id=client_id, + client_roles_owner_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test")], + ) + assert res == dict(), res + + roles = admin.get_client_roles_of_client_scope( + client_id=client_id, client_roles_owner_id=client + ) + assert len(roles) == 1 + client_role_names = [x["name"] for x in roles] + assert "client-role-test" in client_role_names, client_role_names + + # Test remove realm role of client + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_client_roles_of_client_scope( + client_id=client_id, client_roles_owner_id=client, roles=["bad"] + ) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.delete_client_roles_of_client_scope( + client_id=client_id, + client_roles_owner_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test")], + ) + assert res == dict(), res + roles = admin.get_client_roles_of_client_scope( + client_id=client_id, client_roles_owner_id=client + ) + assert len(roles) == 0 + + def test_client_roles(admin: KeycloakAdmin, client: str): """Test client roles.""" # Test get client roles From acd457ef39ca04c8a1c7fb4e0dc44642945cc046 Mon Sep 17 00:00:00 2001 From: Merle Nerger Date: Fri, 19 Aug 2022 14:47:57 +0200 Subject: [PATCH 07/10] docs: fixed docstrings stating incorrect return types for get_client_role(s) and get_realm_role(s) --- src/keycloak/keycloak_admin.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 50299b6..9efa445 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1306,7 +1306,7 @@ class KeycloakAdmin: RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - :return: Keycloak server response (RoleRepresentation) + :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path)) @@ -1329,12 +1329,11 @@ class KeycloakAdmin: def get_client_roles(self, client_id): """Get all roles for the client. - :param client_id: id of client (not client-id) - RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - :return: Keycloak server response (RoleRepresentation) + :param client_id: id of client (not client-id) + :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path)) @@ -1345,13 +1344,12 @@ class KeycloakAdmin: This is required for further actions with this role. - :param client_id: id of client (not client-id) - :param role_name: role’s name (not id!) - RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - :return: role_id + :param client_id: id of client (not client-id) + :param role_name: role’s name (not id!) + :return: Keycloak server response (RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLE.format(**params_path)) @@ -1517,11 +1515,11 @@ class KeycloakAdmin: def get_realm_role(self, role_name): """Get realm role by role name. - :param role_name: role's name, not id! - RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - :return: role_id + + :param role_name: role's name, not id! + :return: Keycloak server response (RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_get( From 739e9abfbebaeff42df981b0ebfadd31fe538f61 Mon Sep 17 00:00:00 2001 From: Merle Nerger Date: Thu, 18 Aug 2022 17:04:17 +0200 Subject: [PATCH 08/10] feat: added missing functionality to include attributes when returning realm roles according to specifications --- src/keycloak/keycloak_admin.py | 57 ++++++++++++++++++++++++---------- tests/test_keycloak_admin.py | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 9efa445..770d3ce 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -578,17 +578,21 @@ class KeycloakAdmin: data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_user_groups(self, user_id): + def get_user_groups(self, user_id, brief_representation=True): """Get user groups. Returns a list of groups of which the user is a member :param user_id: User id + :param brief_representation: whether to omit attributes in the response :return: user groups list """ + params = {"briefRepresentation": brief_representation} params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_GROUPS.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_USER_GROUPS.format(**params_path), **params + ) return raise_error_from_response(data_raw, KeycloakGetError) def update_user(self, user_id, payload): @@ -1300,16 +1304,20 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) - def get_realm_roles(self): + def get_realm_roles(self, brief_representation=True): """Get all roles for the realm or client. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + :param brief_representation: whether to omit role attributes in the response :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path)) + params = {"briefRepresentation": brief_representation} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path), **params + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_realm_role_members(self, role_name, query=None): @@ -1326,17 +1334,21 @@ class KeycloakAdmin: urls_patterns.URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query ) - def get_client_roles(self, client_id): + def get_client_roles(self, client_id, brief_representation=True): """Get all roles for the client. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :param client_id: id of client (not client-id) + :param brief_representation: whether to omit role attributes in the response :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path)) + params = {"briefRepresentation": brief_representation} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path), **params + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role(self, client_id, role_name): @@ -1689,15 +1701,17 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakGetError) - def get_composite_realm_roles_of_user(self, user_id): + def get_composite_realm_roles_of_user(self, user_id, brief_representation=True): """Get all composite (i.e. implicit) realm roles for a user. :param user_id: id of user + :param brief_representation: whether to omit role attributes in the response :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": user_id} + params = {"briefRepresentation": brief_representation} data_raw = self.raw_get( - urls_patterns.URL_ADMIN_USER_REALM_ROLES_COMPOSITE.format(**params_path) + urls_patterns.URL_ADMIN_USER_REALM_ROLES_COMPOSITE.format(**params_path), **params ) return raise_error_from_response(data_raw, KeycloakGetError) @@ -1731,14 +1745,18 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) - def get_group_realm_roles(self, group_id): + def get_group_realm_roles(self, group_id, brief_representation=True): """Get all realm roles for a group. :param user_id: id of the group + :param brief_representation: whether to omit role attributes in the response :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path)) + params = {"briefRepresentation": brief_representation} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), **params + ) return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_client_roles(self, group_id, client_id, roles): @@ -1806,20 +1824,24 @@ class KeycloakAdmin: urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id ) - def get_composite_client_roles_of_user(self, user_id, client_id): + def get_composite_client_roles_of_user(self, user_id, client_id, brief_representation=False): """Get composite client role-mappings for a user. :param user_id: id of user :param client_id: id of client (not client-id) + :param brief_representation: whether to omit attributes in the response :return: Keycloak server response (array RoleRepresentation) """ + params = {"briefRepresentation": brief_representation} return self._get_client_roles_of_user( - urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id, **params ) - def _get_client_roles_of_user(self, client_level_role_mapping_url, user_id, client_id): + def _get_client_roles_of_user( + self, client_level_role_mapping_url, user_id, client_id, **params + ): params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} - data_raw = self.raw_get(client_level_role_mapping_url.format(**params_path)) + data_raw = self.raw_get(client_level_role_mapping_url.format(**params_path), **params) return raise_error_from_response(data_raw, KeycloakGetError) def delete_client_roles_of_user(self, user_id, client_id, roles): @@ -2854,19 +2876,22 @@ class KeycloakAdmin: ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) - def get_composite_client_roles_of_group(self, client_id, group_id): + def get_composite_client_roles_of_group(self, client_id, group_id, brief_representation=True): """Get the composite client roles of the given group for the given client. :param client_id: id of the client. :type client_id: str :param group_id: id of the group. :type group_id: str + :param brief_representation: whether to omit attributes in the response + :type brief_representation: bool :return: the composite client roles of the group (list of RoleRepresentation). :rtype: list """ params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + params = {"briefRepresentation": brief_representation} data_raw = self.raw_get( - urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE.format(**params_path) + urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE.format(**params_path), **params ) return raise_error_from_response(data_raw, KeycloakGetError) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index e9b093c..cf5d8b2 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1030,6 +1030,63 @@ def test_realm_roles(admin: KeycloakAdmin, realm: str): assert err.match('404: b\'{"error":"Could not find role"}\'') +@pytest.mark.parametrize( + "testcase, arg_brief_repr, includes_attributes", + [ + ("brief True", {"brief_representation": True}, False), + ("brief False", {"brief_representation": False}, True), + ("default", {}, False), + ], +) +def test_role_attributes( + admin: KeycloakAdmin, + realm: str, + client: str, + arg_brief_repr: dict, + includes_attributes: bool, + testcase: str, +): + """Test getting role attributes for bulk calls.""" + # setup + attribute_role = "test-realm-role-w-attr" + test_attrs = {"attr1": ["val1"], "attr2": ["val2-1", "val2-2"]} + role_id = admin.create_realm_role( + payload={"name": attribute_role, "attributes": test_attrs}, + skip_exists=True, + ) + assert role_id, role_id + + cli_role_id = admin.create_client_role( + client, + payload={"name": attribute_role, "attributes": test_attrs}, + skip_exists=True, + ) + assert cli_role_id, cli_role_id + + if not includes_attributes: + test_attrs = None + + # tests + roles = admin.get_realm_roles(**arg_brief_repr) + roles_filtered = [role for role in roles if role["name"] == role_id] + assert roles_filtered, roles_filtered + role = roles_filtered[0] + assert role.get("attributes") == test_attrs, testcase + + roles = admin.get_client_roles(client, **arg_brief_repr) + roles_filtered = [role for role in roles if role["name"] == cli_role_id] + assert roles_filtered, roles_filtered + role = roles_filtered[0] + assert role.get("attributes") == test_attrs, testcase + + # cleanup + res = admin.delete_realm_role(role_name=attribute_role) + assert res == dict(), res + + res = admin.delete_client_role(client, role_name=attribute_role) + assert res == dict(), res + + def test_client_scope_realm_roles(admin: KeycloakAdmin, realm: str): """Test client realm roles.""" admin.realm_name = realm From 3c4e0f1443b9215ac360571d4d9c9889ea799328 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Fri, 19 Aug 2022 19:42:03 +0000 Subject: [PATCH 09/10] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8293b..ca19f4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v2.4.0 (2022-08-19) + +### Feat + +- add client scope-mappings client roles operations + ## v2.3.0 (2022-08-13) ### Feat From dfd5381f6e64fcfa8b0a2d09a55a494ff598de30 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Fri, 19 Aug 2022 19:49:58 +0000 Subject: [PATCH 10/10] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca19f4e..0f90b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v2.5.0 (2022-08-19) + +### Feat + +- added missing functionality to include attributes when returning realm roles according to specifications + ## v2.4.0 (2022-08-19) ### Feat