287 lines
14 KiB
YAML
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."
|