diff --git a/roles/web-app-keycloak/config/main.yml b/roles/web-app-keycloak/config/main.yml index 725807d0..c8840f9d 100644 --- a/roles/web-app-keycloak/config/main.yml +++ b/roles/web-app-keycloak/config/main.yml @@ -1,6 +1,7 @@ -load_dependencies: True # When set to false the dependencies aren't loaded. Helpful for developing +load_dependencies: True # When set to false the dependencies aren't loaded. Helpful for developing actions: - import_realm: True # Import REALM + import_realm: True # Import REALM + create_automation_client: True features: matomo: true css: true diff --git a/roles/web-app-keycloak/tasks/05_rbac_client_scope.yml b/roles/web-app-keycloak/tasks/05_rbac_client_scope.yml index 0d62ffdd..fd7bd15a 100644 --- a/roles/web-app-keycloak/tasks/05_rbac_client_scope.yml +++ b/roles/web-app-keycloak/tasks/05_rbac_client_scope.yml @@ -21,6 +21,7 @@ shell: "{{ KEYCLOAK_EXEC_KCADM }} get client-scopes -r {{ KEYCLOAK_REALM }} --format json" register: all_scopes changed_when: false + failed_when: "'HTTP 401' in (all_scopes.stderr | default(''))" - name: Extract RBAC scope id set_fact: diff --git a/roles/web-app-keycloak/tasks/05a_service_account.yml b/roles/web-app-keycloak/tasks/05a_service_account.yml new file mode 100644 index 00000000..623ff2cb --- /dev/null +++ b/roles/web-app-keycloak/tasks/05a_service_account.yml @@ -0,0 +1,63 @@ +# Creates a confidential client with service account, fetches the secret, +# and grants realm-management/realm-admin to its service-account user. + +- name: "Ensure automation client exists (confidential + service accounts)" + shell: | + {{ KEYCLOAK_EXEC_KCADM }} create clients -r {{ KEYCLOAK_REALM }} \ + -s clientId={{ KEYCLOAK_AUTOMATION_CLIENT_ID }} \ + -s protocol=openid-connect \ + -s publicClient=false \ + -s serviceAccountsEnabled=true \ + -s directAccessGrantsEnabled=false + register: create_client + changed_when: create_client.rc == 0 + failed_when: create_client.rc != 0 and ('already exists' not in (create_client.stderr | lower)) + +- name: "Resolve automation client id" + shell: > + {{ KEYCLOAK_EXEC_KCADM }} get clients -r {{ KEYCLOAK_REALM }} + --query 'clientId={{ KEYCLOAK_AUTOMATION_CLIENT_ID }}' --fields id --format json | jq -r '.[0].id' + register: auto_client_id_cmd + changed_when: false + +- name: "Fail if client id could not be resolved" + assert: + that: + - "(auto_client_id_cmd.stdout | trim) is match('^[0-9a-f-]+$')" + fail_msg: "Automation client id could not be resolved." + +- name: "Read client secret" + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" + shell: > + {{ KEYCLOAK_EXEC_KCADM }} get clients/{{ auto_client_id_cmd.stdout | trim }}/client-secret + -r {{ KEYCLOAK_REALM }} --format json | jq -r .value + register: auto_client_secret_cmd + changed_when: false + +- name: "Expose client secret as a fact" + set_fact: + KEYCLOAK_AUTOMATION_CLIENT_SECRET: "{{ auto_client_secret_cmd.stdout | trim }}" + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" + +- name: "Grant {{ KEYCLOAK_AUTOMATION_GRANT_ROLE }} to service account" + shell: > + {{ KEYCLOAK_EXEC_KCADM }} add-roles -r {{ KEYCLOAK_REALM }} + --uusername service-account-{{ KEYCLOAK_AUTOMATION_CLIENT_ID }} + --cclientid realm-management + --rolename {{ KEYCLOAK_AUTOMATION_GRANT_ROLE }} + register: grant_role + changed_when: grant_role.rc == 0 + failed_when: grant_role.rc != 0 and ('already exists' not in (grant_role.stderr | lower)) + +- name: "Verify client-credentials login works" + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" + shell: > + {{ KEYCLOAK_EXEC_KCADM }} config credentials + --server {{ KEYCLOAK_SERVER_INTERNAL_URL }} + --realm {{ KEYCLOAK_REALM }} + --client {{ KEYCLOAK_AUTOMATION_CLIENT_ID }} + --client-secret {{ KEYCLOAK_AUTOMATION_CLIENT_SECRET }} && + {{ KEYCLOAK_EXEC_KCADM }} get realms/{{ KEYCLOAK_REALM }} --format json | jq -r '.realm' + register: verify_cc + changed_when: false + failed_when: (verify_cc.rc != 0) or ((verify_cc.stdout | trim) != (KEYCLOAK_REALM | trim)) \ No newline at end of file diff --git a/roles/web-app-keycloak/tasks/main.yml b/roles/web-app-keycloak/tasks/main.yml index e60650b2..fc5b0a7b 100644 --- a/roles/web-app-keycloak/tasks/main.yml +++ b/roles/web-app-keycloak/tasks/main.yml @@ -36,6 +36,42 @@ --password {{ KEYCLOAK_MASTER_API_USER_PASSWORD }} changed_when: false +- name: Verify kcadm session works (quick read) + shell: > + {{ KEYCLOAK_EXEC_KCADM }} get realms --format json | jq -r '.[0].realm' | head -n1 + register: kcadm_verify + changed_when: false + failed_when: > + (kcadm_verify.rc != 0) + or ('HTTP 401' in (kcadm_verify.stderr | default(''))) + or ((kcadm_verify.stdout | trim) == '') + +# --- Create & grant automation service account (Option A) --- +- name: "Ensure automation service account client (Option A)" + include_tasks: 05a_service_account.yml + when: applications | get_app_conf(application_id, 'actions.create_automation_client', True) + +# --- Switch session to the service account for all subsequent API work --- +- name: kcadm login (realm) using service account + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" + shell: > + {{ KEYCLOAK_EXEC_KCADM }} config credentials + --server {{ KEYCLOAK_SERVER_INTERNAL_URL }} + --realm {{ KEYCLOAK_REALM }} + --client {{ KEYCLOAK_AUTOMATION_CLIENT_ID }} + --client-secret {{ KEYCLOAK_AUTOMATION_CLIENT_SECRET }} + changed_when: false + +- name: Verify kcadm session works (exact realm via service account) + shell: > + {{ KEYCLOAK_EXEC_KCADM }} get realms/{{ KEYCLOAK_REALM }} --format json | jq -r '.realm' + register: kcadm_verify_sa + changed_when: false + failed_when: > + (kcadm_verify_sa.rc != 0) + or ('HTTP 401' in (kcadm_verify_sa.stderr | default(''))) + or ((kcadm_verify_sa.stdout | trim) != (KEYCLOAK_REALM | trim)) + - name: "Update Client settings" vars: kc_object_kind: "client" diff --git a/roles/web-app-keycloak/vars/main.yml b/roles/web-app-keycloak/vars/main.yml index cebb4344..2b07920e 100644 --- a/roles/web-app-keycloak/vars/main.yml +++ b/roles/web-app-keycloak/vars/main.yml @@ -1,6 +1,6 @@ # General -application_id: "web-app-keycloak" # Internal Infinito.Nexus application id -database_type: "postgres" # Database which will be used +application_id: "web-app-keycloak" # Internal Infinito.Nexus application id +database_type: "postgres" # Database which will be used # Keycloak @@ -34,9 +34,16 @@ KEYCLOAK_ADMIN_PASSWORD: "{{ applications | get_app_conf(application_ ## Docker KEYCLOAK_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name') }}" # Name of the keycloak docker container -KEYCLOAK_EXEC_KCADM: "docker exec -i {{ KEYCLOAK_CONTAINER }} /opt/keycloak/bin/kcadm.sh" # Init script for keycloak KEYCLOAK_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image') }}" # Keycloak docker image KEYCLOAK_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version') }}" # Keycloak docker version +KEYCLOAK_KCADM_CONFIG: "/opt/keycloak/data/kcadm.config" +KEYCLOAK_EXEC_KCADM: "docker exec -i {{ KEYCLOAK_CONTAINER }} /opt/keycloak/bin/kcadm.sh --config {{ KEYCLOAK_KCADM_CONFIG }}" + +## Automation Service Account (Option A) +KEYCLOAK_AUTOMATION_CLIENT_ID: "infinito-automation" +KEYCLOAK_AUTOMATION_GRANT_ROLE: "realm-admin" # or granular roles if you prefer +# Will be discovered dynamically and set as a fact during the run: +# KEYCLOAK_AUTOMATION_CLIENT_SECRET ## Server KEYCLOAK_SERVER_HOST: "127.0.0.1:{{ ports.localhost.http[application_id] }}"