--- # Configure Keycloak OIDC login for Automation Orchestrator. # # 1. Keycloak: mapped groups + confidential client with a "groups" claim mapper; # the client secret is read back rather than stored elsewhere. # 2. Backend Deployment: APP_OIDC_ALLOW_PRIVATE_NETWORKS (no CR field exists for it). # 3. AO REST API: create/update the identity provider with group mappings. # # AO identity providers are runtime configuration in the AO database — the # AutomationOrchestrator CR has no auth fields. - name: Validate group mappings ansible.builtin.assert: that: - automation_orchestrator_oidc_group_mappings | length > 0 - >- automation_orchestrator_oidc_group_mappings | map(attribute='group') | difference(__automation_orchestrator_oidc_valid_groups) | length == 0 fail_msg: >- automation_orchestrator_oidc_group_mappings must map at least one Keycloak group to an AO built-in group ({{ __automation_orchestrator_oidc_valid_groups | join(', ') }}); otherwise no Keycloak user can log in. # ------------------------------------------------------------------ # Step 1: Resolve the Automation Orchestrator public URL # ------------------------------------------------------------------ - name: Read Automation Orchestrator Route kubernetes.core.k8s_info: api_version: route.openshift.io/v1 kind: Route namespace: "{{ automation_orchestrator_oidc_namespace }}" name: "{{ automation_orchestrator_oidc_instance_name }}" register: __automation_orchestrator_oidc_route when: automation_orchestrator_oidc_url | length == 0 failed_when: __automation_orchestrator_oidc_route.resources | length == 0 - name: Set Automation Orchestrator URL ansible.builtin.set_fact: __automation_orchestrator_oidc_url: >- {{ (automation_orchestrator_oidc_url if automation_orchestrator_oidc_url | length > 0 else 'https://' + __automation_orchestrator_oidc_route.resources[0].spec.host) | regex_replace('/$', '') }} # ------------------------------------------------------------------ # Step 2: Keycloak client and groups # ------------------------------------------------------------------ - name: Configure Keycloak module_defaults: community.general.keycloak_group: &keycloak_auth auth_client_id: admin-cli auth_keycloak_url: "{{ automation_orchestrator_oidc_keycloak_url }}" auth_realm: master auth_username: "{{ automation_orchestrator_oidc_keycloak_admin_user }}" auth_password: "{{ automation_orchestrator_oidc_keycloak_admin_password }}" validate_certs: "{{ automation_orchestrator_oidc_keycloak_validate_certs }}" community.general.keycloak_clientsecret_info: *keycloak_auth middleware_automation.keycloak.keycloak_client: *keycloak_auth block: - name: Create mapped Keycloak groups community.general.keycloak_group: realm: "{{ automation_orchestrator_oidc_keycloak_realm }}" name: "{{ item }}" state: present loop: "{{ automation_orchestrator_oidc_group_mappings | map(attribute='idp_group') | unique }}" when: automation_orchestrator_oidc_create_keycloak_groups | bool # No secret is passed: Keycloak generates one on create and keeps it on update. - name: Create Automation Orchestrator OIDC client in Keycloak middleware_automation.keycloak.keycloak_client: realm: "{{ automation_orchestrator_oidc_keycloak_realm }}" client_id: "{{ automation_orchestrator_oidc_client_id }}" name: Automation Orchestrator description: OIDC client for Automation Orchestrator at {{ __automation_orchestrator_oidc_url }} enabled: true protocol: openid-connect public_client: false standard_flow_enabled: true implicit_flow_enabled: false direct_access_grants_enabled: false service_accounts_enabled: false root_url: "{{ __automation_orchestrator_oidc_url }}" redirect_uris: - "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/oidc/callback" web_origins: - "+" attributes: post.logout.redirect.uris: "{{ __automation_orchestrator_oidc_url }}##{{ __automation_orchestrator_oidc_url }}/*" protocol_mappers: - name: groups protocol: openid-connect protocolMapper: oidc-group-membership-mapper config: full.path: "false" id.token.claim: "true" access.token.claim: "true" userinfo.token.claim: "true" claim.name: groups state: present no_log: "{{ automation_orchestrator_oidc_no_log }}" - name: Read Automation Orchestrator client secret from Keycloak community.general.keycloak_clientsecret_info: realm: "{{ automation_orchestrator_oidc_keycloak_realm }}" client_id: "{{ automation_orchestrator_oidc_client_id }}" register: __automation_orchestrator_oidc_clientsecret no_log: "{{ automation_orchestrator_oidc_no_log }}" # ------------------------------------------------------------------ # Step 3: Allow private-network issuers on the backend # ------------------------------------------------------------------ - name: Set APP_OIDC_ALLOW_PRIVATE_NETWORKS on backend Deployment kubernetes.core.k8s: state: patched merge_type: strategic-merge api_version: apps/v1 kind: Deployment namespace: "{{ automation_orchestrator_oidc_namespace }}" name: "{{ automation_orchestrator_oidc_instance_name }}-backend" definition: spec: template: spec: containers: - name: backend env: - name: APP_OIDC_ALLOW_PRIVATE_NETWORKS value: "{{ automation_orchestrator_oidc_allow_private_networks | string | lower }}" register: __automation_orchestrator_oidc_backend_patch - name: Wait for backend rollout kubernetes.core.k8s_info: api_version: apps/v1 kind: Deployment namespace: "{{ automation_orchestrator_oidc_namespace }}" name: "{{ automation_orchestrator_oidc_instance_name }}-backend" register: __automation_orchestrator_oidc_backend until: >- __automation_orchestrator_oidc_backend.resources | length > 0 and (__automation_orchestrator_oidc_backend.resources[0].status.observedGeneration | default(0)) >= __automation_orchestrator_oidc_backend.resources[0].metadata.generation and (__automation_orchestrator_oidc_backend.resources[0].status.updatedReplicas | default(0)) == __automation_orchestrator_oidc_backend.resources[0].spec.replicas and (__automation_orchestrator_oidc_backend.resources[0].status.availableReplicas | default(0)) == __automation_orchestrator_oidc_backend.resources[0].spec.replicas retries: 30 delay: 10 when: __automation_orchestrator_oidc_backend_patch is changed # ------------------------------------------------------------------ # Step 4: Register Keycloak as an AO identity provider # ------------------------------------------------------------------ - name: Read AO initial admin password kubernetes.core.k8s_info: api_version: v1 kind: Secret namespace: "{{ automation_orchestrator_oidc_namespace }}" name: "{{ automation_orchestrator_oidc_instance_name }}-initial-admin-password" register: __automation_orchestrator_oidc_admin_secret no_log: "{{ automation_orchestrator_oidc_no_log }}" - name: Log in to the AO API ansible.builtin.uri: url: "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/login" method: POST body_format: json body: username: "{{ automation_orchestrator_oidc_admin_user }}" password: "{{ __automation_orchestrator_oidc_admin_secret.resources[0].data.password | b64decode }}" validate_certs: "{{ automation_orchestrator_oidc_validate_certs }}" register: __automation_orchestrator_oidc_login no_log: "{{ automation_orchestrator_oidc_no_log }}" - name: Call the AO API module_defaults: ansible.builtin.uri: headers: Authorization: "Bearer {{ __automation_orchestrator_oidc_login.json.access_token }}" validate_certs: "{{ automation_orchestrator_oidc_validate_certs }}" block: - name: List AO groups ansible.builtin.uri: url: "{{ __automation_orchestrator_oidc_url }}/api/v1/groups?limit=100" register: __automation_orchestrator_oidc_groups - name: List AO identity providers ansible.builtin.uri: url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers?limit=100" register: __automation_orchestrator_oidc_idps - name: Build desired identity provider configuration ansible.builtin.set_fact: __automation_orchestrator_oidc_existing: >- {{ __automation_orchestrator_oidc_idps.json.resources | selectattr('name', '==', automation_orchestrator_oidc_provider_name) | first | default({}) }} __automation_orchestrator_oidc_config: provider_type: oidc idp_type: custom auto_discovery: true issuer_url: "{{ __automation_orchestrator_oidc_issuer }}" client_id: "{{ automation_orchestrator_oidc_client_id }}" redirect_uri: "{{ __automation_orchestrator_oidc_url }}/api/v1/auth/oidc/callback" scopes: openid profile email group_jmespath_expression: groups allow_all_authenticated: false enable_rp_initiated_logout: "{{ automation_orchestrator_oidc_rp_initiated_logout | bool }}" __automation_orchestrator_oidc_group_ids: >- {{ __automation_orchestrator_oidc_groups.json.resources | items2dict(key_name='name', value_name='id') }} __automation_orchestrator_oidc_entries: [] - name: Build group mapping entries ansible.builtin.set_fact: __automation_orchestrator_oidc_entries: >- {{ __automation_orchestrator_oidc_entries + [{ 'idp_group_value': item.idp_group, 'mapped_group_id': __automation_orchestrator_oidc_group_ids[item.group]}] }} loop: "{{ automation_orchestrator_oidc_group_mappings }}" loop_control: label: "{{ item.idp_group }} -> {{ item.group }}" # Compare everything except client_secret (never returned by the API). - name: Determine whether the identity provider needs updating ansible.builtin.set_fact: __automation_orchestrator_oidc_needs_update: >- {{ automation_orchestrator_oidc_force_update | bool or not (__automation_orchestrator_oidc_existing.enabled | default(false)) or (__automation_orchestrator_oidc_existing.configuration | default({})) != ((__automation_orchestrator_oidc_existing.configuration | default({})) | combine(__automation_orchestrator_oidc_config)) or ((__automation_orchestrator_oidc_existing.configuration.group_mapping_entries | default([]) | map(attribute='idp_group_value') | zip(__automation_orchestrator_oidc_existing.configuration.group_mapping_entries | default([]) | map(attribute='mapped_group_id')) | map('join', '=') | sort) != (__automation_orchestrator_oidc_entries | map(attribute='idp_group_value') | zip(__automation_orchestrator_oidc_entries | map(attribute='mapped_group_id')) | map('join', '=') | sort)) }} when: __automation_orchestrator_oidc_existing | length > 0 - name: Create Keycloak identity provider in AO ansible.builtin.uri: url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers" method: POST body_format: json body: name: "{{ automation_orchestrator_oidc_provider_name }}" description: Keycloak realm {{ automation_orchestrator_oidc_keycloak_realm }} configuration: >- {{ __automation_orchestrator_oidc_config | combine({ 'client_secret': __automation_orchestrator_oidc_clientsecret.clientsecret_info.value, 'group_mapping_entries': __automation_orchestrator_oidc_entries}) }} status_code: 201 changed_when: true when: __automation_orchestrator_oidc_existing | length == 0 no_log: "{{ automation_orchestrator_oidc_no_log }}" - name: Update Keycloak identity provider in AO ansible.builtin.uri: url: "{{ __automation_orchestrator_oidc_url }}/api/v1/identity_providers/{{ __automation_orchestrator_oidc_existing.id }}" method: PATCH body_format: json body: enabled: true configuration: >- {{ __automation_orchestrator_oidc_config | combine({ 'client_secret': __automation_orchestrator_oidc_clientsecret.clientsecret_info.value, 'group_mapping_entries': __automation_orchestrator_oidc_entries}) }} changed_when: true when: - __automation_orchestrator_oidc_existing | length > 0 - __automation_orchestrator_oidc_needs_update | bool no_log: "{{ automation_orchestrator_oidc_no_log }}" # ------------------------------------------------------------------ # Step 5: Summary # ------------------------------------------------------------------ - name: Display Automation Orchestrator OIDC summary ansible.builtin.debug: msg: - "Keycloak OIDC configured for Automation Orchestrator:" - " Login URL : {{ __automation_orchestrator_oidc_url }}" - " Issuer : {{ __automation_orchestrator_oidc_issuer }}" - " Client : {{ automation_orchestrator_oidc_client_id }}" - " Mappings : {{ automation_orchestrator_oidc_group_mappings | map(attribute='idp_group') | zip(automation_orchestrator_oidc_group_mappings | map(attribute='group')) | map('join', ' -> ') | join(', ') }}" - "" - "Add users to the mapped Keycloak groups to grant access."