Repository: ansible-collections/community.general

Issue:
keycloak_user_federation duplicate mapper name
### Summary

Creating an ldap user federation can't be run twice. 

### Issue Type

Bug Report

### Component Name

keycloak_user_federation

### Ansible Version

```
ansible [core 2.13.3]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/handel/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.10/site-packages/ansible
  ansible collection location = /home/handel/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.10.6 (main, Aug  3 2022, 17:39:45) [GCC 12.1.1 20220730]
  jinja version = 3.1.2
  libyaml = True

```


### Community.general Version

```
# /usr/lib/python3.10/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 5.5.0 
```


### Configuration

```empty
```


### OS / Environment

archlinux

keycloak 18.0.2

### Steps to Reproduce

the playbook task
```
- name: ldap
  tags:
  - ldap
  community.general.keycloak_user_federation:
    name: "{{ keycloak.default_realm }}"
    auth_client_id: "{{ keycloak.auth_client_id }}"
    auth_keycloak_url: "{{ keycloak.auth_keycloak_url }}"
    auth_realm: "{{ keycloak.auth_realm }}"
    auth_username: "{{ keycloak.auth_username }}"
    auth_password: "{{ keycloak.auth_password }}"
    realm: "{{ keycloak.default_realm }}"
    provider_id: ldap
    provider_type: org.keycloak.storage.UserStorageProvider
    config:
      # connection
      vendor: ad
      connectionUrl: "{{ ldap.connectionUrl }}"
      authType: simple
      bindDn: "{{ ldap.bindDn }}"
      bindCredential: "{{ ldap.bindCredential }}"
      batchSizeForSync: 500
      pagination: true
      # search users
      userObjectClasses: person, organizationalPerson, user
      usersDn: "{{ ldap.user_base }}"
      searchScope: 2
      # attributes
      uuidLDAPAttribute: objectGUID
      usernameLDAPAttribute: uid
      rdnLDAPAttribute: cn
      # settings
      editMode: READ_ONLY
      validatePasswordPolicy: false
      trustEmail: "true"
      importEnabled: true
      syncRegistrations: false
      usePasswordModifyExtendedOp: "false"
    mappers:
      - name: "username"
        providerId: "user-attribute-ldap-mapper"
        providerType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
        config:
          always.read.value.from.ldap: "false"
          is.mandatory.in.ldap: "true"
          ldap.attribute: "uid"
          read.only: "true"
          user.model.attribute: "username"
```

on the first run it will create the federation, on the second run it will fail with



### Expected Results

ansible creates the federation on the first run, won't change anything on the second run

### Actual Results

second run fails

```
fatal: [host]: FAILED! => {"changed": false, "msg": "Found multiple mappers with name `username`. Cannot continue."}
```

with a bit of debug output in keycloak_user_federation.py, here is what was found

```
FAILED! => {
"changed": false, 
"msg": "Found multiple mappers with name `
[
{'id': 'b2267b7f-063b-4046-ae5d-94cc36c4218b', 'name': 'username', 
'providerId': 'user-attribute-ldap-mapper', 'providerType': 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper', 
'parentId': '3a6b1799-3dd7-416f-ab99-c5cc256a9581', 
'config': {'ldap.attribute': ['uid'], 'is.mandatory.in.ldap': ['true'], 'always.read.value.from.ldap': ['false'], 'read.only': ['true'], 'user.model.attribute': ['username']}}, 

{'id': '6e952bf9-4df1-49ee-8e46-5386d94cb716', 'name': 'username', 
'providerId': 'user-attribute-ldap-mapper', 'providerType': 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper', 
'parentId': '3a6b1799-3dd7-416f-ab99-c5cc256a9581', 
'config': {'ldap.attribute': ['uid'], 'is.mandatory.in.ldap': ['true'], 'read.only': ['true'], 'always.read.value.from.ldap': ['false'], 'user.model.attribute': ['username']}}]`. 

Cannot continue."}
```


i assume keycloak will create a default mapper (with a random id) during creation. Ansible has the mappings cached (before the creation of the federation, in other words no mapping) and adds a second mapping (also with a random id). 

### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/identity/keycloak/keycloak_user_federation.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/identity/keycloak/keycloak_user_federation.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @eikef @laurpaum @ndclt
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Files identified in the description:
* [`plugins/modules/keycloak_user_federation.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/keycloak_user_federation.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->

Resolving patch:
diff --git a/plugins/modules/identity/keycloak/keycloak_user_federation.py b/plugins/modules/identity/keycloak/keycloak_user_federation.py
--- a/plugins/modules/identity/keycloak/keycloak_user_federation.py
+++ b/plugins/modules/identity/keycloak/keycloak_user_federation.py
@@ -24,7 +24,7 @@
       to your needs and a user having the expected roles.
 
     - The names of module options are snake_cased versions of the camelCase ones found in the
-      Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/15.0/rest-api/index.html).
+      Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/20.0.2/rest-api/index.html).
 
 
 options:
@@ -835,7 +835,7 @@ def main():
 
     # See if it already exists in Keycloak
     if cid is None:
-        found = kc.get_components(urlencode(dict(type='org.keycloak.storage.UserStorageProvider', parent=realm, name=name)), realm)
+        found = kc.get_components(urlencode(dict(type='org.keycloak.storage.UserStorageProvider', name=name)), realm)
         if len(found) > 1:
             module.fail_json(msg='No ID given and found multiple user federations with name `{name}`. Cannot continue.'.format(name=name))
         before_comp = next(iter(found), None)


Test patch:
diff --git a/tests/integration/targets/keycloak_user_federation/tasks/main.yml b/tests/integration/targets/keycloak_user_federation/tasks/main.yml
--- a/tests/integration/targets/keycloak_user_federation/tasks/main.yml
+++ b/tests/integration/targets/keycloak_user_federation/tasks/main.yml
@@ -66,6 +66,59 @@
       - result.existing == {}
       - result.end_state.name == "{{ federation }}"
 
+- name: Create new user federation in admin realm
+  community.general.keycloak_user_federation:
+    auth_keycloak_url: "{{ url }}"
+    auth_realm: "{{ admin_realm }}"
+    auth_username: "{{ admin_user }}"
+    auth_password: "{{ admin_password }}"
+    realm: "{{ admin_realm }}"
+    name: "{{ federation }}"
+    state: present
+    provider_id: ldap
+    provider_type: org.keycloak.storage.UserStorageProvider
+    config:
+      enabled: true
+      priority: 0
+      fullSyncPeriod: -1
+      changedSyncPeriod: -1
+      cachePolicy: DEFAULT
+      batchSizeForSync: 1000
+      editMode: READ_ONLY
+      importEnabled: true
+      syncRegistrations: false
+      vendor: other
+      usernameLDAPAttribute: uid
+      rdnLDAPAttribute: uid
+      uuidLDAPAttribute: entryUUID
+      userObjectClasses: "inetOrgPerson, organizationalPerson"
+      connectionUrl: "ldaps://ldap.example.com:636"
+      usersDn: "ou=Users,dc=example,dc=com"
+      authType: simple
+      bindDn: cn=directory reader
+      bindCredential: secret
+      searchScope: 1
+      validatePasswordPolicy: false
+      trustEmail: false
+      useTruststoreSpi: "ldapsOnly"
+      connectionPooling: true
+      pagination: true
+      allowKerberosAuthentication: false
+      useKerberosForPasswordAuthentication: false
+      debug: false
+  register: result
+
+- name: Debug
+  debug:
+    var: result
+
+- name: Assert user federation created (admin realm)
+  assert:
+    that:
+      - result is changed
+      - result.existing == {}
+      - result.end_state.name == "{{ federation }}"
+
 - name: Update existing user federation (no change)
   community.general.keycloak_user_federation:
     auth_keycloak_url: "{{ url }}"
@@ -121,6 +174,61 @@
       - result.end_state != {}
       - result.end_state.name == "{{ federation }}"
 
+- name: Update existing user federation (no change, admin realm)
+  community.general.keycloak_user_federation:
+    auth_keycloak_url: "{{ url }}"
+    auth_realm: "{{ admin_realm }}"
+    auth_username: "{{ admin_user }}"
+    auth_password: "{{ admin_password }}"
+    realm: "{{ admin_realm }}"
+    name: "{{ federation }}"
+    state: present
+    provider_id: ldap
+    provider_type: org.keycloak.storage.UserStorageProvider
+    config:
+      enabled: true
+      priority: 0
+      fullSyncPeriod: -1
+      changedSyncPeriod: -1
+      cachePolicy: DEFAULT
+      batchSizeForSync: 1000
+      editMode: READ_ONLY
+      importEnabled: true
+      syncRegistrations: false
+      vendor: other
+      usernameLDAPAttribute: uid
+      rdnLDAPAttribute: uid
+      uuidLDAPAttribute: entryUUID
+      userObjectClasses: "inetOrgPerson, organizationalPerson"
+      connectionUrl: "ldaps://ldap.example.com:636"
+      usersDn: "ou=Users,dc=example,dc=com"
+      authType: simple
+      bindDn: cn=directory reader
+      bindCredential: "**********"
+      searchScope: 1
+      validatePasswordPolicy: false
+      trustEmail: false
+      useTruststoreSpi: "ldapsOnly"
+      connectionPooling: true
+      pagination: true
+      allowKerberosAuthentication: false
+      useKerberosForPasswordAuthentication: false
+      debug: false
+  register: result
+
+- name: Debug
+  debug:
+    var: result
+
+- name: Assert user federation unchanged (admin realm)
+  assert:
+    that:
+      - result is not changed
+      - result.existing != {}
+      - result.existing.name == "{{ federation }}"
+      - result.end_state != {}
+      - result.end_state.name == "{{ federation }}"
+
 - name: Update existing user federation (with change)
   community.general.keycloak_user_federation:
     auth_keycloak_url: "{{ url }}"