From dfe7517807096c9573de4eb20271dab1f4b9e97b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 10 Dec 2024 13:49:47 +0100 Subject: [PATCH] fix(moved-query-params-out-of-url-patterns): query params in url patterns break with httpx later versions, the query parameters are therefore explicitly set on each request BREAKING CHANGE: removed a couple of unused url patterns --- poetry.lock | 10 ++-- pyproject.toml | 8 ++-- src/keycloak/keycloak_admin.py | 86 +++++++++++++++++++--------------- src/keycloak/urls_patterns.py | 23 ++++----- tests/test_keycloak_admin.py | 2 +- 5 files changed, 66 insertions(+), 63 deletions(-) diff --git a/poetry.lock b/poetry.lock index d77010c..4099c2f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -49,13 +49,13 @@ test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] [[package]] name = "astroid" -version = "3.3.5" +version = "3.3.6" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" files = [ - {file = "astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8"}, - {file = "astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d"}, + {file = "astroid-3.3.6-py3-none-any.whl", hash = "sha256:db676dc4f3ae6bfe31cda227dc60e03438378d7a896aec57422c95634e8d722f"}, + {file = "astroid-3.3.6.tar.gz", hash = "sha256:6aaea045f938c735ead292204afdb977a36e989522b7833ef6fea94de743f442"}, ] [package.dependencies] @@ -2153,4 +2153,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "91b8284a9bc0213fd77fc49cf049ee2f02062cd13d1fa0d52ea711ae400dea5f" +content-hash = "b4e07d4e56d1bc47fe5d3ac0312deaebbef6a5a358688c32a7b3f8466afd5d18" diff --git a/pyproject.toml b/pyproject.toml index a563441..5dc5bb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,12 +4,12 @@ version = "0.0.0" description = "python-keycloak is a Python package providing access to the Keycloak API." license = "MIT" readme = "README.md" -keywords = [ "keycloak", "openid", "oidc" ] +keywords = ["keycloak", "openid", "oidc"] authors = [ "Marcos Pereira ", - "Richard Nemeth " + "Richard Nemeth ", ] -classifiers=[ +classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Development Status :: 3 - Alpha", @@ -35,7 +35,7 @@ requests = ">=2.20.0" requests-toolbelt = ">=0.6.0" deprecation = ">=2.1.0" jwcrypto = ">=1.5.4" -httpx = ">=0.28.0" +httpx = ">=0.23.2" async-property = ">=0.2.2" [tool.poetry.group.docs.dependencies] diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index b37e8f5..eeeaea3 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -298,7 +298,10 @@ class KeycloakAdmin: "export-groups-and-roles": export_groups_and_role, } data_raw = self.connection.raw_post( - urls_patterns.URL_ADMIN_REALM_EXPORT.format(**params_path), data="" + urls_patterns.URL_ADMIN_REALM_EXPORT.format(**params_path), + data="", + exportClients=export_clients, + exportGroupsAndRoles=export_groups_and_role, ) return raise_error_from_response(data_raw, KeycloakPostError) @@ -1334,9 +1337,9 @@ class KeycloakAdmin: :return: client_id (uuid as string) :rtype: str """ - params_path = {"realm-name": self.connection.realm_name, "client-id": client_id} + params_path = {"realm-name": self.connection.realm_name} data_raw = self.connection.raw_get( - urls_patterns.URL_ADMIN_CLIENTS_CLIENT_ID.format(**params_path) + urls_patterns.URL_ADMIN_CLIENTS.format(**params_path), clientId=client_id ) data_response = raise_error_from_response(data_raw, KeycloakGetError) @@ -1381,6 +1384,7 @@ class KeycloakAdmin: data_raw = self.connection.raw_post( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), data=json.dumps(payload), + max=-1, ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -1453,7 +1457,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.connection.realm_name, "id": client_id} data_raw = self.connection.raw_get( - urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path) + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), max=-1 ) return raise_error_from_response(data_raw, KeycloakGetError) @@ -1513,6 +1517,7 @@ class KeycloakAdmin: data_raw = self.connection.raw_post( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), data=json.dumps(payload), + max=-1, ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -1550,6 +1555,8 @@ class KeycloakAdmin: data_raw = self.connection.raw_post( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path), data=json.dumps(payload), + max=-1, + permission=False, ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -1589,6 +1596,7 @@ class KeycloakAdmin: data_raw = self.connection.raw_post( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), data=json.dumps(payload), + max=-1, ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -1605,7 +1613,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.connection.realm_name, "id": client_id} data_raw = self.connection.raw_get( - urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path) + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path), max=-1 ) return raise_error_from_response(data_raw, KeycloakGetError) @@ -1625,6 +1633,7 @@ class KeycloakAdmin: data_raw = self.connection.raw_post( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path), data=json.dumps(payload), + max=-1, ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -1639,7 +1648,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.connection.realm_name, "id": client_id} data_raw = self.connection.raw_get( - urls_patterns.URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path) + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path), max=-1 ) return raise_error_from_response(data_raw, KeycloakGetError) @@ -1654,7 +1663,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.connection.realm_name, "id": client_id} data_raw = self.connection.raw_get( - urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path) + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path), + max=-1, + permission=False, ) return raise_error_from_response(data_raw, KeycloakGetError) @@ -1971,19 +1982,15 @@ class KeycloakAdmin: :return: Keycloak server response (RoleRepresentation) :rtype: list """ - url = urls_patterns.URL_ADMIN_REALM_ROLES params_path = {"realm-name": self.connection.realm_name} params = {"briefRepresentation": brief_representation} - data_raw = self.connection.raw_get( - urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path), **params - ) - # set the search_text path param, if it is a valid string if search_text is not None and search_text.strip() != "": - params_path["search-text"] = search_text - url = urls_patterns.URL_ADMIN_REALM_ROLES_SEARCH + params["search"] = search_text - data_raw = self.connection.raw_get(url.format(**params_path), **params) + data_raw = self.connection.raw_get( + urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path), **params + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_realm_role_groups(self, role_name, query=None, brief_representation=True): @@ -3669,10 +3676,7 @@ class KeycloakAdmin: :return: Keycloak server response (array RoleRepresentation) :rtype: dict """ - params_path = { - "realm-name": self.connection.realm_name, - "scope-id": client_scope_id, - } + params_path = {"realm-name": self.connection.realm_name, "scope-id": client_scope_id} data_raw = self.connection.raw_get( urls_patterns.URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS.format(**params_path) ) @@ -4122,6 +4126,7 @@ class KeycloakAdmin: data_raw = self.connection.raw_post( urls_patterns.URL_ADMIN_ADD_CLIENT_AUTHZ_SCOPE_PERMISSION.format(**params_path), data=json.dumps(payload), + max=-1, ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -4584,7 +4589,10 @@ class KeycloakAdmin: "export-groups-and-roles": export_groups_and_role, } data_raw = await self.connection.a_raw_post( - urls_patterns.URL_ADMIN_REALM_EXPORT.format(**params_path), data="" + urls_patterns.URL_ADMIN_REALM_EXPORT.format(**params_path), + data="", + exportClients=export_clients, + exportGroupsAndRoles=export_groups_and_role, ) return raise_error_from_response(data_raw, KeycloakPostError) @@ -5646,9 +5654,9 @@ class KeycloakAdmin: :return: client_id (uuid as string) :rtype: str """ - params_path = {"realm-name": self.connection.realm_name, "client-id": client_id} + params_path = {"realm-name": self.connection.realm_name} data_raw = await self.connection.a_raw_get( - urls_patterns.URL_ADMIN_CLIENTS_CLIENT_ID.format(**params_path) + urls_patterns.URL_ADMIN_CLIENTS.format(**params_path), clientId=client_id ) data_response = raise_error_from_response(data_raw, KeycloakGetError) @@ -5693,6 +5701,7 @@ class KeycloakAdmin: data_raw = await self.connection.a_raw_post( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), data=json.dumps(payload), + max=-1, ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -5765,7 +5774,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.connection.realm_name, "id": client_id} data_raw = await self.connection.a_raw_get( - urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path) + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), max=-1 ) return raise_error_from_response(data_raw, KeycloakGetError) @@ -5825,6 +5834,7 @@ class KeycloakAdmin: data_raw = await self.connection.a_raw_post( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), data=json.dumps(payload), + max=-1, ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -5862,6 +5872,8 @@ class KeycloakAdmin: data_raw = await self.connection.a_raw_post( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path), data=json.dumps(payload), + max=-1, + permission=False, ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -5903,6 +5915,7 @@ class KeycloakAdmin: data_raw = await self.connection.a_raw_post( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), data=json.dumps(payload), + max=-1, ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -5919,7 +5932,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.connection.realm_name, "id": client_id} data_raw = await self.connection.a_raw_get( - urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path) + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path), max=-1 ) return raise_error_from_response(data_raw, KeycloakGetError) @@ -5939,6 +5952,7 @@ class KeycloakAdmin: data_raw = await self.connection.a_raw_post( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path), data=json.dumps(payload), + max=-1, ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -5953,7 +5967,7 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.connection.realm_name, "id": client_id} data_raw = await self.connection.a_raw_get( - urls_patterns.URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path) + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path), max=-1 ) return raise_error_from_response(data_raw, KeycloakGetError) @@ -5968,7 +5982,9 @@ class KeycloakAdmin: """ params_path = {"realm-name": self.connection.realm_name, "id": client_id} data_raw = await self.connection.a_raw_get( - urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path) + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path), + max=-1, + permission=False, ) return raise_error_from_response(data_raw, KeycloakGetError) @@ -6287,19 +6303,15 @@ class KeycloakAdmin: :return: Keycloak server response (RoleRepresentation) :rtype: list """ - url = urls_patterns.URL_ADMIN_REALM_ROLES params_path = {"realm-name": self.connection.realm_name} params = {"briefRepresentation": brief_representation} - data_raw = await self.connection.a_raw_get( - urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path), **params - ) - # set the search_text path param, if it is a valid string if search_text is not None and search_text.strip() != "": - params_path["search-text"] = search_text - url = urls_patterns.URL_ADMIN_REALM_ROLES_SEARCH + params["search"] = search_text - data_raw = await self.connection.a_raw_get(url.format(**params_path), **params) + data_raw = await self.connection.a_raw_get( + urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path), **params + ) return raise_error_from_response(data_raw, KeycloakGetError) async def a_get_realm_role_groups(self, role_name, query=None, brief_representation=True): @@ -7998,10 +8010,7 @@ class KeycloakAdmin: :return: Keycloak server response (array RoleRepresentation) :rtype: dict """ - params_path = { - "realm-name": self.connection.realm_name, - "scope-id": client_scope_id, - } + params_path = {"realm-name": self.connection.realm_name, "scope-id": client_scope_id} data_raw = await self.connection.a_raw_get( urls_patterns.URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS.format(**params_path) ) @@ -8453,6 +8462,7 @@ class KeycloakAdmin: data_raw = await self.connection.a_raw_post( urls_patterns.URL_ADMIN_ADD_CLIENT_AUTHZ_SCOPE_PERMISSION.format(**params_path), data=json.dumps(payload), + max=-1, ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index bf12090..07aaccd 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -92,7 +92,6 @@ URL_ADMIN_GROUP_MEMBERS = "admin/realms/{realm-name}/groups/{id}/members" URL_ADMIN_CLIENT_INITIAL_ACCESS = "admin/realms/{realm-name}/clients-initial-access" URL_ADMIN_CLIENTS = "admin/realms/{realm-name}/clients" URL_ADMIN_CLIENT = URL_ADMIN_CLIENTS + "/{id}" -URL_ADMIN_CLIENTS_CLIENT_ID = URL_ADMIN_CLIENTS + "?clientId={client-id}" URL_ADMIN_CLIENT_ALL_SESSIONS = URL_ADMIN_CLIENT + "/user-sessions" URL_ADMIN_CLIENT_SECRETS = URL_ADMIN_CLIENT + "/client-secret" URL_ADMIN_CLIENT_ROLES = URL_ADMIN_CLIENT + "/roles" @@ -117,14 +116,12 @@ URL_ADMIN_CLIENT_DEFAULT_CLIENT_SCOPE = ( URL_ADMIN_CLIENT_AUTHZ = URL_ADMIN_CLIENT + "/authz/resource-server" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT_AUTHZ + "/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCE = URL_ADMIN_CLIENT_AUTHZ + "/resource/{resource-id}" -URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT_AUTHZ + "/resource?max=-1" -URL_ADMIN_CLIENT_AUTHZ_SCOPES = URL_ADMIN_CLIENT_AUTHZ + "/scope?max=-1" -URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS = URL_ADMIN_CLIENT_AUTHZ + "/permission?max=-1" -URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT_AUTHZ + "/policy?max=-1&permission=false" -URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = URL_ADMIN_CLIENT_AUTHZ + "/policy/role?max=-1" -URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION = ( - URL_ADMIN_CLIENT_AUTHZ + "/permission/resource?max=-1" -) +URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT_AUTHZ + "/resource" +URL_ADMIN_CLIENT_AUTHZ_SCOPES = URL_ADMIN_CLIENT_AUTHZ + "/scope" +URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS = URL_ADMIN_CLIENT_AUTHZ + "/permission" +URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT_AUTHZ + "/policy" +URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = URL_ADMIN_CLIENT_AUTHZ + "/policy/role" +URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION = URL_ADMIN_CLIENT_AUTHZ + "/permission/resource" URL_ADMIN_CLIENT_AUTHZ_POLICY = URL_ADMIN_CLIENT_AUTHZ + "/policy/{policy-id}" URL_ADMIN_CLIENT_AUTHZ_POLICY_SCOPES = URL_ADMIN_CLIENT_AUTHZ_POLICY + "/scopes" URL_ADMIN_CLIENT_AUTHZ_POLICY_RESOURCES = URL_ADMIN_CLIENT_AUTHZ_POLICY + "/resources" @@ -132,7 +129,7 @@ URL_ADMIN_CLIENT_AUTHZ_SCOPE_PERMISSION = URL_ADMIN_CLIENT_AUTHZ + "/permission/ URL_ADMIN_CLIENT_AUTHZ_RESOURCE_PERMISSION = ( URL_ADMIN_CLIENT_AUTHZ + "/permission/resource/{resource-id}" ) -URL_ADMIN_ADD_CLIENT_AUTHZ_SCOPE_PERMISSION = URL_ADMIN_CLIENT_AUTHZ + "/permission/scope?max=-1" +URL_ADMIN_ADD_CLIENT_AUTHZ_SCOPE_PERMISSION = URL_ADMIN_CLIENT_AUTHZ + "/permission/scope" URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY = URL_ADMIN_CLIENT_AUTHZ + "/policy/client" URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY_ASSOCIATED_POLICIES = ( URL_ADMIN_CLIENT_AUTHZ + "/policy/{policy-id}/associatedPolicies" @@ -155,7 +152,6 @@ URL_ADMIN_CLIENT_SCOPE_ROLE_MAPPINGS_CLIENT = ( ) URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" -URL_ADMIN_REALM_ROLES_SEARCH = URL_ADMIN_REALM_ROLES + "?search={search-text}" URL_ADMIN_REALM_ROLES_MEMBERS = URL_ADMIN_REALM_ROLES + "/{role-name}/users" URL_ADMIN_REALM_ROLES_GROUPS = URL_ADMIN_REALM_ROLES + "/{role-name}/groups" URL_ADMIN_REALMS = "admin/realms" @@ -169,10 +165,7 @@ URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = ( "admin/realms/{realm-name}/roles/{role-name}/composites" ) -URL_ADMIN_REALM_EXPORT = ( - "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&" - + "exportGroupsAndRoles={export-groups-and-roles}" -) +URL_ADMIN_REALM_EXPORT = "admin/realms/{realm-name}/partial-export" URL_ADMIN_REALM_PARTIAL_IMPORT = "admin/realms/{realm-name}/partialImport" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index e33614b..91446f7 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -3353,7 +3353,7 @@ async def test_a_partial_import_realm(admin: KeycloakAdmin, realm: str): client_id = await admin.a_create_client(payload={"name": test_client, "clientId": test_client}) realm_export = await admin.a_export_realm(export_clients=True, export_groups_and_role=False) - + assert realm_export["realm"] == realm, realm client_config = [ client_entry for client_entry in realm_export["clients"] if client_entry["id"] == client_id ][0]