Files
toallab-automation/roles/automation_orchestrator_oidc/tasks/main.yml

287 lines
14 KiB
YAML

---
# 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."