diff --git a/roles/docker-keycloak/meta/main.yml b/roles/docker-keycloak/meta/main.yml index 0607659a..65d2fd0b 100644 --- a/roles/docker-keycloak/meta/main.yml +++ b/roles/docker-keycloak/meta/main.yml @@ -21,4 +21,4 @@ galaxy_info: class: "fa-solid fa-lock" run_after: - docker-matomo -dependencies: [] + - docker-ldap diff --git a/roles/docker-keycloak/tasks/attributes/ssh_public_key.yml b/roles/docker-keycloak/tasks/attributes/ssh_public_key.yml new file mode 100644 index 00000000..d0e5d5a3 --- /dev/null +++ b/roles/docker-keycloak/tasks/attributes/ssh_public_key.yml @@ -0,0 +1,82 @@ +- name: "Wait until Keycloak is reachable at {{ keycloak_server_host_url }}" + uri: + url: "{{ keycloak_server_host_url }}/realms/master" + method: GET + status_code: 200 + validate_certs: false + register: keycloak_check + retries: 30 + delay: 5 + until: keycloak_check.status == 200 + +# Configure Credentials +- name: Ensure Keycloak CLI credentials are configured + shell: | + {{ keycloak_kcadm_path }} config credentials \ + --server {{ keycloak_server_internal_url }} \ + --realm master \ + --user {{ keycloak_administrator_username }} \ + --password {{ keycloak_administrator_password }} + +# LDAP Source +- name: Get ID of LDAP storage provider + shell: | + {{ keycloak_kcadm_path }} get components \ + -r {{ keycloak_realm }} \ + --query 'providerId=ldap' \ + --fields id,name \ + --format json + register: ldap_components + +- name: Extract LDAP component ID + set_fact: + ldap_component_id: "{{ (ldap_components.stdout | from_json)[0].id }}" + +- name: Ensure {{ ldap.attributes.ssh_public_key }} LDAP Mapper exists + shell: | + docker exec -i keycloak_application bash -c ' + /opt/keycloak/bin/kcadm.sh get components -r {{ keycloak_realm }} \ + | grep -q "\"name\" : \"{{ ldap.attributes.ssh_public_key }}\"" \ + || printf "%s\n" "{ + \"name\": \"{{ ldap.attributes.ssh_public_key }}\", + \"parentId\": \"{{ ldap_component_id }}\", + \"providerId\": \"user-attribute-ldap-mapper\", + \"providerType\": \"org.keycloak.storage.ldap.mappers.LDAPStorageMapper\", + \"config\": { + \"user.model.attribute\": [\"{{ ldap.attributes.ssh_public_key }}\"], + \"ldap.attribute\": [\"{{ ldap.attributes.ssh_public_key }}\"], + \"read.only\": [\"false\"], + \"write.only\": [\"true\"], + \"always.read.value.from.ldap\": [\"false\"], + \"multivalued\": [\"true\"] + } + }" | /opt/keycloak/bin/kcadm.sh create components -r {{ keycloak_realm }} -f -' + register: mapper_create + changed_when: mapper_create.rc == 0 and mapper_create.stdout != "" + +# GUI + +- name: Enable user profile in realm + shell: > + {{ keycloak_kcadm_path }} update realms/{{ keycloak_realm }} + -s 'attributes.userProfileEnabled=true' + +- name: Re-authenticate to Keycloak after enabling user profile + shell: | + {{ keycloak_kcadm_path }} config credentials \ + --server {{ keycloak_server_internal_url }} \ + --realm master \ + --user {{ keycloak_administrator_username }} \ + --password {{ keycloak_administrator_password }} + +- name: Render user-profile JSON for SSH key + template: + src: import/user-profile.json.j2 + dest: "{{ import_directory_host }}/user-profile.json" + mode: '0644' + notify: docker compose up + +- name: Apply SSH Public Key to user-profile via kcadm + shell: | + docker exec -i {{ container_name }} \ + /opt/keycloak/bin/kcadm.sh update realms/{{ keycloak_realm }} -f {{ import_directory_docker }}user-profile.json diff --git a/roles/docker-keycloak/tasks/main.yml b/roles/docker-keycloak/tasks/main.yml index 5f0ed425..66408578 100644 --- a/roles/docker-keycloak/tasks/main.yml +++ b/roles/docker-keycloak/tasks/main.yml @@ -24,4 +24,8 @@ dest: "{{ import_directory_host }}/{{ item | basename | regex_replace('\\.j2$', '') }}" mode: '770' loop: "{{ lookup('fileglob', '{{ role_path }}/templates/import/*.j2', wantlist=True) }}" - notify: docker compose up \ No newline at end of file + notify: docker compose up + +# Deactivated temporary. Import now via realm.yml +#- name: Implement SSH Public Key Attribut +# include_tasks: attributes/ssh_public_key.yml \ No newline at end of file diff --git a/roles/docker-keycloak/templates/docker-compose.yml.j2 b/roles/docker-keycloak/templates/docker-compose.yml.j2 index def8fb0e..1882e40e 100644 --- a/roles/docker-keycloak/templates/docker-compose.yml.j2 +++ b/roles/docker-keycloak/templates/docker-compose.yml.j2 @@ -8,7 +8,7 @@ services: command: start {% if applications[application_id].import_realm | bool %}--import-realm{% endif %} {% include 'roles/docker-compose/templates/services/base.yml.j2' %} ports: - - "127.0.0.1:{{ports.localhost.http[application_id]}}:8080" + - "{{ keycloak_server_host }}:8080" volumes: - "{{import_directory_host}}:{{import_directory_docker}}" {% include 'templates/docker/container/depends-on-just-database.yml.j2' %} diff --git a/roles/docker-keycloak/templates/env.j2 b/roles/docker-keycloak/templates/env.j2 index f8e77397..edc6465e 100644 --- a/roles/docker-keycloak/templates/env.j2 +++ b/roles/docker-keycloak/templates/env.j2 @@ -10,8 +10,11 @@ KC_HTTP_ENABLED= true KC_HEALTH_ENABLED= true KC_METRICS_ENABLED= true +# Administrator KEYCLOAK_ADMIN= "{{applications[application_id].users.administrator.username}}" KEYCLOAK_ADMIN_PASSWORD= "{{applications[application_id].credentials.administrator_password}}" + +# Database KC_DB= postgres KC_DB_URL= {{database_url_jdbc}} KC_DB_USERNAME= {{database_username}} diff --git a/roles/docker-keycloak/templates/import/realm.json.j2 b/roles/docker-keycloak/templates/import/realm.json.j2 index e9eaf376..53f2c0ea 100644 --- a/roles/docker-keycloak/templates/import/realm.json.j2 +++ b/roles/docker-keycloak/templates/import/realm.json.j2 @@ -1,6 +1,6 @@ { "id": "3b03105b-5fe6-4b53-ba24-c8796525be0e", - "realm": "{{realm}}", + "realm": "{{ keycloak_realm }}", "displayName": "", "displayNameHtml": "", "notBefore": 0, @@ -60,7 +60,7 @@ }, { "id": "01d9dd2a-75b2-47a2-af36-b14251f1b956", - "name": "default-roles-{{realm}}", + "name": "default-roles-{{ keycloak_realm }}", "description": "${role_default-roles}", "composite": true, "composites": { @@ -302,7 +302,7 @@ "attributes": {} } ], - "{{realm}}": [], + "{{ keycloak_realm }}": [], "security-admin-console": [], "admin-cli": [], "account-console": [], @@ -410,7 +410,7 @@ "groups": [], "defaultRole": { "id": "01d9dd2a-75b2-47a2-af36-b14251f1b956", - "name": "default-roles-{{realm}}", + "name": "default-roles-{{ keycloak_realm }}", "description": "${role_default-roles}", "composite": true, "clientRole": false, @@ -464,18 +464,18 @@ "users": [ { "id": "19ecedfd-acf2-49e8-9f66-91ab71d54fc3", - "username": "service-account-{{realm}}", + "username": "service-account-{{ keycloak_realm }}", "emailVerified": false, "createdTimestamp": 1737925519602, "enabled": true, "totp": false, - "serviceAccountClientId": "{{realm}}", + "serviceAccountClientId": "{{ keycloak_realm }}", "disableableCredentialTypes": [], "requiredActions": [ "CONFIGURE_TOTP" ], "realmRoles": [ - "default-roles-{{realm}}" + "default-roles-{{ keycloak_realm }}" ], "notBefore": 0, "groups": [] @@ -508,13 +508,13 @@ "description": "", "rootUrl": "${authBaseUrl}", "adminUrl": "", - "baseUrl": "/realms/{{realm}}/account/", + "baseUrl": "/realms/{{ keycloak_realm }}/account/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ - "/realms/{{realm}}/account/*" + "/realms/{{ keycloak_realm }}/account/*" ], "webOrigins": [ "{{ web_protocol }}://{{domains | get_domain('keycloak')}}" @@ -564,13 +564,13 @@ "description": "", "rootUrl": "${authBaseUrl}", "adminUrl": "", - "baseUrl": "/realms/{{realm}}/account/", + "baseUrl": "/realms/{{ keycloak_realm }}/account/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ - "/realms/{{realm}}/account/*" + "/realms/{{ keycloak_realm }}/account/*" ], "webOrigins": [ "*" @@ -756,13 +756,13 @@ "clientId": "security-admin-console", "name": "${client_security-admin-console}", "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/{{realm}}/console/", + "baseUrl": "/admin/{{ keycloak_realm }}/console/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "redirectUris": [ - "/admin/{{realm}}/console/*" + "/admin/{{ keycloak_realm }}/console/*" ], "webOrigins": [ "+" @@ -822,12 +822,12 @@ }, { "id": "7b5f97e3-7fa8-4d86-b1e9-80aac996da26", - "clientId": "{{realm}}", + "clientId": "{{ keycloak_realm }}", "name": "", "description": "", - "rootUrl": "{{ web_protocol }}://{{realm}}/", - "adminUrl": "{{ web_protocol }}://{{realm}}/", - "baseUrl": "{{ web_protocol }}://{{realm}}/", + "rootUrl": "{{ web_protocol }}://{{ keycloak_realm }}/", + "adminUrl": "{{ web_protocol }}://{{ keycloak_realm }}/", + "baseUrl": "{{ web_protocol }}://{{ keycloak_realm }}/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, @@ -1792,7 +1792,7 @@ "subComponents": {}, "config": { "kc.user.profile.config": [ - "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"pattern\":{\"pattern\":\"^[a-z0-9]+$\",\"error-message\":\"\"}},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"pattern\":{\"pattern\":\"^[a-z0-9]+$\",\"error-message\":\"\"}},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"{{ ldap.attributes.ssh_public_key }}\",\"displayName\":\"SSH Public Key\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"group\":\"user-metadata\",\"multivalued\":true}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}" ] } } @@ -1977,6 +1977,35 @@ ] } }, + { + "id": "24cd9c3b-e22d-4540-bddf-ae7faac0196c", + "name": "SSH Public Key", + "providerId": "user-attribute-ldap-mapper", + "subComponents": {}, + "config": { + "ldap.attribute": [ + "{{ ldap.attributes.ssh_public_key }}" + ], + "is.mandatory.in.ldap": [ + "false" + ], + "attribute.force.default": [ + "false" + ], + "is.binary.attribute": [ + "false" + ], + "read.only": [ + "false" + ], + "always.read.value.from.ldap": [ + "true" + ], + "user.model.attribute": [ + "{{ ldap.attributes.ssh_public_key }}" + ] + } + }, { "id": "85cd9847-4063-4d8b-be03-fa16377cde56", "name": "email", diff --git a/roles/docker-keycloak/vars/main.yml b/roles/docker-keycloak/vars/main.yml index ddc4261c..43d03244 100644 --- a/roles/docker-keycloak/vars/main.yml +++ b/roles/docker-keycloak/vars/main.yml @@ -1,6 +1,14 @@ -application_id: "keycloak" -database_type: "postgres" -container_name: "{{application_id}}_application" -realm: "{{primary_domain}}" # This is the name of the default realm which is used by the applications -import_directory_host: "{{docker_compose.directories.volumes}}import/" # Directory in which keycloack import files are placed on the host -import_directory_docker: "/opt/keycloak/data/import/" # Directory in which keycloack import files are placed in the running docker container \ No newline at end of file +application_id: "keycloak" +database_type: "postgres" +container_name: "{{application_id}}_application" +import_directory_host: "{{docker_compose.directories.volumes}}import/" # Directory in which keycloack import files are placed on the host +import_directory_docker: "/opt/keycloak/data/import/" # Directory in which keycloack import files are placed in the running docker container +keycloak_realm: "{{ primary_domain}}" # This is the name of the default realm which is used by the applications +keycloak_administrator: "{{ applications[application_id].users.administrator }}" # Master Administrator +keycloak_administrator_username: "{{ keycloak_administrator.username}}" # Master Administrator Username +keycloak_administrator_password: "{{ keycloak_administrator.password}}" # Master Administrator Password +keycloak_kcadm_path: "docker exec -i {{ container_name }} /opt/keycloak/bin/kcadm.sh" +keycloak_server_internal_url: "http://127.0.0.1:8080" +keycloak_server_host: "127.0.0.1:{{ports.localhost.http[application_id]}}" +keycloak_server_host_url: "http://{{ keycloak_server_host }}" + diff --git a/roles/docker-ldap/tasks/schemas/openssh_lpk.yml b/roles/docker-ldap/tasks/schemas/openssh_lpk.yml index f4572099..92cc16b1 100644 --- a/roles/docker-ldap/tasks/schemas/openssh_lpk.yml +++ b/roles/docker-ldap/tasks/schemas/openssh_lpk.yml @@ -8,7 +8,7 @@ vars: schema_name: "openssh-lpk" attribute_defs: - - "( 1.3.6.1.4.1.24552.1.1 NAME 'sshPublicKey' DESC 'OpenSSH Public Key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )" + - "( 1.3.6.1.4.1.24552.1.1 NAME '{{ ldap.attributes.ssh_public_key }}' DESC 'OpenSSH Public Key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )" - "( 1.3.6.1.4.1.24552.1.2 NAME 'sshFingerprint' DESC 'OpenSSH Public Key Fingerprint' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )" objectclass_defs: - >- @@ -17,7 +17,7 @@ DESC 'Auxiliary class for OpenSSH public keys' SUP top AUXILIARY - MAY ( sshPublicKey $ sshFingerprint ) ) + MAY ( {{ ldap.attributes.ssh_public_key }} $ sshFingerprint ) ) command: > ldapsm