INF-162 - Enable the LDAP configuration from REST API

- Create LDAP configuration
- Create LDAP mappers
- Enable Audit logging
- Verify the LDAP Authentication in Molecule environment
- Verify the user operation, create from Keycloak and search from LDAP

Change-Id: Ie6ea7f40cfe403ee3747a30b0bfb3acc9c72057f
diff --git a/README.md b/README.md
index 29133b7..94062b6 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,9 @@
 ```yaml
 - hosts: all
   vars:
+    keycloak_ldap_userdn: "ou=people,dc=testing,dc=org"
+    keycloak_ldap_admin_dn: "cn=Your Admin DN"
+    keycloak_ldap_admin_password: "changeme"
     keycloak_admin_password: changeme
     keycloak_client_settings:
       - name: ...
diff --git a/defaults/main.yml b/defaults/main.yml
index ba627dd..57b65e7 100644
--- a/defaults/main.yml
+++ b/defaults/main.yml
@@ -6,6 +6,8 @@
 #
 keycloak_java_version: 11
 
+keycloak_server: "http://localhost:8080"
+keycloak_admin_api: "{{ keycloak_server }}/auth/admin/realms/master"
 keycloak_username: "keycloak"
 keycloak_groupname: "keycloak"
 keycloak_comment: "Keycloak, an identity and access management solution "
@@ -28,3 +30,16 @@
 
 # client_settings is a list of Keycloak client to configure - see README.md
 keycloak_client_settings: []
+
+# ldap configuration
+keycloak_ldap_sync_registration: "true"
+keycloak_ldap_vendor: "rhds"
+keycloak_ldap_username: "uid"
+keycloak_ldap_rdn: "uid"
+keycloak_ldap_uuid: "nsuniqueid"
+keyclaok_ldap_user_object: "inetOrgPerson,organizationalPerson"
+keycloak_ldap_server: "ldap://127.0.0.1:389"
+keycloak_ldap_userdn: "ou=people,dc=testing,dc=org"
+keycloak_ldap_admin_dn: "cn=Directory Manager"
+keycloak_ldap_admin_password: "changeme"
+keycloak_ldap_group_dn: "ou=groups,dc=testing,dc=org"
diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml
index 5796c8d..3e075cf 100644
--- a/molecule/default/molecule.yml
+++ b/molecule/default/molecule.yml
@@ -19,6 +19,14 @@
   inventory:
     host_vars:
       debian-11-priv:
+        keycloak_server: "http://localhost:8080"
+        keycloak_admin_api: "{{ keycloak_server }}/auth/admin/realms/master"
+        keycloak_ldap_testing_user: "test2"
+        keycloak_ldap_server: "ldap://127.0.0.1:389"
+        keycloak_ldap_userdn: "ou=people,dc=testing,dc=org"
+        keycloak_ldap_admin_dn: "cn=Directory Manager"
+        keycloak_ldap_admin_password: "changeme"
+        keyclaok_ldap_user_object: "inetOrgPerson,organizationalPerson"
         keycloak_admin_username: "admin"
         keycloak_admin_password: "changeme"
         keycloak_client_settings:
diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml
new file mode 100644
index 0000000..7c28828
--- /dev/null
+++ b/molecule/default/prepare.yml
@@ -0,0 +1,15 @@
+---
+# keycloak molecule/default/prepare.yml
+#
+# SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+# SPDX-License-Identifier: Apache-2.0
+
+- name: Prepare for Keycloak by installing dependency roles
+  hosts: all
+
+  vars:
+    ds389_suffix: "dc=testing,dc=org"
+    ds389_root_password: >-
+        {SSHA512}hZUoI/9C3PeGS4nphClWw6Rg2CTX4P0S74GS3YxsmfQdeBobBEkKyUycxQ1HNB9TJHVokY9dpChFOfDPbKzmf3BbiOC1YAFI
+  roles:
+    - ds389
diff --git a/molecule/default/templates/ldap.testconnection.j2 b/molecule/default/templates/ldap.testconnection.j2
new file mode 100644
index 0000000..7b2950f
--- /dev/null
+++ b/molecule/default/templates/ldap.testconnection.j2
@@ -0,0 +1,15 @@
+{#
+SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+SPDX-License-Identifier: Apache-2.0
+#}
+{
+   "action":"testAuthentication",
+   "connectionUrl":"{{ keycloak_ldap_server }}",
+   "authType":"simple",
+   "bindDn":"{{ keycloak_ldap_admin_dn }}",
+   "bindCredential":"{{ keycloak_ldap_admin_password }}",
+   "useTruststoreSpi":"ldapsOnly",
+   "connectionTimeout":"",
+   "startTls":"",
+   "componentId":"{{ ldap_id }}"
+}
diff --git a/molecule/default/templates/ldap.testuser.j2 b/molecule/default/templates/ldap.testuser.j2
new file mode 100644
index 0000000..faa527e
--- /dev/null
+++ b/molecule/default/templates/ldap.testuser.j2
@@ -0,0 +1,16 @@
+{#
+SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+SPDX-License-Identifier: Apache-2.0
+#}
+{
+   "enabled":true,
+   "attributes":{
+   },
+   "groups":[
+   ],
+   "emailVerified":"",
+   "username":"{{ keycloak_ldap_testing_user }}",
+   "email":"test@test.com",
+   "firstName":"first",
+   "lastName":"last"
+}
diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml
index 74ba85a..15e0431 100644
--- a/molecule/default/verify.yml
+++ b/molecule/default/verify.yml
@@ -9,7 +9,7 @@
   tasks:
   - name: "Create Token for service Keycloak"
     uri:
-      url: http://localhost:8080/auth/realms/master/protocol/openid-connect/token
+      url: "{{ keycloak_server }}/auth/realms/master/protocol/openid-connect/token"
       method: POST
       body_format: form-urlencoded
       body:
@@ -21,7 +21,7 @@
 
   - name: "Get Client List"
     uri:
-      url: http://localhost:8080/auth/admin/realms/master/clients
+      url: "{{ keycloak_admin_api }}/clients"
       method: GET
       headers:
         Accept: "application/json"
@@ -38,3 +38,92 @@
     assert:
       that:
         - find is defined
+
+  - name: "Get existing LDAP configuration"
+    uri:
+      url: "{{ keycloak_admin_api }}/components?type=org.keycloak.storage.UserStorageProvider"
+      method: GET
+      headers:
+        Accept: "application/json"
+        Authorization: "Bearer {{ keycloak_token.json.access_token }}"
+    register: keycloak_components_list
+
+  - name: Check if the Keycloak already has the LDAP configuration
+    set_fact:
+      ldap_id: "{{ item.id }}"
+    with_items: "{{ keycloak_components_list.json }}"
+    when: item.name == "ldap"
+
+  - name: Generate a local json file for LDAP configuration
+    become: false
+    delegate_to: localhost
+    template:
+      src: "{{ item }}.j2"
+      dest: "/tmp/{{ item }}"
+      mode: "0600"
+    with_items:
+      - ldap.testconnection
+      - ldap.testuser
+
+  - name: Test LDAP Authentication
+    uri:
+      url: "{{ keycloak_admin_api }}/testLDAPConnection"
+      method: POST
+      src: /tmp/ldap.testconnection
+      status_code: [204]
+      headers:
+        Content-Type: application/json
+        Authorization: "Bearer {{ keycloak_token.json.access_token }}"
+
+  - name: Create user via Keycloak
+    uri:
+      url: "{{ keycloak_admin_api }}/users"
+      method: POST
+      src: /tmp/ldap.testuser
+      status_code: [201]
+      headers:
+        Content-Type: application/json
+        Authorization: "Bearer {{ keycloak_token.json.access_token }}"
+    register: keycloak_create_user_response
+
+  - name: Get User ID from previous response
+    set_fact:
+      user_id: "{{ keycloak_create_user_response.location | basename }}"
+
+  - name: Verify created user via LDAP
+    community.general.ldap_entry:
+      dn: "uid={{ keycloak_ldap_testing_user }},{{ keycloak_ldap_userdn }}"
+      objectClass: "{{ keyclaok_ldap_user_object }}"
+      server_uri: "{{ keycloak_ldap_server }}"
+      bind_dn: "{{ keycloak_ldap_admin_dn }}"
+      bind_pw: "{{ keycloak_ldap_admin_password }}"
+    register: result
+
+  - name: Delete user via Keycloak
+    uri:
+      url: "{{ keycloak_admin_api }}/users/{{ user_id }}"
+      method: DELETE
+      status_code: [204]
+      headers:
+        Content-Type: application/json
+        Authorization: "Bearer {{ keycloak_token.json.access_token }}"
+
+  - name: Verify removed user via LDAP
+    community.general.ldap_entry:
+      dn: "uid={{ keycloak_ldap_testing_user }},{{ keycloak_ldap_userdn }}"
+      objectClass: "{{ keyclaok_ldap_user_object }}"
+      server_uri: "{{ keycloak_ldap_server }}"
+      bind_dn: "{{ keycloak_ldap_admin_dn }}"
+      bind_pw: "{{ keycloak_ldap_admin_password }}"
+    register: result
+    failed_when:
+      - '"missing attribute" not in result.details'
+
+  - name: Remove local LDAP json file
+    delegate_to: localhost
+    file:
+      path: "/tmp/{{ item }}"
+      state: absent
+    with_items:
+      - ldap.testconnection
+      - ldap.testuser
diff --git a/tasks/main.yml b/tasks/main.yml
index 5722125..aa31ddb 100644
--- a/tasks/main.yml
+++ b/tasks/main.yml
@@ -57,7 +57,7 @@
     chdir: "{{ keycloak_working_dir }}/bin/"
     cmd: >
       ./kcadm.sh get users
-      --server http://localhost:8080/auth
+      --server {{ keycloak_server }}/auth
       --realm master --user {{ keycloak_admin_username }}
       --password {{ keycloak_admin_password }}
   register: kcadm_result
@@ -94,7 +94,7 @@
 
 - name: Configure Keycloak client
   community.general.keycloak_client:
-    auth_keycloak_url: http://localhost:8080/auth
+    auth_keycloak_url: "{{ keycloak_server }}/auth"
     auth_realm: "{{ item.auth_realm }}"
     auth_username: "{{ keycloak_admin_username }}"
     auth_password: "{{ keycloak_admin_password }}"
@@ -107,3 +107,112 @@
     protocol_mappers: "{{ item.protocol_mappers }}"
     state: present
   with_items: "{{ keycloak_client_settings }}"
+
+- name: "Create Token for Keycloak service"
+  uri:
+    url: "{{ keycloak_server }}/auth/realms/master/protocol/openid-connect/token"
+    method: POST
+    body_format: form-urlencoded
+    body:
+      username: "{{ keycloak_admin_username }}"
+      password: "{{ keycloak_admin_password }}"
+      grant_type: "password"
+      client_id: "admin-cli"
+  register: keycloak_token
+
+- name: "Get existing LDAP configuration"
+  uri:
+    url: "{{ keycloak_admin_api }}/components?type=org.keycloak.storage.UserStorageProvider"
+    method: GET
+    headers:
+      Accept: "application/json"
+      Authorization: "Bearer {{ keycloak_token.json.access_token }}"
+  register: keycloak_components_list
+
+- name: Check if the Keycloak already has the LDAP configuration
+  set_fact:
+    ldap_id: "{{ item.id }}"
+  with_items: "{{ keycloak_components_list.json }}"
+  when: item.name == "ldap"
+
+- name: Generate a local json file for LDAP configuration
+  become: false
+  delegate_to: localhost
+  template:
+    src: "ldap.config.j2"
+    dest: "/tmp/ldap.config"
+    mode: "0600"
+  changed_when: false
+
+- name: "Create LDAP Provider if not exist"
+  uri:
+    url: "{{ keycloak_admin_api }}/components"
+    method: POST
+    src: /tmp/ldap.config
+    status_code: [201]
+    headers:
+      Content-Type: application/json
+      Authorization: "Bearer {{ keycloak_token.json.access_token }}"
+  register: keycloak_create_ldap_response
+  when: ldap_id is not defined
+
+- name: "Update LDAP Provider if exist"
+  uri:
+    url: "{{ keycloak_admin_api }}/components/{{ ldap_id }}"
+    method: PUT
+    src: /tmp/ldap.config
+    status_code: [204]
+    headers:
+      Content-Type: application/json
+      Authorization: "Bearer {{ keycloak_token.json.access_token }}"
+  when: ldap_id is defined
+
+- name: Update LDAP_ID with new created LDAP components
+  set_fact:
+    ldap_id: "{{ keycloak_create_ldap_response.location | basename }}"
+  when: ldap_id is not defined
+
+- name: Generate a local json file for LDAP mapper configuration
+  become: false
+  delegate_to: localhost
+  template:
+    src: "{{ item }}.j2"
+    dest: "/tmp/{{ item }}"
+    mode: "0600"
+  with_items:
+    - ldap.mapper.group
+    - keycloak.event.config
+  changed_when: false
+
+- name: Create LDAP mapper from local json configuraiton
+  uri:
+    url: "{{ keycloak_admin_api }}/components/"
+    method: POST
+    src: "/tmp/{{ item }}"
+    status_code: [201]
+    headers:
+      Content-Type: application/json
+      Authorization: "Bearer {{ keycloak_token.json.access_token }}"
+  with_items:
+    - ldap.mapper.group
+
+- name: Create LDAP mapper from local json configuraiton
+  uri:
+    url: "{{ keycloak_admin_api }}/events/config"
+    method: PUT
+    src: "/tmp/keycloak.event.config"
+    status_code: [204]
+    headers:
+      Content-Type: application/json
+      Authorization: "Bearer {{ keycloak_token.json.access_token }}"
+
+- name: Remove local LDAP json file
+  delegate_to: localhost
+  file:
+    path: "/tmp/{{ item }}"
+    state: absent
+  with_items:
+    - ldap.config
+    - ldap.mapper.group
+    - keycloak.event.config
+  changed_when: false
diff --git a/templates/keycloak.event.config.j2 b/templates/keycloak.event.config.j2
new file mode 100644
index 0000000..d13367d
--- /dev/null
+++ b/templates/keycloak.event.config.j2
@@ -0,0 +1,93 @@
+{#
+SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+SPDX-License-Identifier: Apache-2.0
+#}
+{
+   "eventsEnabled":true,
+   "eventsListeners":[
+      "jboss-logging"
+   ],
+   "enabledEventTypes":[
+      "SEND_RESET_PASSWORD",
+      "UPDATE_CONSENT_ERROR",
+      "GRANT_CONSENT",
+      "VERIFY_PROFILE_ERROR", "REMOVE_TOTP",
+      "REVOKE_GRANT",
+      "UPDATE_TOTP",
+      "LOGIN_ERROR",
+      "CLIENT_LOGIN",
+      "RESET_PASSWORD_ERROR",
+      "IMPERSONATE_ERROR",
+      "CODE_TO_TOKEN_ERROR",
+      "CUSTOM_REQUIRED_ACTION",
+      "OAUTH2_DEVICE_CODE_TO_TOKEN_ERROR",
+      "RESTART_AUTHENTICATION",
+      "IMPERSONATE",
+      "UPDATE_PROFILE_ERROR",
+      "LOGIN",
+      "OAUTH2_DEVICE_VERIFY_USER_CODE",
+      "UPDATE_PASSWORD_ERROR",
+      "CLIENT_INITIATED_ACCOUNT_LINKING",
+      "TOKEN_EXCHANGE",
+      "AUTHREQID_TO_TOKEN",
+      "LOGOUT",
+      "REGISTER",
+      "DELETE_ACCOUNT_ERROR",
+      "CLIENT_REGISTER",
+      "IDENTITY_PROVIDER_LINK_ACCOUNT",
+      "DELETE_ACCOUNT",
+      "UPDATE_PASSWORD",
+      "CLIENT_DELETE",
+      "FEDERATED_IDENTITY_LINK_ERROR",
+      "IDENTITY_PROVIDER_FIRST_LOGIN",
+      "CLIENT_DELETE_ERROR",
+      "VERIFY_EMAIL",
+      "CLIENT_LOGIN_ERROR",
+      "RESTART_AUTHENTICATION_ERROR",
+      "EXECUTE_ACTIONS",
+      "REMOVE_FEDERATED_IDENTITY_ERROR",
+      "TOKEN_EXCHANGE_ERROR",
+      "PERMISSION_TOKEN",
+      "SEND_IDENTITY_PROVIDER_LINK_ERROR",
+      "EXECUTE_ACTION_TOKEN_ERROR",
+      "SEND_VERIFY_EMAIL",
+      "OAUTH2_DEVICE_AUTH",
+      "EXECUTE_ACTIONS_ERROR",
+      "REMOVE_FEDERATED_IDENTITY",
+      "OAUTH2_DEVICE_CODE_TO_TOKEN",
+      "IDENTITY_PROVIDER_POST_LOGIN",
+      "IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR",
+      "OAUTH2_DEVICE_VERIFY_USER_CODE_ERROR",
+      "UPDATE_EMAIL",
+      "REGISTER_ERROR",
+      "REVOKE_GRANT_ERROR",
+      "EXECUTE_ACTION_TOKEN",
+      "LOGOUT_ERROR",
+      "UPDATE_EMAIL_ERROR",
+      "CLIENT_UPDATE_ERROR",
+      "AUTHREQID_TO_TOKEN_ERROR",
+      "UPDATE_PROFILE",
+      "CLIENT_REGISTER_ERROR",
+      "FEDERATED_IDENTITY_LINK",
+      "SEND_IDENTITY_PROVIDER_LINK",
+      "SEND_VERIFY_EMAIL_ERROR",
+      "RESET_PASSWORD",
+      "CLIENT_INITIATED_ACCOUNT_LINKING_ERROR",
+      "OAUTH2_DEVICE_AUTH_ERROR",
+      "UPDATE_CONSENT",
+      "REMOVE_TOTP_ERROR",
+      "VERIFY_EMAIL_ERROR",
+      "SEND_RESET_PASSWORD_ERROR",
+      "CLIENT_UPDATE",
+      "CUSTOM_REQUIRED_ACTION_ERROR",
+      "IDENTITY_PROVIDER_POST_LOGIN_ERROR",
+      "UPDATE_TOTP_ERROR",
+      "CODE_TO_TOKEN",
+      "VERIFY_PROFILE",
+      "GRANT_CONSENT_ERROR",
+      "IDENTITY_PROVIDER_FIRST_LOGIN_ERROR"
+   ],
+   "adminEventsEnabled":true,
+   "adminEventsDetailsEnabled":true,
+   "eventsExpiration":null
+}
diff --git a/templates/ldap.config.j2 b/templates/ldap.config.j2
new file mode 100644
index 0000000..81a87b3
--- /dev/null
+++ b/templates/ldap.config.j2
@@ -0,0 +1,138 @@
+{#
+SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+SPDX-License-Identifier: Apache-2.0
+#}
+
+{
+{% if ldap_id is defined %}
+   "id": "{{ ldap_id}}",
+{% endif %}
+   "name":"ldap",
+   "providerId":"ldap",
+   "providerType":"org.keycloak.storage.UserStorageProvider",
+   "parentId":"master",
+   "config":{
+      "enabled":[
+         "true"
+      ],
+      "priority":[
+         "0"
+      ],
+      "fullSyncPeriod":[
+         "-1"
+      ],
+      "changedSyncPeriod":[
+         "-1"
+      ],
+      "cachePolicy":[
+         "DEFAULT"
+      ],
+      "evictionDay":[
+      ],
+      "evictionHour":[
+      ],
+      "evictionMinute":[
+      ],
+      "maxLifespan":[
+      ],
+      "batchSizeForSync":[
+         "1000"
+      ],
+      "editMode":[
+         "WRITABLE"
+      ],
+      "importEnabled":[
+         "true"
+      ],
+      "syncRegistrations":[
+         "{{ keycloak_ldap_sync_registration }}"
+      ],
+      "vendor":[
+         "{{ keycloak_ldap_vendor }}"
+      ],
+      "usePasswordModifyExtendedOp":[
+      ],
+      "usernameLDAPAttribute":[
+         "{{ keycloak_ldap_username }}"
+      ],
+      "rdnLDAPAttribute":[
+         "{{ keycloak_ldap_rdn }}"
+      ],
+      "uuidLDAPAttribute":[
+         "{{ keycloak_ldap_uuid }}"
+      ],
+      "userObjectClasses":[
+         "{{ keyclaok_ldap_user_object }}"
+      ],
+      "connectionUrl":[
+         "{{ keycloak_ldap_server }}"
+      ],
+      "usersDn":[
+         "{{ keycloak_ldap_userdn }}"
+      ],
+      "authType":[
+         "simple"
+      ],
+      "startTls":[
+      ],
+      "bindDn":[
+         "{{ keycloak_ldap_admin_dn }}"
+      ],
+      "bindCredential":[
+         "{{ keycloak_ldap_admin_password }}"
+      ],
+      "customUserSearchFilter":[
+      ],
+      "searchScope":[
+         "1"
+      ],
+      "validatePasswordPolicy":[
+         "false"
+      ],
+      "trustEmail":[
+         "false"
+      ],
+      "useTruststoreSpi":[
+         "ldapsOnly"
+      ],
+      "connectionPooling":[
+         "true"
+      ],
+      "connectionPoolingAuthentication":[
+      ],
+      "connectionPoolingDebug":[
+      ],
+      "connectionPoolingInitSize":[
+      ],
+      "connectionPoolingMaxSize":[
+      ],
+      "connectionPoolingPrefSize":[
+      ],
+      "connectionPoolingProtocol":[
+      ],
+      "connectionPoolingTimeout":[
+      ],
+      "connectionTimeout":[
+      ],
+      "readTimeout":[
+      ],
+      "pagination":[
+         "true"
+      ],
+      "allowKerberosAuthentication":[
+         "false"
+      ],
+      "serverPrincipal":[
+      ],
+      "keyTab":[
+      ],
+      "kerberosRealm":[
+      ],
+      "debug":[
+         "false"
+      ],
+      "useKerberosForPasswordAuthentication":[
+         "false"
+      ]
+   }
+}
diff --git a/templates/ldap.mapper.group.j2 b/templates/ldap.mapper.group.j2
new file mode 100644
index 0000000..c8e2269
--- /dev/null
+++ b/templates/ldap.mapper.group.j2
@@ -0,0 +1,55 @@
+{#
+SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+SPDX-License-Identifier: Apache-2.0
+#}
+{
+   "config":{
+      "groups.dn":[
+         "{{ keycloak_ldap_group_dn }}"
+      ],
+      "group.name.ldap.attribute":[
+         "cn"
+      ],
+      "group.object.classes":[
+         "groupOfNames"
+      ],
+      "preserve.group.inheritance":[
+         "true"
+      ],
+      "ignore.missing.groups":[
+         "false"
+      ],
+      "membership.ldap.attribute":[
+         "member"
+      ],
+      "membership.attribute.type":[
+         "DN"
+      ],
+      "membership.user.ldap.attribute":[
+         "uid"
+      ],
+      "groups.ldap.filter":[
+      ],
+      "mode":[
+         "LDAP_ONLY"
+      ],
+      "user.roles.retrieve.strategy":[
+         "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"
+      ],
+      "memberof.ldap.attribute":[
+         "memberOf"
+      ],
+      "mapped.group.attributes":[
+      ],
+      "drop.non.existing.groups.during.sync":[
+         "true"
+      ],
+      "groups.path":[
+         "/"
+      ]
+   },
+   "name":"group",
+   "providerId":"group-ldap-mapper",
+   "providerType":"org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
+   "parentId":"{{ ldap_id }}"
+}