1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-03-21 20:59:10 +00:00

keycloak_user_rolemapping: handle None response for client role lookup (#11471)

* fix(keycloak_user_rolemapping): handle None response for client role lookup

When adding a client role to a user who has no existing roles for that
client, get_client_user_rolemapping_by_id() returns None. The existing
code indexed directly into the result causing a TypeError. Add the same
None check that already existed for realm roles since PR #11256.

Fixes #10960

* fix(tests): use dict format for task vars in keycloak_user_rolemapping tests

Task-level vars requires a YAML mapping, not a sequence. The leading
dash (- roles:) produced a list instead of a dict, which ansible-core
2.20 rejects with "Vars in a Task must be specified as a dictionary".

* Update changelogs/fragments/keycloak-user-rolemapping-client-none-check.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Ivan Kokalovic 2026-02-18 20:24:35 +01:00 committed by GitHub
parent 80d21f2a0d
commit 34938ca1ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 73 additions and 11 deletions

View file

@ -0,0 +1,5 @@
bugfixes:
- keycloak_user_rolemapping - fix ``TypeError`` crash when adding a client
role to a user who has no existing roles for that client
(https://github.com/ansible-collections/community.general/issues/10960,
https://github.com/ansible-collections/community.general/pull/11471).

View file

@ -356,9 +356,9 @@ def main():
if role_rep is not None: if role_rep is not None:
role["name"] = role_rep["name"] role["name"] = role_rep["name"]
else: else:
role["name"] = kc.get_client_user_rolemapping_by_id( role_rep = kc.get_client_user_rolemapping_by_id(uid=uid, cid=cid, rid=role.get("id"), realm=realm)
uid=uid, cid=cid, rid=role.get("id"), realm=realm if role_rep is not None:
)["name"] role["name"] = role_rep["name"]
if role.get("name") is None: if role.get("name") is None:
module.fail_json( module.fail_json(
msg=f"Could not fetch role {role.get('id')} for client_id {client_id} or realm {realm}" msg=f"Could not fetch role {role.get('id')} for client_id {client_id} or realm {realm}"

View file

@ -37,8 +37,8 @@
- name: Map a realm role to client service account - name: Map a realm role to client service account
vars: vars:
- roles: roles:
- name: '{{ role }}' - name: '{{ role }}'
community.general.keycloak_user_rolemapping: community.general.keycloak_user_rolemapping:
auth_keycloak_url: "{{ url }}" auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}" auth_realm: "{{ admin_realm }}"
@ -58,8 +58,8 @@
- name: Unmap a realm role from client service account - name: Unmap a realm role from client service account
vars: vars:
- roles: roles:
- name: '{{ role }}' - name: '{{ role }}'
community.general.keycloak_user_rolemapping: community.general.keycloak_user_rolemapping:
auth_keycloak_url: "{{ url }}" auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}" auth_realm: "{{ admin_realm }}"
@ -89,6 +89,18 @@
name: "{{ role }}" name: "{{ role }}"
state: absent state: absent
- name: Create second client (for cross-client role mapping test)
community.general.keycloak_client:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
client_id: "{{ client_id_2 }}"
service_accounts_enabled: true
state: present
register: client_2
- name: Create new client role - name: Create new client role
community.general.keycloak_role: community.general.keycloak_role:
auth_keycloak_url: "{{ url }}" auth_keycloak_url: "{{ url }}"
@ -101,10 +113,54 @@
description: "{{ description_1 }}" description: "{{ description_1 }}"
state: present state: present
- name: Map a client role to a user with no existing roles for that client
vars:
roles:
- name: '{{ role }}'
community.general.keycloak_user_rolemapping:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
client_id: "{{ client_id }}"
service_account_user_client_id: "{{ client_id_2 }}"
roles: "{{ roles }}"
state: present
register: result
- name: Assert client role is assigned to user with no prior roles
assert:
that:
- result is changed
- result.end_state | selectattr("clientRole", "eq", true) | selectattr("name", "eq", role) | list | count > 0
- name: Unmap the cross-client role mapping
vars:
roles:
- name: '{{ role }}'
community.general.keycloak_user_rolemapping:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
client_id: "{{ client_id }}"
service_account_user_client_id: "{{ client_id_2 }}"
roles: "{{ roles }}"
state: absent
register: result
- name: Assert cross-client role mapping is removed
assert:
that:
- result is changed
- result.end_state == []
- name: Map a client role to client service account - name: Map a client role to client service account
vars: vars:
- roles: roles:
- name: '{{ role }}' - name: '{{ role }}'
community.general.keycloak_user_rolemapping: community.general.keycloak_user_rolemapping:
auth_keycloak_url: "{{ url }}" auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}" auth_realm: "{{ admin_realm }}"
@ -125,8 +181,8 @@
- name: Unmap a client role from client service account - name: Unmap a client role from client service account
vars: vars:
- roles: roles:
- name: '{{ role }}' - name: '{{ role }}'
community.general.keycloak_user_rolemapping: community.general.keycloak_user_rolemapping:
auth_keycloak_url: "{{ url }}" auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}" auth_realm: "{{ admin_realm }}"

View file

@ -9,6 +9,7 @@ admin_user: admin
admin_password: password admin_password: password
realm: myrealm realm: myrealm
client_id: myclient client_id: myclient
client_id_2: myotherclient
role: myrole role: myrole
description_1: desc 1 description_1: desc 1
description_2: desc 2 description_2: desc 2