From d637db7623caeb4def20e6e48494df638f9758e2 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:36:02 +0100 Subject: [PATCH] [PR #11472/c41de53d backport][stable-12] keycloak: URL-encode query parameters for usernames with special characters (#11474) keycloak: URL-encode query parameters for usernames with special characters (#11472) * fix(keycloak): URL-encode query params for usernames with special chars get_user_by_username() concatenates the username directly into the URL query string. When the username contains a +, it is interpreted as a space by the server, returning no match and causing a TypeError. Use urllib.parse.quote() (already imported) for the username parameter. Also replace three fragile .replace(' ', '%20') calls in the authz search methods with proper quote() calls. Fixes #10305 * Update changelogs/fragments/keycloak-url-encode-query-params.yml --------- (cherry picked from commit c41de53dbb133ce3d6d9a0fa1fecd7e66603fece) Co-authored-by: Ivan Kokalovic <67540157+koke1997@users.noreply.github.com> Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- .../keycloak-url-encode-query-params.yml | 7 +++ .../identity/keycloak/keycloak.py | 8 +-- .../targets/keycloak_user/tasks/main.yml | 56 +++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/keycloak-url-encode-query-params.yml diff --git a/changelogs/fragments/keycloak-url-encode-query-params.yml b/changelogs/fragments/keycloak-url-encode-query-params.yml new file mode 100644 index 0000000000..572f4b99d9 --- /dev/null +++ b/changelogs/fragments/keycloak-url-encode-query-params.yml @@ -0,0 +1,7 @@ +bugfixes: + - keycloak module utils - fix ``TypeError`` crash when managing users whose username + or email contains special characters such as ``+`` + (https://github.com/ansible-collections/community.general/issues/10305, https://github.com/ansible-collections/community.general/pull/11472). + - keycloak module utils - use proper URL encoding (``urllib.parse.quote``) for query + parameters in authorization permission name searches, replacing fragile + manual space replacement (https://github.com/ansible-collections/community.general/pull/11472). diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 1482f9693b..e27c7b3fb5 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -998,7 +998,7 @@ class KeycloakAPI: :param realm: Realm in which the user resides; default 'master' """ users_url = URL_USERS.format(url=self.baseurl, realm=realm) - users_url += f"?username={username}&exact=true" + users_url += f"?username={quote(username, safe='')}&exact=true" try: userrep = None users = self._request_and_deserialize(users_url, method="GET") @@ -3018,7 +3018,7 @@ class KeycloakAPI: def get_authz_permission_by_name(self, name, client_id, realm): """Get authorization permission by name""" url = URL_AUTHZ_POLICIES.format(url=self.baseurl, client_id=client_id, realm=realm) - search_url = f"{url}/search?name={name.replace(' ', '%20')}" + search_url = f"{url}/search?name={quote(name, safe='')}" try: return self._request_and_deserialize(search_url, method="GET") @@ -3064,7 +3064,7 @@ class KeycloakAPI: def get_authz_resource_by_name(self, name, client_id, realm): """Get authorization resource by name""" url = URL_AUTHZ_RESOURCES.format(url=self.baseurl, client_id=client_id, realm=realm) - search_url = f"{url}/search?name={name.replace(' ', '%20')}" + search_url = f"{url}/search?name={quote(name, safe='')}" try: return self._request_and_deserialize(search_url, method="GET") @@ -3074,7 +3074,7 @@ class KeycloakAPI: def get_authz_policy_by_name(self, name, client_id, realm): """Get authorization policy by name""" url = URL_AUTHZ_POLICIES.format(url=self.baseurl, client_id=client_id, realm=realm) - search_url = f"{url}/search?name={name.replace(' ', '%20')}" + search_url = f"{url}/search?name={quote(name, safe='')}" try: return self._request_and_deserialize(search_url, method="GET") diff --git a/tests/integration/targets/keycloak_user/tasks/main.yml b/tests/integration/targets/keycloak_user/tasks/main.yml index 0f1fe152d0..4f5a545b50 100644 --- a/tests/integration/targets/keycloak_user/tasks/main.yml +++ b/tests/integration/targets/keycloak_user/tasks/main.yml @@ -112,3 +112,59 @@ that: - delete_result.changed - delete_result.end_state | length == 0 + +- name: Create user with plus-addressed email + community.general.keycloak_user: + auth_keycloak_url: "{{ url }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + auth_realm: "{{ admin_realm }}" + username: "testuser+tag" + realm: "{{ realm }}" + first_name: Plus + last_name: User + email: "testuser+tag@example.org" + state: present + register: plus_create_result + +- name: Assert plus-addressed user is created + assert: + that: + - plus_create_result.changed + - plus_create_result.end_state.username == 'testuser+tag' + - plus_create_result.end_state.email == 'testuser+tag@example.org' + +- name: Re-run plus-addressed user creation (idempotency) + community.general.keycloak_user: + auth_keycloak_url: "{{ url }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + auth_realm: "{{ admin_realm }}" + username: "testuser+tag" + realm: "{{ realm }}" + first_name: Plus + last_name: User + email: "testuser+tag@example.org" + state: present + register: plus_idempotent_result + +- name: Assert plus-addressed user is idempotent + assert: + that: + - plus_idempotent_result is not changed + +- name: Delete plus-addressed user + community.general.keycloak_user: + auth_keycloak_url: "{{ url }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + auth_realm: "{{ admin_realm }}" + username: "testuser+tag" + realm: "{{ realm }}" + state: absent + register: plus_delete_result + +- name: Assert plus-addressed user is deleted + assert: + that: + - plus_delete_result.changed