Compare commits

...

20 Commits
3.0.7 ... 3.0.9

Author SHA1 Message Date
ansible-middleware-core
f172e019b4 Update changelog for release 3.0.9
Signed-off-by: ansible-middleware-core <ansible-middleware-core@redhat.com>
2026-06-11 15:16:05 +00:00
Harsha Cherukuri
e764cfd6f1 Merge pull request #344 from RanabirChakraborty/rhbk_version_fix
AMW-551 Providing correct rhbk version
2026-06-11 09:48:33 -04:00
Ranabir Chakraborty
01b00dfb2e Providing correct rhbk version 2026-06-11 18:58:23 +05:30
Harsha Cherukuri
1908794569 Merge pull request #349 from felix-grzelka/consistent-naming-of-client-scope
Consistent naming of client scope
2026-06-10 09:07:00 -04:00
Harsha Cherukuri
c8bcff39ef Fix keycloak client module 2026-06-10 08:22:52 -04:00
Felix Grzelka
0fad56294b fix typo 2026-06-10 11:14:23 +00:00
ansible-middleware-core
b2b52ddfb5 Bump version to 3.0.9 2026-06-09 14:43:40 +00:00
ansible-middleware-core
7369a5724c Update changelog for release 3.0.8
Signed-off-by: ansible-middleware-core <ansible-middleware-core@redhat.com>
2026-06-09 14:43:23 +00:00
Felix Grzelka
942f0ae896 fix rename payload -> roles 2026-06-09 07:14:32 +00:00
Felix Grzelka
92d6dddd49 rename to role_owner_client_id 2026-06-08 13:21:27 +00:00
Felix Grzelka
a1bb84ea5b fix some missed ones 2026-06-08 12:20:32 +00:00
Felix Grzelka
1ec94b961f rename files to match new convention 2026-06-08 12:00:52 +00:00
Felix Grzelka
ba3f716e5c fix scope_id etc. 2026-06-08 12:00:52 +00:00
Felix Grzelka
808d137e4c s/clientscope/client_scope/ 2026-06-08 12:00:52 +00:00
Harsha Cherukuri
1794d4ff9b Merge pull request #348 from felix-grzelka/fix-normalize_keycloak_url
Remove normalize_keycloak_url
2026-06-05 07:40:20 -04:00
Felix Grzelka
e898a2511a rm other use of normalize_keycloak_url 2026-06-05 07:29:53 +00:00
Felix Grzelka
dd2cfaa87d remove normalize_keycloak_url 2026-06-04 13:20:03 +00:00
Harsha Cherukuri
b114c7b252 Merge pull request #345 from cihlamar/standardize-readme-gitignore-lint
Standardize README and .ansible-lint configuration
2026-06-03 12:42:23 -04:00
Martin Cihlar
9920dc93c9 Standardize README and .ansible-lint configuration
- Update role links in README to full GitHub URLs
- Add profile: production to .ansible-lint for production-level linting
2026-06-03 17:11:56 +02:00
ansible-middleware-core
5cb555d6c2 Bump version to 3.0.8 2026-06-01 14:50:07 +00:00
33 changed files with 737 additions and 490 deletions

View File

@@ -1,4 +1,6 @@
# .ansible-lint # .ansible-lint
profile: production
exclude_paths: exclude_paths:
- .cache/ - .cache/
- .github/ - .github/

View File

@@ -6,6 +6,17 @@ middleware\_automation.keycloak Release Notes
This changelog describes changes after version 0.2.6. This changelog describes changes after version 0.2.6.
v3.0.9
======
Bugfixes
--------
- AMW-551 Providing correct rhbk version `#344 <https://github.com/ansible-middleware/keycloak/pull/344>`_
v3.0.8
======
v3.0.7 v3.0.7
====== ======

View File

@@ -49,9 +49,9 @@ A requirement file is provided to install:
<!--start roles_paths --> <!--start roles_paths -->
### Included roles ### Included roles
* `keycloak_quarkus`: role for installing keycloak (>= 19.0.0, quarkus based). * [`keycloak_quarkus`](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak_quarkus/README.md): role for installing keycloak (>= 19.0.0, quarkus based).
* `keycloak_realm`: role for configuring a realm, user federation(s), clients and users, in an installed service. * [`keycloak_realm`](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak_realm/README.md): role for configuring a realm, user federation(s), clients and users, in an installed service.
* `keycloak`: role for installing legacy keycloak (<= 19.0, wildfly based). * [`keycloak`](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak/README.md): role for installing legacy keycloak (<= 19.0, wildfly based).
<!--end roles_paths --> <!--end roles_paths -->
@@ -71,7 +71,7 @@ All Keycloak administration modules from `community.general` are provided in thi
* `keycloak_client_rolemapping`: manage client role mappings for users and groups. * `keycloak_client_rolemapping`: manage client role mappings for users and groups.
* `keycloak_client_rolescope`: manage client role scope mappings. * `keycloak_client_rolescope`: manage client role scope mappings.
* `keycloak_client_scope`: manage client scopes and protocol mappers (replaces `community.general.keycloak_clientscope`). * `keycloak_client_scope`: manage client scopes and protocol mappers (replaces `community.general.keycloak_clientscope`).
* `keycloak_clientscope_type`: manage default and optional client scope assignments. * `keycloak_client_scope_type`: manage default and optional client scope assignments.
* `keycloak_clientsecret_info`: retrieve client secret information. * `keycloak_clientsecret_info`: retrieve client secret information.
* `keycloak_clientsecret_regenerate`: regenerate a client secret. * `keycloak_clientsecret_regenerate`: regenerate a client secret.
* `keycloak_clienttemplate`: manage legacy client templates. * `keycloak_clienttemplate`: manage legacy client templates.
@@ -220,5 +220,5 @@ For details on changes between versions, please see the [CHANGELOG](https://gith
Apache License v2.0 or later Apache License v2.0 or later
<!--start license --> <!--start license -->
See [LICENSE](LICENSE) to view the full text. See [LICENSE](https://github.com/ansible-middleware/keycloak/blob/main/LICENSE) to view the full text.
<!--end license --> <!--end license -->

View File

@@ -6,4 +6,5 @@ python3-netaddr [platform:rpm platform:dpkg]
python3-lxml [platform:rpm platform:dpkg] python3-lxml [platform:rpm platform:dpkg]
python3-jmespath [platform:rpm platform:dpkg] python3-jmespath [platform:rpm platform:dpkg]
python3-requests [platform:rpm platform:dpkg] python3-requests [platform:rpm platform:dpkg]
podman [platform:rpm platform:dpkg]

View File

@@ -825,3 +825,14 @@ releases:
- 341.yaml - 341.yaml
- 343.yaml - 343.yaml
release_date: '2026-06-01' release_date: '2026-06-01'
3.0.8:
release_date: '2026-06-09'
3.0.9:
changes:
bugfixes:
- 'AMW-551 Providing correct rhbk version `#344 <https://github.com/ansible-middleware/keycloak/pull/344>`_
'
fragments:
- 344.yaml
release_date: '2026-06-11'

View File

@@ -1,7 +1,7 @@
--- ---
namespace: middleware_automation namespace: middleware_automation
name: keycloak name: keycloak
version: "3.0.7" version: "3.0.9"
readme: README.md readme: README.md
authors: authors:
- Romain Pelisse <rpelisse@redhat.com> - Romain Pelisse <rpelisse@redhat.com>

View File

@@ -14,8 +14,8 @@ action_groups:
- keycloak_client_rolemapping - keycloak_client_rolemapping
- keycloak_client_rolescope - keycloak_client_rolescope
- keycloak_client_scope - keycloak_client_scope
- keycloak_clientscope_type - keycloak_client_scope_type
- keycloak_clientscope_rolemappings - keycloak_client_scope_rolemappings
- keycloak_clientsecret_info - keycloak_clientsecret_info
- keycloak_clientsecret_regenerate - keycloak_clientsecret_regenerate
- keycloak_clienttemplate - keycloak_clienttemplate
@@ -44,3 +44,19 @@ plugin_routing:
warning_text: >- warning_text: >-
The module has been renamed to keycloak_client_scope for Keycloak 17+ (Quarkus). The module has been renamed to keycloak_client_scope for Keycloak 17+ (Quarkus).
Update playbooks to use middleware_automation.keycloak.keycloak_client_scope. Update playbooks to use middleware_automation.keycloak.keycloak_client_scope.
keycloak_clientscope_type:
redirect: middleware_automation.keycloak.keycloak_client_scope_type
deprecation:
removal_version: 5.0.0
warning_text: >-
The module has been renamed to keycloak_client_scope_type for Keycloak 17+ (Quarkus).
Update playbooks to use middleware_automation.keycloak.keycloak_client_scope_type.
keycloak_clientscope_rolemappings:
redirect: middleware_automation.keycloak.keycloak_client_scope_rolemappings
deprecation:
removal_version: 5.0.0
warning_text: >-
The module has been renamed to keycloak_client_scope_rolemappings for Keycloak 17+ (Quarkus).
Update playbooks to use middleware_automation.keycloak.keycloak_client_scope_rolemappings.

View File

@@ -1,6 +1,6 @@
--- ---
driver: driver:
name: podman name: docker
platforms: platforms:
- name: instance - name: instance
image: registry.access.redhat.com/ubi9/ubi-init:latest image: registry.access.redhat.com/ubi9/ubi-init:latest

View File

@@ -20,8 +20,50 @@
- name: Download keycloak archive to controller directory - name: Download keycloak archive to controller directory
ansible.builtin.get_url: # noqa risky-file-permissions delegated, uses controller host user ansible.builtin.get_url: # noqa risky-file-permissions delegated, uses controller host user
url: https://github.com/keycloak/keycloak/releases/download/26.4.7/keycloak-26.4.7.zip url: https://github.com/keycloak/keycloak/releases/download/26.6.2/keycloak-26.6.2.zip
dest: /tmp/keycloak dest: /tmp/keycloak
mode: '0640' mode: '0640'
delegate_to: localhost delegate_to: localhost
run_once: true run_once: true
ignore_errors: true
- name: Attempt RHBK download using redhat.runtimes_common collection
when:
- rhn_username is defined
- rhn_username | length > 0
block:
- name: Retrieve RHBK product download using Unified Downloads API
middleware_automation.common.product_search:
client_id: "{{ rhn_username }}"
client_secret: "{{ rhn_password }}"
product_type: DISTRIBUTION
product_version: "{{ keycloak_quarkus_version | default('26.6.2') }}"
product_category: "RHBK"
register: rhn_products
no_log: "{{ omit_rhn_output | default(true) }}"
delegate_to: localhost
run_once: true
ignore_errors: true
- name: Determine install zipfile from search results
ansible.builtin.set_fact:
rhn_matched_products: "{{ rhn_products.results | selectattr('file_name', 'match', '.*keycloak-' + (keycloak_quarkus_version | default('26.6.2')) + '.zip$') }}"
delegate_to: localhost
run_once: true
when:
- rhn_products is defined
- rhn_products.results is defined
- name: Download Red Hat Build of Keycloak
middleware_automation.common.product_download:
client_id: "{{ rhn_username }}"
client_secret: "{{ rhn_password }}"
product_id: "{{ (rhn_matched_products | first).id }}"
dest: "/tmp/keycloak/keycloak-{{ keycloak_quarkus_version | default('26.6.2') }}.zip"
no_log: "{{ omit_rhn_output | default(true) }}"
delegate_to: localhost
run_once: true
when:
- rhn_matched_products is defined
- rhn_matched_products | length > 0
ignore_errors: true

View File

@@ -1,6 +1,6 @@
--- ---
driver: driver:
name: podman name: docker
platforms: platforms:
- name: instance - name: instance
image: registry.access.redhat.com/ubi9/ubi-init:latest image: registry.access.redhat.com/ubi9/ubi-init:latest

View File

@@ -37,8 +37,8 @@
- keycloak_client_rolemapping - keycloak_client_rolemapping
- keycloak_client_rolescope - keycloak_client_rolescope
- keycloak_client_scope - keycloak_client_scope
- keycloak_clientscope_type - keycloak_client_scope_type
- keycloak_clientscope_rolemappings - keycloak_client_scope_rolemappings
- keycloak_clientsecret_info - keycloak_clientsecret_info
- keycloak_clientsecret_regenerate - keycloak_clientsecret_regenerate
- keycloak_clienttemplate - keycloak_clienttemplate
@@ -265,10 +265,10 @@
- "'404' not in (clienttemplate_result.msg | default(''))" - "'404' not in (clienttemplate_result.msg | default(''))"
- "'Not Found' not in (clienttemplate_result.msg | default(''))" - "'Not Found' not in (clienttemplate_result.msg | default(''))"
- name: keycloak_clientscope_type — attach scope as optional on realm - name: keycloak_client_scope_type — attach scope as optional on realm
middleware_automation.keycloak.keycloak_clientscope_type: middleware_automation.keycloak.keycloak_client_scope_type:
realm: "{{ target_realm }}" realm: "{{ target_realm }}"
optional_clientscopes: optional_client_scopes:
- "{{ scope }}" - "{{ scope }}"
- name: keycloak_user_rolemapping — assign realm role to user - name: keycloak_user_rolemapping — assign realm role to user
@@ -299,54 +299,54 @@
- name: keycloak_client_rolescope — restrict realm role on client - name: keycloak_client_rolescope — restrict realm role on client
middleware_automation.keycloak.keycloak_client_rolescope: middleware_automation.keycloak.keycloak_client_rolescope:
realm: "{{ target_realm }}" realm: "{{ target_realm }}"
client_id: "{{ client }}" target_client_id: "{{ client }}"
role_names: role_names:
- "{{ role }}" - "{{ role }}"
state: present state: present
- name: keycloak_clientscope_rolemappings — map client roles to clientscope - name: keycloak_client_scope_rolemappings — map client roles to client scope
middleware_automation.keycloak.keycloak_clientscope_rolemappings: middleware_automation.keycloak.keycloak_client_scope_rolemappings:
realm: "{{ target_realm }}" realm: "{{ target_realm }}"
client_id: "{{ client }}" client_id: "{{ client }}"
clientscope_id: "{{ scope }}" client_scope_id: "{{ scope }}"
role_names: role_names:
- "{{ client_role }}" - "{{ client_role }}"
register: clientscope_rolemappings_result register: client_scope_rolemappings_result
- name: Assert clientscope role mappings were created - name: Assert client scope role mappings were created
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- clientscope_rolemappings_result is changed - client_scope_rolemappings_result is changed
- clientscope_rolemappings_result.end_state | length == 1 - client_scope_rolemappings_result.end_state | length == 1
- name: keycloak_clientscope_rolemappings — remap client role (idempotency) - name: keycloak_client_scope_rolemappings — remap client role (idempotency)
middleware_automation.keycloak.keycloak_clientscope_rolemappings: middleware_automation.keycloak.keycloak_client_scope_rolemappings:
realm: "{{ target_realm }}" realm: "{{ target_realm }}"
client_id: "{{ client }}" client_id: "{{ client }}"
clientscope_id: "{{ scope }}" client_scope_id: "{{ scope }}"
role_names: role_names:
- "{{ client_role }}" - "{{ client_role }}"
register: clientscope_rolemappings_idempotent_result register: client_scope_rolemappings_idempotent_result
- name: Assert clientscope role mappings are idempotent - name: Assert client scope role mappings are idempotent
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- clientscope_rolemappings_idempotent_result is not changed - client_scope_rolemappings_idempotent_result is not changed
- clientscope_rolemappings_idempotent_result.end_state | length == 1 - client_scope_rolemappings_idempotent_result.end_state | length == 1
- name: keycloak_clientscope_rolemappings — map realm role to clientscope - name: keycloak_client_scope_rolemappings — map realm role to client scope
middleware_automation.keycloak.keycloak_clientscope_rolemappings: middleware_automation.keycloak.keycloak_client_scope_rolemappings:
realm: "{{ target_realm }}" realm: "{{ target_realm }}"
clientscope_id: "{{ scope }}" client_scope_id: "{{ scope }}"
role_names: role_names:
- "{{ role }}" - "{{ role }}"
register: clientscope_realm_rolemappings_result register: client_scope_realm_rolemappings_result
- name: Assert realm role was mapped to clientscope - name: Assert realm role was mapped to client_scope
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- clientscope_realm_rolemappings_result is changed - client_scope_realm_rolemappings_result is changed
- clientscope_realm_rolemappings_result.end_state | length == 1 - client_scope_realm_rolemappings_result.end_state | length == 1
- name: keycloak_user — set email_verified explicitly - name: keycloak_user — set email_verified explicitly
middleware_automation.keycloak.keycloak_user: middleware_automation.keycloak.keycloak_user:
@@ -517,19 +517,19 @@
name: "{{ authz_scope }}" name: "{{ authz_scope }}"
state: absent state: absent
- name: keycloak_clientscope_rolemappings — remove realm role from clientscope - name: keycloak_client_scope_rolemappings — remove realm role from client scope
middleware_automation.keycloak.keycloak_clientscope_rolemappings: middleware_automation.keycloak.keycloak_client_scope_rolemappings:
realm: "{{ target_realm }}" realm: "{{ target_realm }}"
clientscope_id: "{{ scope }}" client_scope_id: "{{ scope }}"
role_names: role_names:
- "{{ role }}" - "{{ role }}"
state: absent state: absent
- name: keycloak_clientscope_rolemappings — remove client role from clientscope - name: keycloak_client_scope_rolemappings — remove client role from client scope
middleware_automation.keycloak.keycloak_clientscope_rolemappings: middleware_automation.keycloak.keycloak_client_scope_rolemappings:
realm: "{{ target_realm }}" realm: "{{ target_realm }}"
client_id: "{{ client }}" client_id: "{{ client }}"
clientscope_id: "{{ scope }}" client_scope_id: "{{ scope }}"
role_names: role_names:
- "{{ client_role }}" - "{{ client_role }}"
state: absent state: absent
@@ -537,7 +537,7 @@
- name: keycloak_client_rolescope — remove role scope mapping - name: keycloak_client_rolescope — remove role scope mapping
middleware_automation.keycloak.keycloak_client_rolescope: middleware_automation.keycloak.keycloak_client_rolescope:
realm: "{{ target_realm }}" realm: "{{ target_realm }}"
client_id: "{{ client }}" target_client_id: "{{ client }}"
role_names: role_names:
- "{{ role }}" - "{{ role }}"
state: absent state: absent

View File

@@ -4,7 +4,11 @@
vars_files: vars_files:
- ../group_vars/all/vars.yml - ../group_vars/all/vars.yml
vars: vars:
rhn_username: "{{ lookup('env', 'rhn_username') | default('4278e994-7f90-46eb-b99c-90f2815b845f', true) }}"
rhn_password: "{{ lookup('env', 'rhn_password') | default('AHOLJo08ursGdWVm0F66iDR5Owk0CwpL', true) }}"
keycloak_quarkus_bootstrap_admin_password: "remembertochangeme"
keycloak_admin_password: "remembertochangeme" keycloak_admin_password: "remembertochangeme"
keycloak_quarkus_hostname: "http://instance:8080"
keycloak_config_override_template: custom.xml.j2 keycloak_config_override_template: custom.xml.j2
keycloak_http_port: 8081 keycloak_http_port: 8081
keycloak_management_http_port: 19990 keycloak_management_http_port: 19990

View File

@@ -25,7 +25,7 @@
keycloak_quarkus_systemd_wait_for_delay: 2 keycloak_quarkus_systemd_wait_for_delay: 2
keycloak_quarkus_systemd_wait_for_log: true keycloak_quarkus_systemd_wait_for_log: true
keycloak_quarkus_restart_health_check: false # would fail because of self-signed cert keycloak_quarkus_restart_health_check: false # would fail because of self-signed cert
keycloak_quarkus_version: 26.4.7 keycloak_quarkus_version: 26.6.2
keycloak_quarkus_java_heap_opts: "-Xms1024m -Xmx1024m" keycloak_quarkus_java_heap_opts: "-Xms1024m -Xmx1024m"
keycloak_quarkus_additional_env_vars: keycloak_quarkus_additional_env_vars:
- key: KC_FEATURES_DISABLED - key: KC_FEATURES_DISABLED
@@ -39,16 +39,16 @@
- key: default-connection-pool-size - key: default-connection-pool-size
value: 10 value: 10
- id: spid-saml - id: spid-saml
url: https://github.com/italia/spid-keycloak-provider/releases/download/24.0.2/spid-provider.jar url: https://github.com/italia/spid-keycloak-provider/releases/download/26.5.6/spid-provider.jar
- id: spid-saml-w-checksum - id: spid-saml-w-checksum
url: https://github.com/italia/spid-keycloak-provider/releases/download/24.0.2/spid-provider.jar url: https://github.com/italia/spid-keycloak-provider/releases/download/26.5.6/spid-provider.jar
checksum: sha256:fbb50e73739d7a6d35b5bff611b1c01668b29adf6f6259624b95e466a305f377 checksum: sha256:2ddafc389a5f017d8665bfdfa2f72b3784fc74b9f3a482e796fa89a5ba5cc95b
- id: keycloak-kerberos-federation - id: keycloak-kerberos-federation
maven: maven:
repository_url: https://repo1.maven.org/maven2/ # https://mvnrepository.com/artifact/org.keycloak/keycloak-kerberos-federation/24.0.4 repository_url: https://repo1.maven.org/maven2/ # https://mvnrepository.com/artifact/org.keycloak/keycloak-kerberos-federation/24.0.4
group_id: org.keycloak group_id: org.keycloak
artifact_id: keycloak-kerberos-federation artifact_id: keycloak-kerberos-federation
version: 26.4.7 # optional version: 26.6.3 # optional
# username: myUser # optional # username: myUser # optional
# password: myPAT # optional # password: myPAT # optional
# - id: my-static-theme # - id: my-static-theme

View File

@@ -1,6 +1,6 @@
--- ---
driver: driver:
name: podman name: docker
platforms: platforms:
- name: instance - name: instance
image: registry.access.redhat.com/ubi9/ubi-init:latest image: registry.access.redhat.com/ubi9/ubi-init:latest

View File

@@ -9,6 +9,6 @@
keycloak_quarkus_additional_env_vars: keycloak_quarkus_additional_env_vars:
- key: KC_FEATURES_DISABLED - key: KC_FEATURES_DISABLED
value: ciba,device-flow,impersonation,kerberos,docker value: ciba,device-flow,impersonation,kerberos,docker
keycloak_quarkus_version: 26.0.7 keycloak_quarkus_version: 26.6.2
roles: roles:
- role: keycloak_quarkus - role: keycloak_quarkus

View File

@@ -4,7 +4,7 @@ dependency:
options: options:
requirements-file: molecule/requirements.yml requirements-file: molecule/requirements.yml
driver: driver:
name: podman name: docker
platforms: platforms:
- name: instance - name: instance
image: registry.access.redhat.com/ubi9/ubi-init:latest image: registry.access.redhat.com/ubi9/ubi-init:latest

View File

@@ -6,7 +6,7 @@
- vars.yml - vars.yml
vars: vars:
sudo_pkg_name: sudo sudo_pkg_name: sudo
keycloak_quarkus_version: 26.0.4 keycloak_quarkus_version: 26.6.1
keycloak_quarkus_additional_env_vars: keycloak_quarkus_additional_env_vars:
- key: KC_FEATURES_DISABLED - key: KC_FEATURES_DISABLED
value: impersonation,kerberos value: impersonation,kerberos

View File

@@ -57,23 +57,23 @@ URL_GROUPS = "{url}/admin/realms/{realm}/groups"
URL_GROUP = "{url}/admin/realms/{realm}/groups/{groupid}" URL_GROUP = "{url}/admin/realms/{realm}/groups/{groupid}"
URL_GROUP_CHILDREN = "{url}/admin/realms/{realm}/groups/{groupid}/children" URL_GROUP_CHILDREN = "{url}/admin/realms/{realm}/groups/{groupid}/children"
URL_CLIENTSCOPES = "{url}/admin/realms/{realm}/client-scopes" URL_CLIENT_SCOPES = "{url}/admin/realms/{realm}/client-scopes"
URL_CLIENTSCOPE = "{url}/admin/realms/{realm}/client-scopes/{id}" URL_CLIENT_SCOPE = "{url}/admin/realms/{realm}/client-scopes/{id}"
URL_CLIENTSCOPE_SCOPE_MAPPINGS = "{url}/admin/realms/{realm}/client-scopes/{id}/scope-mappings" URL_CLIENT_SCOPE_SCOPE_MAPPINGS = "{url}/admin/realms/{realm}/client-scopes/{id}/scope-mappings"
URL_CLIENTSCOPE_SCOPE_MAPPINGS_REALM = "{url}/admin/realms/{realm}/client-scopes/{id}/scope-mappings/realm" URL_CLIENT_SCOPE_SCOPE_MAPPINGS_REALM = "{url}/admin/realms/{realm}/client-scopes/{id}/scope-mappings/realm"
URL_CLIENTSCOPE_SCOPE_MAPPINGS_CLIENT = "{url}/admin/realms/{realm}/client-scopes/{id}/scope-mappings/clients/{client}" URL_CLIENT_SCOPE_SCOPE_MAPPINGS_CLIENT = "{url}/admin/realms/{realm}/client-scopes/{id}/scope-mappings/clients/{client}"
URL_CLIENTSCOPE_PROTOCOLMAPPERS = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models" URL_CLIENT_SCOPE_PROTOCOLMAPPERS = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models"
URL_CLIENTSCOPE_PROTOCOLMAPPER = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models/{mapper_id}" URL_CLIENT_SCOPE_PROTOCOLMAPPER = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models/{mapper_id}"
URL_DEFAULT_CLIENTSCOPES = "{url}/admin/realms/{realm}/default-default-client-scopes" URL_DEFAULT_CLIENT_SCOPES = "{url}/admin/realms/{realm}/default-default-client-scopes"
URL_DEFAULT_CLIENTSCOPE = "{url}/admin/realms/{realm}/default-default-client-scopes/{id}" URL_DEFAULT_CLIENT_SCOPE = "{url}/admin/realms/{realm}/default-default-client-scopes/{id}"
URL_OPTIONAL_CLIENTSCOPES = "{url}/admin/realms/{realm}/default-optional-client-scopes" URL_OPTIONAL_CLIENT_SCOPES = "{url}/admin/realms/{realm}/default-optional-client-scopes"
URL_OPTIONAL_CLIENTSCOPE = "{url}/admin/realms/{realm}/default-optional-client-scopes/{id}" URL_OPTIONAL_CLIENT_SCOPE = "{url}/admin/realms/{realm}/default-optional-client-scopes/{id}"
URL_CLIENT_DEFAULT_CLIENTSCOPES = "{url}/admin/realms/{realm}/clients/{cid}/default-client-scopes" URL_CLIENT_DEFAULT_CLIENT_SCOPES = "{url}/admin/realms/{realm}/clients/{cid}/default-client-scopes"
URL_CLIENT_DEFAULT_CLIENTSCOPE = "{url}/admin/realms/{realm}/clients/{cid}/default-client-scopes/{id}" URL_CLIENT_DEFAULT_CLIENT_SCOPE = "{url}/admin/realms/{realm}/clients/{cid}/default-client-scopes/{id}"
URL_CLIENT_OPTIONAL_CLIENTSCOPES = "{url}/admin/realms/{realm}/clients/{cid}/optional-client-scopes" URL_CLIENT_OPTIONAL_CLIENT_SCOPES = "{url}/admin/realms/{realm}/clients/{cid}/optional-client-scopes"
URL_CLIENT_OPTIONAL_CLIENTSCOPE = "{url}/admin/realms/{realm}/clients/{cid}/optional-client-scopes/{id}" URL_CLIENT_OPTIONAL_CLIENT_SCOPE = "{url}/admin/realms/{realm}/clients/{cid}/optional-client-scopes/{id}"
URL_CLIENT_GROUP_ROLEMAPPINGS = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}" URL_CLIENT_GROUP_ROLEMAPPINGS = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}"
URL_CLIENT_GROUP_ROLEMAPPINGS_AVAILABLE = ( URL_CLIENT_GROUP_ROLEMAPPINGS_AVAILABLE = (
@@ -154,18 +154,6 @@ URL_AUTHZ_CUSTOM_POLICY = "{url}/admin/realms/{realm}/clients/{client_id}/authz/
URL_AUTHZ_CUSTOM_POLICIES = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/policy" URL_AUTHZ_CUSTOM_POLICIES = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/policy"
def normalize_keycloak_url(url: str) -> str:
"""Normalize Keycloak base URL for Admin REST API access.
Keycloak 17+ (Quarkus) exposes the API at the server root without an /auth prefix.
WildFly-based Keycloak used /auth as the context path. Trailing slashes are removed.
"""
url = url.rstrip("/")
if url.endswith("/auth"):
return url[:-5]
return url
def keycloak_argument_spec() -> dict[str, t.Any]: def keycloak_argument_spec() -> dict[str, t.Any]:
""" """
Returns argument_spec of options common to keycloak_*-modules Returns argument_spec of options common to keycloak_*-modules
@@ -215,7 +203,7 @@ def _token_request(module_params: dict[str, t.Any], payload: dict[str, t.Any]) -
'refresh_token' for type 'refresh_token'. 'refresh_token' for type 'refresh_token'.
:return: access token :return: access token
""" """
base_url = normalize_keycloak_url(module_params["auth_keycloak_url"]) base_url = module_params["auth_keycloak_url"]
if not base_url.lower().startswith(("http", "https")): if not base_url.lower().startswith(("http", "https")):
raise KeycloakError(f"auth_url '{base_url}' should either start with 'http' or 'https'.") raise KeycloakError(f"auth_url '{base_url}' should either start with 'http' or 'https'.")
auth_realm = module_params.get("auth_realm") auth_realm = module_params.get("auth_realm")
@@ -403,7 +391,7 @@ class KeycloakAPI:
def __init__(self, module: AnsibleModule, connection_header: dict[str, str]) -> None: def __init__(self, module: AnsibleModule, connection_header: dict[str, str]) -> None:
self.module = module self.module = module
self.baseurl = normalize_keycloak_url(self.module.params.get("auth_keycloak_url")) self.baseurl = self.module.params.get("auth_keycloak_url")
self.validate_certs = self.module.params.get("validate_certs") self.validate_certs = self.module.params.get("validate_certs")
self.connection_timeout = self.module.params.get("connection_timeout") self.connection_timeout = self.module.params.get("connection_timeout")
self.restheaders = connection_header self.restheaders = connection_header
@@ -713,7 +701,7 @@ class KeycloakAPI:
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not obtain list of clients for realm {realm}: {e}") self.fail_request(e, msg=f"Could not obtain list of clients for realm {realm}: {e}")
def get_client_by_clientid(self, client_id, realm: str = "master"): def get_client_by_client_id(self, client_id, realm: str = "master"):
"""Get client representation by clientId """Get client representation by clientId
:param client_id: The clientId to be queried :param client_id: The clientId to be queried
:param realm: realm from which to obtain the client representation :param realm: realm from which to obtain the client representation
@@ -756,7 +744,7 @@ class KeycloakAPI:
:param realm: client template from this realm :param realm: client template from this realm
:return: id of client (usually a UUID) :return: id of client (usually a UUID)
""" """
result = self.get_client_by_clientid(client_id, realm) result = self.get_client_by_client_id(client_id, realm)
if isinstance(result, dict) and "id" in result: if isinstance(result, dict) and "id" in result:
return result["id"] return result["id"]
else: else:
@@ -1301,99 +1289,99 @@ class KeycloakAPI:
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not delete client template {id} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not delete client template {id} in realm {realm}: {e}")
def get_clientscopes(self, realm: str = "master"): def get_client_scopes(self, realm: str = "master"):
"""Fetch the name and ID of all clientscopes on the Keycloak server. """Fetch the name and ID of all client scopes on the Keycloak server.
To fetch the full data of the group, make a subsequent call to To fetch the full data of the group, make a subsequent call to
get_clientscope_by_clientscopeid, passing in the ID of the group you wish to return. get_client_scope_by_client_scope_id, passing in the ID of the group you wish to return.
:param realm: Realm in which the clientscope resides; default 'master'. :param realm: Realm in which the client scope resides; default 'master'.
:return The clientscopes of this realm (default "master") :return The client scopes of this realm (default "master")
""" """
clientscopes_url = URL_CLIENTSCOPES.format(url=self.baseurl, realm=realm) client_scopes_url = URL_CLIENT_SCOPES.format(url=self.baseurl, realm=realm)
try: try:
return self._request_and_deserialize(clientscopes_url, method="GET") return self._request_and_deserialize(client_scopes_url, method="GET")
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not fetch list of clientscopes in realm {realm}: {e}") self.fail_request(e, msg=f"Could not fetch list of client scopes in realm {realm}: {e}")
def get_clientscope_by_clientscopeid(self, cid, realm: str = "master"): def get_client_scope_by_client_scope_id(self, cid, realm: str = "master"):
"""Fetch a keycloak clientscope from the provided realm using the clientscope's unique ID. """Fetch a keycloak client scope from the provided realm using the client scope's unique ID.
If the clientscope does not exist, None is returned. If the client scope does not exist, None is returned.
gid is a UUID provided by the Keycloak API gid is a UUID provided by the Keycloak API
:param cid: UUID of the clientscope to be returned :param cid: UUID of the client scope to be returned
:param realm: Realm in which the clientscope resides; default 'master'. :param realm: Realm in which the client scope resides; default 'master'.
""" """
clientscope_url = URL_CLIENTSCOPE.format(url=self.baseurl, realm=realm, id=cid) client_scope_url = URL_CLIENT_SCOPE.format(url=self.baseurl, realm=realm, id=cid)
try: try:
return self._request_and_deserialize(clientscope_url, method="GET") return self._request_and_deserialize(client_scope_url, method="GET")
except HTTPError as e: except HTTPError as e:
if e.code == HTTPStatus.NOT_FOUND: if e.code == HTTPStatus.NOT_FOUND:
return None return None
else: else:
self.fail_request(e, msg=f"Could not fetch clientscope {cid} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not fetch client scope {cid} in realm {realm}: {e}")
except Exception as e: except Exception as e:
self.module.fail_json(msg=f"Could not clientscope group {cid} in realm {realm}: {e}") self.module.fail_json(msg=f"Could not client scope group {cid} in realm {realm}: {e}")
def get_clientscope_by_name(self, name, realm: str = "master"): def get_client_scope_by_name(self, name, realm: str = "master"):
"""Fetch a keycloak clientscope within a realm based on its name. """Fetch a keycloak client scope within a realm based on its name.
The Keycloak API does not allow filtering of the clientscopes resource by name. The Keycloak API does not allow filtering of the client scopes resource by name.
As a result, this method first retrieves the entire list of clientscopes - name and ID - As a result, this method first retrieves the entire list of client scopes - name and ID -
then performs a second query to fetch the group. then performs a second query to fetch the group.
If the clientscope does not exist, None is returned. If the client scope does not exist, None is returned.
:param name: Name of the clientscope to fetch. :param name: Name of the client scope to fetch.
:param realm: Realm in which the clientscope resides; default 'master' :param realm: Realm in which the client scope resides; default 'master'
""" """
try: try:
all_clientscopes = self.get_clientscopes(realm=realm) all_client_scopes = self.get_client_scopes(realm=realm)
for clientscope in all_clientscopes: for client_scope in all_client_scopes:
if clientscope["name"] == name: if client_scope["name"] == name:
return self.get_clientscope_by_clientscopeid(clientscope["id"], realm=realm) return self.get_client_scope_by_client_scope_id(client_scope["id"], realm=realm)
return None return None
except Exception as e: except Exception as e:
self.module.fail_json(msg=f"Could not fetch clientscope {name} in realm {realm}: {e}") self.module.fail_json(msg=f"Could not fetch client scope {name} in realm {realm}: {e}")
def create_clientscope(self, clientscoperep, realm: str = "master"): def create_client_scope(self, client_scope_rep, realm: str = "master"):
"""Create a Keycloak clientscope. """Create a Keycloak client scope.
:param clientscoperep: a ClientScopeRepresentation of the clientscope to be created. Must contain at minimum the field name. :param client_scope_rep: a ClientScopeRepresentation of the client scope to be created. Must contain at minimum the field name.
:return: HTTPResponse object on success :return: HTTPResponse object on success
""" """
clientscopes_url = URL_CLIENTSCOPES.format(url=self.baseurl, realm=realm) client_scopes_url = URL_CLIENT_SCOPES.format(url=self.baseurl, realm=realm)
try: try:
return self._request(clientscopes_url, method="POST", data=json.dumps(clientscoperep)) return self._request(client_scopes_url, method="POST", data=json.dumps(client_scope_rep))
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not create clientscope {clientscoperep['name']} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not create client scope {client_scope_rep['name']} in realm {realm}: {e}")
def update_clientscope(self, clientscoperep, realm: str = "master"): def update_client_scope(self, client_scope_rep, realm: str = "master"):
"""Update an existing clientscope. """Update an existing client scope.
:param grouprep: A GroupRepresentation of the updated group. :param grouprep: A GroupRepresentation of the updated group.
:return HTTPResponse object on success :return HTTPResponse object on success
""" """
clientscope_url = URL_CLIENTSCOPE.format(url=self.baseurl, realm=realm, id=clientscoperep["id"]) client_scope_url = URL_CLIENT_SCOPE.format(url=self.baseurl, realm=realm, id=client_scope_rep["id"])
try: try:
return self._request(clientscope_url, method="PUT", data=json.dumps(clientscoperep)) return self._request(client_scope_url, method="PUT", data=json.dumps(client_scope_rep))
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not update clientscope {clientscoperep['name']} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not update client scope {client_scope_rep['name']} in realm {realm}: {e}")
def delete_clientscope(self, name=None, cid=None, realm: str = "master"): def delete_client_scope(self, name=None, cid=None, realm: str = "master"):
"""Delete a clientscope. One of name or cid must be provided. """Delete a client scope. One of name or cid must be provided.
Providing the clientscope ID is preferred as it avoids a second lookup to Providing the client scope ID is preferred as it avoids a second lookup to
convert a clientscope name to an ID. convert a client scope name to an ID.
:param name: The name of the clientscope. A lookup will be performed to retrieve the clientscope ID. :param name: The name of the client scope. A lookup will be performed to retrieve the client scope ID.
:param cid: The ID of the clientscope (preferred to name). :param cid: The ID of the client scope (preferred to name).
:param realm: The realm in which this group resides, default "master". :param realm: The realm in which this group resides, default "master".
""" """
@@ -1405,9 +1393,9 @@ class KeycloakAPI:
# in the case that both are provided, prefer the ID, since it is one # in the case that both are provided, prefer the ID, since it is one
# less lookup. # less lookup.
if cid is None and name is not None: if cid is None and name is not None:
for clientscope in self.get_clientscopes(realm=realm): for client_scope in self.get_client_scopes(realm=realm):
if clientscope["name"] == name: if client_scope["name"] == name:
cid = clientscope["id"] cid = client_scope["id"]
break break
# if the group doesn't exist - no problem, nothing to delete. # if the group doesn't exist - no problem, nothing to delete.
@@ -1415,41 +1403,41 @@ class KeycloakAPI:
return None return None
# should have a good cid by here. # should have a good cid by here.
clientscope_url = URL_CLIENTSCOPE.format(realm=realm, id=cid, url=self.baseurl) client_scope_url = URL_CLIENT_SCOPE.format(realm=realm, id=cid, url=self.baseurl)
try: try:
return self._request(clientscope_url, method="DELETE") return self._request(client_scope_url, method="DELETE")
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Unable to delete clientscope {cid}: {e}") self.fail_request(e, msg=f"Unable to delete client scope {cid}: {e}")
def get_clientscope_protocolmappers(self, cid, realm: str = "master"): def get_client_scope_protocolmappers(self, cid, realm: str = "master"):
"""Fetch the name and ID of all clientscopes on the Keycloak server. """Fetch the name and ID of all client scopes on the Keycloak server.
To fetch the full data of the group, make a subsequent call to To fetch the full data of the group, make a subsequent call to
get_clientscope_by_clientscopeid, passing in the ID of the group you wish to return. get_client_scope_by_client_scope_id, passing in the ID of the group you wish to return.
:param cid: id of clientscope (not name). :param cid: id of client scope (not name).
:param realm: Realm in which the clientscope resides; default 'master'. :param realm: Realm in which the client scope resides; default 'master'.
:return The protocolmappers of this realm (default "master") :return The protocolmappers of this realm (default "master")
""" """
protocolmappers_url = URL_CLIENTSCOPE_PROTOCOLMAPPERS.format(id=cid, url=self.baseurl, realm=realm) protocolmappers_url = URL_CLIENT_SCOPE_PROTOCOLMAPPERS.format(id=cid, url=self.baseurl, realm=realm)
try: try:
return self._request_and_deserialize(protocolmappers_url, method="GET") return self._request_and_deserialize(protocolmappers_url, method="GET")
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not fetch list of protocolmappers in realm {realm}: {e}") self.fail_request(e, msg=f"Could not fetch list of protocolmappers in realm {realm}: {e}")
def get_clientscope_protocolmapper_by_protocolmapperid(self, pid, cid, realm: str = "master"): def get_client_scope_protocolmapper_by_protocolmapperid(self, pid, cid, realm: str = "master"):
"""Fetch a keycloak clientscope from the provided realm using the clientscope's unique ID. """Fetch a keycloak client scope from the provided realm using the client scope's unique ID.
If the clientscope does not exist, None is returned. If the client scope does not exist, None is returned.
gid is a UUID provided by the Keycloak API gid is a UUID provided by the Keycloak API
:param cid: UUID of the protocolmapper to be returned :param cid: UUID of the protocolmapper to be returned
:param cid: UUID of the clientscope to be returned :param cid: UUID of the client scope to be returned
:param realm: Realm in which the clientscope resides; default 'master'. :param realm: Realm in which the client scope resides; default 'master'.
""" """
protocolmapper_url = URL_CLIENTSCOPE_PROTOCOLMAPPER.format(url=self.baseurl, realm=realm, id=cid, mapper_id=pid) protocolmapper_url = URL_CLIENT_SCOPE_PROTOCOLMAPPER.format(url=self.baseurl, realm=realm, id=cid, mapper_id=pid)
try: try:
return self._request_and_deserialize(protocolmapper_url, method="GET") return self._request_and_deserialize(protocolmapper_url, method="GET")
@@ -1461,24 +1449,24 @@ class KeycloakAPI:
except Exception as e: except Exception as e:
self.module.fail_json(msg=f"Could not fetch protocolmapper {cid} in realm {realm}: {e}") self.module.fail_json(msg=f"Could not fetch protocolmapper {cid} in realm {realm}: {e}")
def get_clientscope_protocolmapper_by_name(self, cid, name, realm: str = "master"): def get_client_scope_protocolmapper_by_name(self, cid, name, realm: str = "master"):
"""Fetch a keycloak clientscope within a realm based on its name. """Fetch a keycloak client scope within a realm based on its name.
The Keycloak API does not allow filtering of the clientscopes resource by name. The Keycloak API does not allow filtering of the client scopes resource by name.
As a result, this method first retrieves the entire list of clientscopes - name and ID - As a result, this method first retrieves the entire list of client scopes - name and ID -
then performs a second query to fetch the group. then performs a second query to fetch the group.
If the clientscope does not exist, None is returned. If the client scope does not exist, None is returned.
:param cid: Id of the clientscope (not name). :param cid: Id of the client scope (not name).
:param name: Name of the protocolmapper to fetch. :param name: Name of the protocolmapper to fetch.
:param realm: Realm in which the clientscope resides; default 'master' :param realm: Realm in which the client scope resides; default 'master'
""" """
try: try:
all_protocolmappers = self.get_clientscope_protocolmappers(cid, realm=realm) all_protocolmappers = self.get_client_scope_protocolmappers(cid, realm=realm)
for protocolmapper in all_protocolmappers: for protocolmapper in all_protocolmappers:
if protocolmapper["name"] == name: if protocolmapper["name"] == name:
return self.get_clientscope_protocolmapper_by_protocolmapperid( return self.get_client_scope_protocolmapper_by_protocolmapperid(
protocolmapper["id"], cid, realm=realm protocolmapper["id"], cid, realm=realm
) )
@@ -1487,27 +1475,27 @@ class KeycloakAPI:
except Exception as e: except Exception as e:
self.module.fail_json(msg=f"Could not fetch protocolmapper {name} in realm {realm}: {e}") self.module.fail_json(msg=f"Could not fetch protocolmapper {name} in realm {realm}: {e}")
def create_clientscope_protocolmapper(self, cid, mapper_rep, realm: str = "master"): def create_client_scope_protocolmapper(self, cid, mapper_rep, realm: str = "master"):
"""Create a Keycloak clientscope protocolmapper. """Create a Keycloak client scope protocolmapper.
:param cid: Id of the clientscope. :param cid: Id of the client scope.
:param mapper_rep: a ProtocolMapperRepresentation of the protocolmapper to be created. Must contain at minimum the field name. :param mapper_rep: a ProtocolMapperRepresentation of the protocolmapper to be created. Must contain at minimum the field name.
:return: HTTPResponse object on success :return: HTTPResponse object on success
""" """
protocolmappers_url = URL_CLIENTSCOPE_PROTOCOLMAPPERS.format(url=self.baseurl, id=cid, realm=realm) protocolmappers_url = URL_CLIENT_SCOPE_PROTOCOLMAPPERS.format(url=self.baseurl, id=cid, realm=realm)
try: try:
return self._request(protocolmappers_url, method="POST", data=json.dumps(mapper_rep)) return self._request(protocolmappers_url, method="POST", data=json.dumps(mapper_rep))
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not create protocolmapper {mapper_rep['name']} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not create protocolmapper {mapper_rep['name']} in realm {realm}: {e}")
def update_clientscope_protocolmappers(self, cid, mapper_rep, realm: str = "master"): def update_client_scope_protocolmappers(self, cid, mapper_rep, realm: str = "master"):
"""Update an existing clientscope. """Update an existing client scope.
:param cid: Id of the clientscope. :param cid: Id of the client scope.
:param mapper_rep: A ProtocolMapperRepresentation of the updated protocolmapper. :param mapper_rep: A ProtocolMapperRepresentation of the updated protocolmapper.
:return HTTPResponse object on success :return HTTPResponse object on success
""" """
protocolmapper_url = URL_CLIENTSCOPE_PROTOCOLMAPPER.format( protocolmapper_url = URL_CLIENT_SCOPE_PROTOCOLMAPPER.format(
url=self.baseurl, realm=realm, id=cid, mapper_id=mapper_rep["id"] url=self.baseurl, realm=realm, id=cid, mapper_id=mapper_rep["id"]
) )
@@ -1516,137 +1504,137 @@ class KeycloakAPI:
except Exception as e: except Exception as e:
self.fail_request( self.fail_request(
e, msg=f"Could not update protocolmappers for clientscope {mapper_rep} in realm {realm}: {e}" e, msg=f"Could not update protocolmappers for client scope {mapper_rep} in realm {realm}: {e}"
) )
def get_default_clientscopes(self, realm, client_id=None): def get_default_client_scopes(self, realm, client_id=None):
"""Fetch the name and ID of all clientscopes on the Keycloak server. """Fetch the name and ID of all client scopes on the Keycloak server.
To fetch the full data of the client scope, make a subsequent call to To fetch the full data of the client scope, make a subsequent call to
get_clientscope_by_clientscopeid, passing in the ID of the client scope you wish to return. get_client_scope_by_client_scope_id, passing in the ID of the client scope you wish to return.
:param realm: Realm in which the clientscope resides. :param realm: Realm in which the client scope resides.
:param client_id: The client in which the clientscope resides. :param client_id: The client in which the client scope resides.
:return The default clientscopes of this realm or client :return The default client scopes of this realm or client
""" """
url = URL_DEFAULT_CLIENTSCOPES if client_id is None else URL_CLIENT_DEFAULT_CLIENTSCOPES url = URL_DEFAULT_CLIENT_SCOPES if client_id is None else URL_CLIENT_DEFAULT_CLIENT_SCOPES
return self._get_clientscopes_of_type(realm, url, "default", client_id) return self._get_client_scopes_of_type(realm, url, "default", client_id)
def get_optional_clientscopes(self, realm, client_id=None): def get_optional_client_scopes(self, realm, client_id=None):
"""Fetch the name and ID of all clientscopes on the Keycloak server. """Fetch the name and ID of all client scopes on the Keycloak server.
To fetch the full data of the client scope, make a subsequent call to To fetch the full data of the client scope, make a subsequent call to
get_clientscope_by_clientscopeid, passing in the ID of the client scope you wish to return. get_client_scope_by_client_scope_id, passing in the ID of the client scope you wish to return.
:param realm: Realm in which the clientscope resides. :param realm: Realm in which the client scope resides.
:param client_id: The client in which the clientscope resides. :param client_id: The client in which the client scope resides.
:return The optional clientscopes of this realm or client :return The optional client scopes of this realm or client
""" """
url = URL_OPTIONAL_CLIENTSCOPES if client_id is None else URL_CLIENT_OPTIONAL_CLIENTSCOPES url = URL_OPTIONAL_CLIENT_SCOPES if client_id is None else URL_CLIENT_OPTIONAL_CLIENT_SCOPES
return self._get_clientscopes_of_type(realm, url, "optional", client_id) return self._get_client_scopes_of_type(realm, url, "optional", client_id)
def _get_clientscopes_of_type(self, realm, url_template, scope_type, client_id=None): def _get_client_scopes_of_type(self, realm, url_template, scope_type, client_id=None):
"""Fetch the name and ID of all clientscopes on the Keycloak server. """Fetch the name and ID of all client scopes on the Keycloak server.
To fetch the full data of the client scope, make a subsequent call to To fetch the full data of the client scope, make a subsequent call to
get_clientscope_by_clientscopeid, passing in the ID of the client scope you wish to return. get_client_scope_by_client_scope_id, passing in the ID of the client scope you wish to return.
:param realm: Realm in which the clientscope resides. :param realm: Realm in which the client scope resides.
:param url_template the template for the right type :param url_template the template for the right type
:param scope_type this can be either optional or default :param scope_type this can be either optional or default
:param client_id: The client in which the clientscope resides. :param client_id: The client in which the client scope resides.
:return The clientscopes of the specified type of this realm :return The client scopes of the specified type of this realm
""" """
if client_id is None: if client_id is None:
clientscopes_url = url_template.format(url=self.baseurl, realm=realm) client_scopes_url = url_template.format(url=self.baseurl, realm=realm)
try: try:
return self._request_and_deserialize(clientscopes_url, method="GET") return self._request_and_deserialize(client_scopes_url, method="GET")
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not fetch list of {scope_type} clientscopes in realm {realm}: {e}") self.fail_request(e, msg=f"Could not fetch list of {scope_type} client scopes in realm {realm}: {e}")
else: else:
cid = self.get_client_id(client_id=client_id, realm=realm) cid = self.get_client_id(client_id=client_id, realm=realm)
clientscopes_url = url_template.format(url=self.baseurl, realm=realm, cid=cid) client_scopes_url = url_template.format(url=self.baseurl, realm=realm, cid=cid)
try: try:
return self._request_and_deserialize(clientscopes_url, method="GET") return self._request_and_deserialize(client_scopes_url, method="GET")
except Exception as e: except Exception as e:
self.fail_request( self.fail_request(
e, e,
msg=f"Could not fetch list of {scope_type} clientscopes in client {client_id}: {clientscopes_url}", msg=f"Could not fetch list of {scope_type} client scopes in client {client_id}: {client_scopes_url}",
) )
def _decide_url_type_clientscope(self, client_id=None, scope_type="default"): def _decide_url_type_client_scope(self, client_id=None, scope_type="default"):
"""Decides which url to use. """Decides which url to use.
:param scope_type this can be either optional or default :param scope_type this can be either optional or default
:param client_id: The client in which the clientscope resides. :param client_id: The client in which the client scope resides.
""" """
if client_id is None: if client_id is None:
if scope_type == "default": if scope_type == "default":
return URL_DEFAULT_CLIENTSCOPE return URL_DEFAULT_CLIENT_SCOPE
if scope_type == "optional": if scope_type == "optional":
return URL_OPTIONAL_CLIENTSCOPE return URL_OPTIONAL_CLIENT_SCOPE
else: else:
if scope_type == "default": if scope_type == "default":
return URL_CLIENT_DEFAULT_CLIENTSCOPE return URL_CLIENT_DEFAULT_CLIENT_SCOPE
if scope_type == "optional": if scope_type == "optional":
return URL_CLIENT_OPTIONAL_CLIENTSCOPE return URL_CLIENT_OPTIONAL_CLIENT_SCOPE
def add_default_clientscope(self, id, realm: str = "master", client_id=None): def add_default_client_scope(self, id, realm: str = "master", client_id=None):
"""Add a client scope as default either on realm or client level. """Add a client scope as default either on realm or client level.
:param id: Client scope Id. :param id: Client scope Id.
:param realm: Realm in which the clientscope resides. :param realm: Realm in which the client scope resides.
:param client_id: The client in which the clientscope resides. :param client_id: The client in which the client scope resides.
""" """
self._action_type_clientscope(id, client_id, "default", realm, "add") self._action_type_client_scope(id, client_id, "default", realm, "add")
def add_optional_clientscope(self, id, realm: str = "master", client_id=None): def add_optional_client_scope(self, id, realm: str = "master", client_id=None):
"""Add a client scope as optional either on realm or client level. """Add a client scope as optional either on realm or client level.
:param id: Client scope Id. :param id: Client scope Id.
:param realm: Realm in which the clientscope resides. :param realm: Realm in which the client scope resides.
:param client_id: The client in which the clientscope resides. :param client_id: The client in which the client scope resides.
""" """
self._action_type_clientscope(id, client_id, "optional", realm, "add") self._action_type_client_scope(id, client_id, "optional", realm, "add")
def delete_default_clientscope(self, id, realm: str = "master", client_id=None): def delete_default_client_scope(self, id, realm: str = "master", client_id=None):
"""Remove a client scope as default either on realm or client level. """Remove a client scope as default either on realm or client level.
:param id: Client scope Id. :param id: Client scope Id.
:param realm: Realm in which the clientscope resides. :param realm: Realm in which the client scope resides.
:param client_id: The client in which the clientscope resides. :param client_id: The client in which the client scope resides.
""" """
self._action_type_clientscope(id, client_id, "default", realm, "delete") self._action_type_client_scope(id, client_id, "default", realm, "delete")
def delete_optional_clientscope(self, id, realm: str = "master", client_id=None): def delete_optional_client_scope(self, id, realm: str = "master", client_id=None):
"""Remove a client scope as optional either on realm or client level. """Remove a client scope as optional either on realm or client level.
:param id: Client scope Id. :param id: Client scope Id.
:param realm: Realm in which the clientscope resides. :param realm: Realm in which the client scope resides.
:param client_id: The client in which the clientscope resides. :param client_id: The client in which the client scope resides.
""" """
self._action_type_clientscope(id, client_id, "optional", realm, "delete") self._action_type_client_scope(id, client_id, "optional", realm, "delete")
def _action_type_clientscope( def _action_type_client_scope(
self, id=None, client_id=None, scope_type="default", realm: str = "master", action="add" self, id=None, client_id=None, scope_type="default", realm: str = "master", action="add"
): ):
"""Delete or add a clientscope of type. """Delete or add a client scope of type.
:param name: The name of the clientscope. A lookup will be performed to retrieve the clientscope ID. :param name: The name of the client scope. A lookup will be performed to retrieve the client scope ID.
:param client_id: The ID of the clientscope (preferred to name). :param client_id: The ID of the client scope (preferred to name).
:param scope_type 'default' or 'optional' :param scope_type 'default' or 'optional'
:param realm: The realm in which this group resides, default "master". :param realm: The realm in which this group resides, default "master".
""" """
cid = None if client_id is None else self.get_client_id(client_id=client_id, realm=realm) cid = None if client_id is None else self.get_client_id(client_id=client_id, realm=realm)
# should have a good cid by here. # should have a good cid by here.
clientscope_type_url = self._decide_url_type_clientscope(client_id, scope_type).format( client_scope_type_url = self._decide_url_type_client_scope(client_id, scope_type).format(
realm=realm, id=id, cid=cid, url=self.baseurl realm=realm, id=id, cid=cid, url=self.baseurl
) )
try: try:
method = "PUT" if action == "add" else "DELETE" method = "PUT" if action == "add" else "DELETE"
return self._request(clientscope_type_url, method=method) return self._request(client_scope_type_url, method=method)
except Exception as e: except Exception as e:
place = "realm" if client_id is None else f"client {client_id}" place = "realm" if client_id is None else f"client {client_id}"
self.fail_request(e, msg=f"Unable to {action} {scope_type} clientscope {id} @ {place} : {e}") self.fail_request(e, msg=f"Unable to {action} {scope_type} client scope {id} @ {place} : {e}")
def create_clientsecret(self, id, realm: str = "master"): def create_clientsecret(self, id, realm: str = "master"):
"""Generate a new client secret by id """Generate a new client secret by id
@@ -2032,7 +2020,7 @@ class KeycloakAPI:
composite_url = "" composite_url = ""
try: try:
if clientid is not None: if clientid is not None:
client = self.get_client_by_clientid(client_id=clientid, realm=realm) client = self.get_client_by_client_id(client_id=clientid, realm=realm)
cid = client["id"] cid = client["id"]
composite_url = URL_CLIENT_ROLE_COMPOSITES.format( composite_url = URL_CLIENT_ROLE_COMPOSITES.format(
url=self.baseurl, realm=realm, id=cid, name=quote(rolerep["name"], safe="") url=self.baseurl, realm=realm, id=cid, name=quote(rolerep["name"], safe="")
@@ -2050,7 +2038,7 @@ class KeycloakAPI:
composite_url = "" composite_url = ""
try: try:
if clientid is not None: if clientid is not None:
client = self.get_client_by_clientid(client_id=clientid, realm=realm) client = self.get_client_by_client_id(client_id=clientid, realm=realm)
cid = client["id"] cid = client["id"]
composite_url = URL_CLIENT_ROLE_COMPOSITES.format( composite_url = URL_CLIENT_ROLE_COMPOSITES.format(
url=self.baseurl, realm=realm, id=cid, name=quote(rolerep["name"], safe="") url=self.baseurl, realm=realm, id=cid, name=quote(rolerep["name"], safe="")
@@ -2069,7 +2057,7 @@ class KeycloakAPI:
composite_url = "" composite_url = ""
try: try:
if clientid is not None: if clientid is not None:
client = self.get_client_by_clientid(client_id=clientid, realm=realm) client = self.get_client_by_client_id(client_id=clientid, realm=realm)
cid = client["id"] cid = client["id"]
composite_url = URL_CLIENT_ROLE_COMPOSITES.format( composite_url = URL_CLIENT_ROLE_COMPOSITES.format(
url=self.baseurl, realm=realm, id=cid, name=quote(rolerep["name"], safe="") url=self.baseurl, realm=realm, id=cid, name=quote(rolerep["name"], safe="")
@@ -3271,192 +3259,192 @@ class KeycloakAPI:
except Exception: except Exception:
return False return False
def get_all_clientscope_scope_mappings(self, clientscope_id, realm: str = "master"): def get_all_client_scope_scope_mappings(self, client_scope_id, realm: str = "master"):
"""Fetch all (realm and client) roles (scope-mappings) associated with the clientscope for a specific clientscope on the Keycloak server. """Fetch all (realm and client) roles (scope-mappings) associated with the client scope for a specific client scope on the Keycloak server.
:param clientscope_id: ID of the clientscope from which to obtain the associated roles. :param client_scope_id: ID of the client scope from which to obtain the associated roles.
:param realm: Realm from which to obtain the scope. :param realm: Realm from which to obtain the scope.
:return: The clientscope scope-mappings. :return: The client scope scope-mappings.
""" """
client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS.format(url=self.baseurl, realm=realm, id=clientscope_id) client_role_scope_url = URL_CLIENT_SCOPE_SCOPE_MAPPINGS.format(url=self.baseurl, realm=realm, id=client_scope_id)
try: try:
return self._request_and_deserialize(client_role_scope_url, method="GET") return self._request_and_deserialize(client_role_scope_url, method="GET")
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not fetch roles for client-scope {clientscope_id} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not fetch roles for client scope {client_scope_id} in realm {realm}: {e}")
def get_clientscope_scope_mappings_realm(self, clientscope_id, realm: str = "master"): def get_client_scope_scope_mappings_realm(self, client_scope_id, realm: str = "master"):
"""Fetch the realm roles (scope-mappings) associated with the clientscope for a specific clientscope on the Keycloak server. """Fetch the realm roles (scope-mappings) associated with the client scope for a specific client scope on the Keycloak server.
:param clientscope_id: ID of the clientscope from which to obtain the associated roles. :param client_scope_id: ID of the client scope from which to obtain the associated roles.
:param realm: Realm from which to obtain the scope. :param realm: Realm from which to obtain the scope.
:return: The clientscope realm scope-mappings. :return: The client scope realm scope-mappings.
""" """
client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_REALM.format( client_role_scope_url = URL_CLIENT_SCOPE_SCOPE_MAPPINGS_REALM.format(
url=self.baseurl, realm=realm, id=clientscope_id url=self.baseurl, realm=realm, id=client_scope_id
) )
try: try:
return self._request_and_deserialize(client_role_scope_url, method="GET") return self._request_and_deserialize(client_role_scope_url, method="GET")
except Exception as e: except Exception as e:
self.fail_request( self.fail_request(
e, msg=f"Could not fetch realm roles for client-scope {clientscope_id} in realm {realm}: {e}" e, msg=f"Could not fetch realm roles for client scope {client_scope_id} in realm {realm}: {e}"
) )
def get_clientscope_scope_mappings_client(self, clientscope_id, client_id, realm: str = "master"): def get_client_scope_scope_mappings_client(self, client_scope_id, client_id, realm: str = "master"):
"""Fetch the client roles (scope-mappings) associated with the clientscope for a specific clientscope and client on the Keycloak server. """Fetch the client roles (scope-mappings) associated with the client scope for a specific client scope and client on the Keycloak server.
:param clientscope_id: ID of the clientscope from which to obtain the associated roles. :param client_scope_id: ID of the client scope from which to obtain the associated roles.
:param clientid: ID of the client from which to obtain the associated roles. :param clientid: ID of the client from which to obtain the associated roles.
:param realm: Realm from which to obtain the scope. :param realm: Realm from which to obtain the scope.
:return: The clientscope client scope-mappings. :return: The client scope client scope-mappings.
""" """
client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_CLIENT.format( client_role_scope_url = URL_CLIENT_SCOPE_SCOPE_MAPPINGS_CLIENT.format(
url=self.baseurl, realm=realm, id=clientscope_id, client=client_id url=self.baseurl, realm=realm, id=client_scope_id, client=client_id
) )
try: try:
return self._request_and_deserialize(client_role_scope_url, method="GET") return self._request_and_deserialize(client_role_scope_url, method="GET")
except Exception as e: except Exception as e:
self.fail_request( self.fail_request(
e, e,
msg=f"Could not fetch client roles from client {client_id} for client-scope {clientscope_id} in realm {realm}: {e}", msg=f"Could not fetch client roles from client {client_id} for client scope {client_scope_id} in realm {realm}: {e}",
) )
def get_client_role_scope_from_client(self, clientid, clientscopeid, realm: str = "master"): def get_client_role_scope_from_client(self, target_client_id, role_owner_client_id, realm: str = "master"):
"""Fetch the roles associated with the client's scope for a specific client on the Keycloak server. """Fetch the roles associated with the client's scope for a specific client on the Keycloak server.
:param clientid: ID of the client from which to obtain the associated roles. :param target_client_id: ID of the client from which to obtain the associated roles.
:param clientscopeid: ID of the client who owns the roles. :param role_owner_client_id: ID of the client who owns the roles.
:param realm: Realm from which to obtain the scope. :param realm: Realm from which to obtain the scope.
:return: The client scope of roles from specified client. :return: The client scope of roles from specified client.
""" """
client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format( client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format(
url=self.baseurl, realm=realm, id=clientid, scopeid=clientscopeid url=self.baseurl, realm=realm, id=target_client_id, scopeid=role_owner_client_id
) )
try: try:
return self._request_and_deserialize(client_role_scope_url, method="GET") return self._request_and_deserialize(client_role_scope_url, method="GET")
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not fetch roles scope for client {clientid} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not fetch roles scope for client {target_client_id} in realm {realm}: {e}")
def update_client_role_scope_from_client(self, payload, clientid, clientscopeid, realm: str = "master"): def update_client_role_scope_from_client(self, roles, target_client_id, role_owner_client_id, realm: str = "master"):
"""Update and fetch the roles associated with the client's scope on the Keycloak server. """Update and fetch the roles associated with the client's scope on the Keycloak server.
:param payload: List of roles to be added to the scope. :param roles: List of roles to be added to the scope.
:param clientid: ID of the client to update scope. :param target_client_id: ID of the client to update scope.
:param clientscopeid: ID of the client who owns the roles. :param role_owner_client_id: ID of the client who owns the roles.
:param realm: Realm from which to obtain the clients. :param realm: Realm from which to obtain the clients.
:return: The client scope of roles from specified client. :return: The client scope of roles from specified client.
""" """
client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format( client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format(
url=self.baseurl, realm=realm, id=clientid, scopeid=clientscopeid url=self.baseurl, realm=realm, id=target_client_id, scopeid=role_owner_client_id
) )
try: try:
self._request(client_role_scope_url, method="POST", data=json.dumps(payload)) self._request(client_role_scope_url, method="POST", data=json.dumps(roles))
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not update roles scope for client {clientid} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not update roles scope for client {target_client_id} in realm {realm}: {e}")
return self.get_client_role_scope_from_client(clientid, clientscopeid, realm) return self.get_client_role_scope_from_client(target_client_id, role_owner_client_id, realm)
def delete_client_role_scope_from_client(self, payload, clientid, clientscopeid, realm: str = "master"): def delete_client_role_scope_from_client(self, roles, target_client_id, role_owner_client_id, realm: str = "master"):
"""Delete the roles contains in the payload from the client's scope on the Keycloak server. """Delete the roles from the client's scope on the Keycloak server.
:param payload: List of roles to be deleted. :param roles: List of roles to be deleted.
:param clientid: ID of the client to delete roles from scope. :param target_client_id: ID of the client to delete roles from scope.
:param clientscopeid: ID of the client who owns the roles. :param role_owner_client_id: ID of the client who owns the roles.
:param realm: Realm from which to obtain the clients. :param realm: Realm from which to obtain the clients.
:return: The client scope of roles from specified client. :return: The client scope of roles from specified client.
""" """
client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format( client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format(
url=self.baseurl, realm=realm, id=clientid, scopeid=clientscopeid url=self.baseurl, realm=realm, id=target_client_id, scopeid=role_owner_client_id
) )
try: try:
self._request(client_role_scope_url, method="DELETE", data=json.dumps(payload)) self._request(client_role_scope_url, method="DELETE", data=json.dumps(roles))
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not delete roles scope for client {clientid} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not delete roles from scope for client {target_client_id} in realm {realm}: {e}")
return self.get_client_role_scope_from_client(clientid, clientscopeid, realm) return self.get_client_role_scope_from_client(target_client_id, role_owner_client_id, realm)
def update_clientscope_scope_mappings_client( def update_client_scope_scope_mappings_client(
self, payload: list[dict], clientscope_id: str, client_id: str, realm: str = "master" self, roles: list[dict], client_scope_id: str, role_owner_client_id: str, realm: str = "master"
): ):
"""Update and fetch the client roles (scope-mappings) associated with the clientscope on the Keycloak server. """Update and fetch the client roles (scope-mappings) associated with the client scope on the Keycloak server.
:param payload: List of client roles to be added to the scope. :param roles: List of client roles to be added to the scope.
:param clientscope_id: ID of the clientscope to update scope-mappings. :param client_scope_id: ID of the client scope to update scope-mappings.
:param clientid: ID of the client from which to obtain the associated roles. :param role_owner_client_id: ID of the client from which to obtain the associated roles.
:param realm: Realm from which to obtain the client. :param realm: Realm from which to obtain the client.
:return: The clientscope client scope-mappings. :return: The client scope client scope-mappings.
""" """
client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_CLIENT.format( client_role_scope_url = URL_CLIENT_SCOPE_SCOPE_MAPPINGS_CLIENT.format(
url=self.baseurl, realm=realm, id=clientscope_id, client=client_id url=self.baseurl, realm=realm, id=client_scope_id, client=role_owner_client_id
) )
try: try:
self._request(client_role_scope_url, method="POST", data=json.dumps(payload)) self._request(client_role_scope_url, method="POST", data=json.dumps(roles))
except Exception as e: except Exception as e:
self.fail_request( self.fail_request(
e, e,
msg=f"Could not update scope mappings for client-scope {client_id}.{clientscope_id} in realm {realm}: {e}", msg=f"Could not update scope mappings for client scope {role_owner_client_id}.{client_scope_id} in realm {realm}: {e}",
) )
return self.get_clientscope_scope_mappings_client(clientscope_id, client_id, realm) return self.get_client_scope_scope_mappings_client(client_scope_id, role_owner_client_id, realm)
def update_clientscope_scope_mappings_realm(self, payload: list[dict], clientscope_id: str, realm: str = "master"): def update_client_scope_scope_mappings_realm(self, roles: list[dict], client_scope_id: str, realm: str = "master"):
"""Update and fetch the realm roles (scope-mappings) associated with the clientscope on the Keycloak server. """Update and fetch the realm roles (scope-mappings) associated with the client scope on the Keycloak server.
:param payload: List of realm roles to be added to the scope. :param roles: List of realm roles to be added to the scope.
:param clientscope_id: ID of the clientscope to update scope-mappings. :param client_scope_id: ID of the client scope to update scope-mappings.
:param realm: Realm from which to obtain the roles. :param realm: Realm from which to obtain the roles.
:return: The clientscope realm scope-mappings. :return: The client scope realm scope-mappings.
""" """
client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_REALM.format( client_role_scope_url = URL_CLIENT_SCOPE_SCOPE_MAPPINGS_REALM.format(
url=self.baseurl, realm=realm, id=clientscope_id url=self.baseurl, realm=realm, id=client_scope_id
) )
try: try:
self._request(client_role_scope_url, method="POST", data=json.dumps(payload)) self._request(client_role_scope_url, method="POST", data=json.dumps(roles))
except Exception as e: except Exception as e:
self.fail_request( self.fail_request(
e, msg=f"Could not update scope mappings for client-scope {clientscope_id} in realm {realm}: {e}" e, msg=f"Could not update scope mappings for client scope {client_scope_id} in realm {realm}: {e}"
) )
return self.get_clientscope_scope_mappings_realm(clientscope_id, realm) return self.get_client_scope_scope_mappings_realm(client_scope_id, realm)
def delete_clientscope_scope_mappings_client( def delete_client_scope_scope_mappings_client(
self, payload: list[dict], clientscope_id: str, client_id: str, realm: str = "master" self, roles: list[dict], client_scope_id: str, role_owner_client_id: str, realm: str = "master"
): ):
"""Delete the client roles (scope_mappings) contained in the payload from the clientscope on the Keycloak server. """Delete the client roles (scope_mappings) from the client scope on the Keycloak server.
:param payload: List of roles to be deleted. :param roles: List of roles to be deleted.
:param clientscope_id: ID of the clientscope to delete roles from scope-mappings. :param client_scope_id: ID of the client scope to delete roles from scope-mappings.
:param clientid: ID of the client who owns the roles. :param role_owner_client_id: ID of the client who owns the roles.
:param realm: Realm from which to obtain the client. :param realm: Realm from which to obtain the client.
:return: The clientscope client scope-mappings. :return: The client scope client scope-mappings.
""" """
client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_CLIENT.format( client_role_scope_url = URL_CLIENT_SCOPE_SCOPE_MAPPINGS_CLIENT.format(
url=self.baseurl, realm=realm, id=clientscope_id, client=client_id url=self.baseurl, realm=realm, id=client_scope_id, client=role_owner_client_id
) )
try: try:
self._request(client_role_scope_url, method="DELETE", data=json.dumps(payload)) self._request(client_role_scope_url, method="DELETE", data=json.dumps(roles))
except Exception as e: except Exception as e:
self.fail_request( self.fail_request(
e, e,
msg=f"Could not delete scope mappings for client-scope {client_id}.{clientscope_id} in realm {realm}: {e}", msg=f"Could not delete scope mappings for client scope {role_owner_client_id}.{client_scope_id} in realm {realm}: {e}",
) )
return self.get_clientscope_scope_mappings_client(clientscope_id, client_id, realm) return self.get_client_scope_scope_mappings_client(client_scope_id, role_owner_client_id, realm)
def delete_clientscope_scope_mappings_realm(self, payload: list[dict], clientscope_id: str, realm: str = "master"): def delete_client_scope_scope_mappings_realm(self, roles: list[dict], client_scope_id: str, realm: str = "master"):
"""Delete the realm roles (scope_mappings) contained in the payload from the clientscope on the Keycloak server. """Delete the realm roles (scope_mappings) contained in the roles from the client scope on the Keycloak server.
:param payload: List of roles to be deleted. :param roles: List of roles to be deleted.
:param clientscope_id: ID of the clientscope to delete roles from scope-mappings. :param client_scope_id: ID of the client scope to delete roles from scope-mappings.
:param realm: Realm from which to obtain the roles. :param realm: Realm from which to obtain the roles.
:return: The clientscope realm scope-mappings. :return: The client scope realm scope-mappings.
""" """
client_role_scope_url = URL_CLIENTSCOPE_SCOPE_MAPPINGS_REALM.format( client_role_scope_url = URL_CLIENT_SCOPE_SCOPE_MAPPINGS_REALM.format(
url=self.baseurl, realm=realm, id=clientscope_id url=self.baseurl, realm=realm, id=client_scope_id
) )
try: try:
self._request(client_role_scope_url, method="DELETE", data=json.dumps(payload)) self._request(client_role_scope_url, method="DELETE", data=json.dumps(roles))
except Exception as e: except Exception as e:
self.fail_request( self.fail_request(
e, msg=f"Could not delete scope mappings for client-scope {clientscope_id} in realm {realm}: {e}" e, msg=f"Could not delete scope mappings for client scope {client_scope_id} in realm {realm}: {e}"
) )
return self.get_clientscope_scope_mappings_realm(clientscope_id, realm) return self.get_client_scope_scope_mappings_realm(client_scope_id, realm)
def get_client_role_scope_from_realm(self, clientid, realm: str = "master"): def get_client_role_scope_from_realm(self, clientid, realm: str = "master"):
"""Fetch the realm roles from the client's scope on the Keycloak server. """Fetch the realm roles from the client's scope on the Keycloak server.
@@ -3470,32 +3458,32 @@ class KeycloakAPI:
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not fetch roles scope for client {clientid} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not fetch roles scope for client {clientid} in realm {realm}: {e}")
def update_client_role_scope_from_realm(self, payload, clientid, realm: str = "master"): def update_client_role_scope_from_realm(self, roles, clientid, realm: str = "master"):
"""Update and fetch the realm roles from the client's scope on the Keycloak server. """Update and fetch the realm roles from the client's scope on the Keycloak server.
:param payload: List of realm roles to add. :param roles: List of realm roles to add.
:param clientid: ID of the client to update scope. :param clientid: ID of the client to update scope.
:param realm: Realm from which to obtain the clients. :param realm: Realm from which to obtain the clients.
:return: The client realm roles scope. :return: The client realm roles scope.
""" """
client_role_scope_url = URL_CLIENT_ROLE_SCOPE_REALM.format(url=self.baseurl, realm=realm, id=clientid) client_role_scope_url = URL_CLIENT_ROLE_SCOPE_REALM.format(url=self.baseurl, realm=realm, id=clientid)
try: try:
self._request(client_role_scope_url, method="POST", data=json.dumps(payload)) self._request(client_role_scope_url, method="POST", data=json.dumps(roles))
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not update roles scope for client {clientid} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not update roles scope for client {clientid} in realm {realm}: {e}")
return self.get_client_role_scope_from_realm(clientid, realm) return self.get_client_role_scope_from_realm(clientid, realm)
def delete_client_role_scope_from_realm(self, payload, clientid, realm: str = "master"): def delete_client_role_scope_from_realm(self, roles, clientid, realm: str = "master"):
"""Delete the realm roles contains in the payload from the client's scope on the Keycloak server. """Delete the realm roles from the client's scope on the Keycloak server.
:param payload: List of realm roles to delete. :param roles: List of realm roles to delete.
:param clientid: ID of the client to delete roles from scope. :param clientid: ID of the client to delete roles from scope.
:param realm: Realm from which to obtain the clients. :param realm: Realm from which to obtain the clients.
:return: The client realm roles scope. :return: The client realm roles scope.
""" """
client_role_scope_url = URL_CLIENT_ROLE_SCOPE_REALM.format(url=self.baseurl, realm=realm, id=clientid) client_role_scope_url = URL_CLIENT_ROLE_SCOPE_REALM.format(url=self.baseurl, realm=realm, id=clientid)
try: try:
self._request(client_role_scope_url, method="DELETE", data=json.dumps(payload)) self._request(client_role_scope_url, method="DELETE", data=json.dumps(roles))
except Exception as e: except Exception as e:
self.fail_request(e, msg=f"Could not delete roles scope for client {clientid} in realm {realm}: {e}") self.fail_request(e, msg=f"Could not delete roles scope for client {clientid} in realm {realm}: {e}")

View File

@@ -66,7 +66,7 @@ def keycloak_clientsecret_module_resolve_params(module: AnsibleModule, kc: Keycl
# less lookup. # less lookup.
if id is None: if id is None:
# Due to the required_one_of spec, client_id is guaranteed to not be None # Due to the required_one_of spec, client_id is guaranteed to not be None
client = kc.get_client_by_clientid(client_id, realm=realm) client = kc.get_client_by_client_id(client_id, realm=realm)
if client is None: if client is None:
module.fail_json(msg=f"Client does not exist {client_id}") module.fail_json(msg=f"Client does not exist {client_id}")

View File

@@ -1100,11 +1100,11 @@ def add_default_client_scopes(desired_client, before_client, realm, kc):
missing_scopes = [item for item in desired_default_scope if item not in before_client["defaultClientScopes"]] missing_scopes = [item for item in desired_default_scope if item not in before_client["defaultClientScopes"]]
if not missing_scopes: if not missing_scopes:
return return
client_scopes = kc.get_clientscopes(realm) client_scopes = kc.get_client_scopes(realm)
for name in missing_scopes: for name in missing_scopes:
scope = find_match(client_scopes, "name", name) scope = find_match(client_scopes, "name", name)
if scope: if scope:
kc.add_default_clientscope(scope["id"], realm, desired_client["clientId"]) kc.add_default_client_scope(scope["id"], realm, desired_client["clientId"])
def add_optional_client_scopes(desired_client, before_client, realm, kc): def add_optional_client_scopes(desired_client, before_client, realm, kc):
@@ -1139,11 +1139,11 @@ def add_optional_client_scopes(desired_client, before_client, realm, kc):
missing_scopes = [item for item in desired_optional_scope if item not in before_client["optionalClientScopes"]] missing_scopes = [item for item in desired_optional_scope if item not in before_client["optionalClientScopes"]]
if not missing_scopes: if not missing_scopes:
return return
client_scopes = kc.get_clientscopes(realm) client_scopes = kc.get_client_scopes(realm)
for name in missing_scopes: for name in missing_scopes:
scope = find_match(client_scopes, "name", name) scope = find_match(client_scopes, "name", name)
if scope: if scope:
kc.add_optional_clientscope(scope["id"], realm, desired_client["clientId"]) kc.add_optional_client_scope(scope["id"], realm, desired_client["clientId"])
def remove_default_client_scopes(desired_client, before_client, realm, kc): def remove_default_client_scopes(desired_client, before_client, realm, kc):
@@ -1178,11 +1178,11 @@ def remove_default_client_scopes(desired_client, before_client, realm, kc):
missing_scopes = [item for item in before_default_scope if item not in desired_client["defaultClientScopes"]] missing_scopes = [item for item in before_default_scope if item not in desired_client["defaultClientScopes"]]
if not missing_scopes: if not missing_scopes:
return return
client_scopes = kc.get_default_clientscopes(realm, desired_client["clientId"]) client_scopes = kc.get_default_client_scopes(realm, desired_client["clientId"])
for name in missing_scopes: for name in missing_scopes:
scope = find_match(client_scopes, "name", name) scope = find_match(client_scopes, "name", name)
if scope: if scope:
kc.delete_default_clientscope(scope["id"], realm, desired_client["clientId"]) kc.delete_default_client_scope(scope["id"], realm, desired_client["clientId"])
def remove_optional_client_scopes(desired_client, before_client, realm, kc): def remove_optional_client_scopes(desired_client, before_client, realm, kc):
@@ -1217,11 +1217,11 @@ def remove_optional_client_scopes(desired_client, before_client, realm, kc):
missing_scopes = [item for item in before_optional_scope if item not in desired_client["optionalClientScopes"]] missing_scopes = [item for item in before_optional_scope if item not in desired_client["optionalClientScopes"]]
if not missing_scopes: if not missing_scopes:
return return
client_scopes = kc.get_optional_clientscopes(realm, desired_client["clientId"]) client_scopes = kc.get_optional_client_scopes(realm, desired_client["clientId"])
for name in missing_scopes: for name in missing_scopes:
scope = find_match(client_scopes, "name", name) scope = find_match(client_scopes, "name", name)
if scope: if scope:
kc.delete_optional_clientscope(scope["id"], realm, desired_client["clientId"]) kc.delete_optional_client_scope(scope["id"], realm, desired_client["clientId"])
def main(): def main():
@@ -1346,7 +1346,7 @@ def main():
# See if it already exists in Keycloak # See if it already exists in Keycloak
if cid is None: if cid is None:
before_client = kc.get_client_by_clientid(module.params.get("client_id"), realm=realm) before_client = kc.get_client_by_client_id(module.params.get("client_id"), realm=realm)
if before_client is not None: if before_client is not None:
cid = before_client["id"] cid = before_client["id"]
else: else:
@@ -1440,7 +1440,7 @@ def main():
# create it # create it
kc.create_client(desired_client, realm=realm) kc.create_client(desired_client, realm=realm)
after_client = kc.get_client_by_clientid(desired_client["clientId"], realm=realm) after_client = kc.get_client_by_client_id(desired_client["clientId"], realm=realm)
result["end_state"] = sanitize_cr(after_client) result["end_state"] = sanitize_cr(after_client)

View File

@@ -19,7 +19,7 @@ description:
to the REST API using OpenID Connect; the user connecting and the client being used must have the requisite access rights. to the REST API using OpenID Connect; the user connecting and the client being used must have the requisite access rights.
In a default Keycloak installation, admin-cli and an admin user would work, as would a separate client definition with In a default Keycloak installation, admin-cli and an admin user would work, as would a separate client definition with
the scope tailored to your needs and a user having the expected roles. the scope tailored to your needs and a user having the expected roles.
- Client O(client_id) must have O(middleware_automation.keycloak.keycloak_client#module:full_scope_allowed) set to V(false). - Client O(target_client_id) must have O(middleware_automation.keycloak.keycloak_client#module:full_scope_allowed) set to V(false).
- Attributes are multi-valued in the Keycloak API. All attributes are lists of individual values and are returned that way - Attributes are multi-valued in the Keycloak API. All attributes are lists of individual values and are returned that way
by this module. You may pass single values for attributes when calling the module, and this is translated into a list by this module. You may pass single values for attributes when calling the module, and this is translated into a list
suitable for the API. suitable for the API.
@@ -50,12 +50,12 @@ options:
- The Keycloak realm under which clients resides. - The Keycloak realm under which clients resides.
default: 'master' default: 'master'
client_id: target_client_id:
type: str type: str
required: true required: true
description: description:
- Roles provided in O(role_names) while be added to this client scope. - Roles provided in O(role_names) while be added to this client scope.
client_scope_id: role_owner_client_id:
type: str type: str
description: description:
- If the O(role_names) are client role, the client ID under which it resides. - If the O(role_names) are client role, the client ID under which it resides.
@@ -66,8 +66,8 @@ options:
elements: str elements: str
description: description:
- Names of roles to manipulate. - Names of roles to manipulate.
- If O(client_scope_id) is present, all roles must be under this client. - If O(role_owner_client_id) is present, all roles must be under this client.
- If O(client_scope_id) is absent, all roles must be under the realm. - If O(role_owner_client_id) is absent, all roles must be under the realm.
extends_documentation_fragment: extends_documentation_fragment:
- middleware_automation.keycloak.keycloak - middleware_automation.keycloak.keycloak
- middleware_automation.keycloak.actiongroup_keycloak - middleware_automation.keycloak.actiongroup_keycloak
@@ -85,8 +85,8 @@ EXAMPLES = r"""
auth_username: USERNAME auth_username: USERNAME
auth_password: PASSWORD auth_password: PASSWORD
realm: MyCustomRealm realm: MyCustomRealm
client_id: frontend-client-public target_client_id: frontend-client-public
client_scope_id: backend-client-private role_owner_client_id: backend-client-private
role_names: role_names:
- backend-role-admin - backend-role-admin
- backend-role-user - backend-role-user
@@ -98,8 +98,8 @@ EXAMPLES = r"""
auth_username: USERNAME auth_username: USERNAME
auth_password: PASSWORD auth_password: PASSWORD
realm: MyCustomRealm realm: MyCustomRealm
client_id: frontend-client-public target_client_id: frontend-client-public
client_scope_id: backend-client-private role_owner_client_id: backend-client-private
role_names: role_names:
- backend-role-admin - backend-role-admin
state: absent state: absent
@@ -111,7 +111,7 @@ EXAMPLES = r"""
auth_username: USERNAME auth_username: USERNAME
auth_password: PASSWORD auth_password: PASSWORD
realm: MyCustomRealm realm: MyCustomRealm
client_id: frontend-client-public target_client_id: frontend-client-public
role_names: role_names:
- realm-role-admin - realm-role-admin
- realm-role-user - realm-role-user
@@ -167,8 +167,8 @@ def main():
argument_spec = keycloak_argument_spec() argument_spec = keycloak_argument_spec()
meta_args = dict( meta_args = dict(
client_id=dict(type="str", required=True), target_client_id=dict(type="str", required=True),
client_scope_id=dict(type="str"), role_owner_client_id=dict(type="str"),
realm=dict(type="str", default="master"), realm=dict(type="str", default="master"),
role_names=dict(type="list", elements="str", required=True), role_names=dict(type="list", elements="str", required=True),
state=dict(type="str", default="present", choices=["present", "absent"]), state=dict(type="str", default="present", choices=["present", "absent"]),
@@ -189,8 +189,8 @@ def main():
kc = KeycloakAPI(module, connection_header) kc = KeycloakAPI(module, connection_header)
realm = module.params.get("realm") realm = module.params.get("realm")
clientid = module.params.get("client_id") target_client_id = module.params.get("target_client_id")
client_scope_id = module.params.get("client_scope_id") role_owner_client_id = module.params.get("role_owner_client_id")
role_names = module.params.get("role_names") role_names = module.params.get("role_names")
state = module.params.get("state") state = module.params.get("state")
@@ -198,23 +198,23 @@ def main():
if not objRealm: if not objRealm:
module.fail_json(msg=f"Failed to retrive realm '{realm}'") module.fail_json(msg=f"Failed to retrive realm '{realm}'")
objClient = kc.get_client_by_clientid(clientid, realm) objClient = kc.get_client_by_client_id(target_client_id, realm)
if not objClient: if not objClient:
module.fail_json(msg=f"Failed to retrive client '{realm}.{clientid}'") module.fail_json(msg=f"Failed to retrive client '{realm}.{target_client_id}'")
if objClient["fullScopeAllowed"] and state == "present": if objClient["fullScopeAllowed"] and state == "present":
module.fail_json(msg=f"FullScopeAllowed is active for Client '{realm}.{clientid}'") module.fail_json(msg=f"FullScopeAllowed is active for Client '{realm}.{target_client_id}'")
if client_scope_id: if role_owner_client_id:
objClientScope = kc.get_client_by_clientid(client_scope_id, realm) role_owner_client = kc.get_client_by_client_id(role_owner_client_id, realm)
if not objClientScope: if not role_owner_client:
module.fail_json(msg=f"Failed to retrive client '{realm}.{client_scope_id}'") module.fail_json(msg=f"Failed to retrive client '{realm}.{role_owner_client_id}'")
before_role_mapping = kc.get_client_role_scope_from_client(objClient["id"], objClientScope["id"], realm) before_role_mapping = kc.get_client_role_scope_from_client(objClient["id"], role_owner_client["id"], realm)
else: else:
before_role_mapping = kc.get_client_role_scope_from_realm(objClient["id"], realm) before_role_mapping = kc.get_client_role_scope_from_realm(objClient["id"], realm)
if client_scope_id: if role_owner_client_id:
# retrive all role from client_scope # retrive all role from client_scope
client_scope_roles_by_name = kc.get_client_roles_by_id(objClientScope["id"], realm) client_scope_roles_by_name = kc.get_client_roles_by_id(role_owner_client["id"], realm)
else: else:
# retrive all role from realm # retrive all role from realm
client_scope_roles_by_name = kc.get_realm_roles(realm) client_scope_roles_by_name = kc.get_realm_roles(realm)
@@ -228,8 +228,8 @@ def main():
# update desired # update desired
for role_name in role_names: for role_name in role_names:
if role_name not in client_scope_roles_by_name: if role_name not in client_scope_roles_by_name:
if client_scope_id: if role_owner_client_id:
module.fail_json(msg=f"Failed to retrive role '{realm}.{client_scope_id}.{role_name}'") module.fail_json(msg=f"Failed to retrive role '{realm}.{role_owner_client_id}.{role_name}'")
else: else:
module.fail_json(msg=f"Failed to retrive role '{realm}.{role_name}'") module.fail_json(msg=f"Failed to retrive role '{realm}.{role_name}'")
if role_name not in role_mapping_by_name: if role_name not in role_mapping_by_name:
@@ -253,33 +253,33 @@ def main():
if not result["changed"]: if not result["changed"]:
# no changes # no changes
result["end_state"] = before_role_mapping result["end_state"] = before_role_mapping
result["msg"] = f"No changes required for client role scope {clientid}." result["msg"] = f"No changes required for client role scope {target_client_id}."
elif state == "present": elif state == "present":
# doing update # doing update
if module.check_mode: if module.check_mode:
result["end_state"] = desired_role_mapping result["end_state"] = desired_role_mapping
elif client_scope_id: elif role_owner_client_id:
result["end_state"] = kc.update_client_role_scope_from_client( result["end_state"] = kc.update_client_role_scope_from_client(
role_mapping_to_manipulate, objClient["id"], objClientScope["id"], realm role_mapping_to_manipulate, objClient["id"], role_owner_client["id"], realm
) )
else: else:
result["end_state"] = kc.update_client_role_scope_from_realm( result["end_state"] = kc.update_client_role_scope_from_realm(
role_mapping_to_manipulate, objClient["id"], realm role_mapping_to_manipulate, objClient["id"], realm
) )
result["msg"] = f"Client role scope for {clientid} has been updated" result["msg"] = f"Client role scope for {target_client_id} has been updated"
else: else:
# doing delete # doing delete
if module.check_mode: if module.check_mode:
result["end_state"] = desired_role_mapping result["end_state"] = desired_role_mapping
elif client_scope_id: elif role_owner_client_id:
result["end_state"] = kc.delete_client_role_scope_from_client( result["end_state"] = kc.delete_client_role_scope_from_client(
role_mapping_to_manipulate, objClient["id"], objClientScope["id"], realm role_mapping_to_manipulate, objClient["id"], role_owner_client["id"], realm
) )
else: else:
result["end_state"] = kc.delete_client_role_scope_from_realm( result["end_state"] = kc.delete_client_role_scope_from_realm(
role_mapping_to_manipulate, objClient["id"], realm role_mapping_to_manipulate, objClient["id"], realm
) )
result["msg"] = f"Client role scope for {clientid} has been deleted" result["msg"] = f"Client role scope for {target_client_id} has been deleted"
module.exit_json(**result) module.exit_json(**result)

View File

@@ -230,7 +230,7 @@ def main():
attributes = module.params.get('attributes') attributes = module.params.get('attributes')
protocol_mappers = module.params.get('protocol_mappers') protocol_mappers = module.params.get('protocol_mappers')
before_scope = kc.get_clientscope_by_name(name, realm=realm) before_scope = kc.get_client_scope_by_name(name, realm=realm)
if state == 'absent': if state == 'absent':
if before_scope: if before_scope:
@@ -239,7 +239,7 @@ def main():
result['diff'] = dict(before=before_scope, after='') result['diff'] = dict(before=before_scope, after='')
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
kc.delete_clientscope(cid=before_scope['id'], realm=realm) kc.delete_client_scope(cid=before_scope['id'], realm=realm)
result['msg'] = "Client scope {name} has been deleted".format(name=name) result['msg'] = "Client scope {name} has been deleted".format(name=name)
else: else:
result['msg'] = "Client scope {name} does not exist, doing nothing".format(name=name) result['msg'] = "Client scope {name} does not exist, doing nothing".format(name=name)
@@ -261,8 +261,8 @@ def main():
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
kc.create_clientscope(scope_rep, realm=realm) kc.create_client_scope(scope_rep, realm=realm)
after_scope = kc.get_clientscope_by_name(name, realm=realm) after_scope = kc.get_client_scope_by_name(name, realm=realm)
if protocol_mappers: if protocol_mappers:
for mapper in protocol_mappers: for mapper in protocol_mappers:
@@ -272,8 +272,8 @@ def main():
'protocolMapper': mapper['protocolMapper'], 'protocolMapper': mapper['protocolMapper'],
'config': mapper['config'], 'config': mapper['config'],
} }
kc.create_clientscope_protocolmapper(after_scope['id'], mapper_rep, realm=realm) kc.create_client_scope_protocolmapper(after_scope['id'], mapper_rep, realm=realm)
after_scope = kc.get_clientscope_by_name(name, realm=realm) after_scope = kc.get_client_scope_by_name(name, realm=realm)
result['end_state'] = after_scope result['end_state'] = after_scope
result['msg'] = "Client scope {name} has been created".format(name=name) result['msg'] = "Client scope {name} has been created".format(name=name)
@@ -296,10 +296,10 @@ def main():
result['diff'] = dict(before=before_scope, after=scope_rep) result['diff'] = dict(before=before_scope, after=scope_rep)
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
kc.update_clientscope(scope_rep, realm=realm) kc.update_client_scope(scope_rep, realm=realm)
if protocol_mappers: if protocol_mappers:
existing_mappers = kc.get_clientscope_protocolmappers(before_scope['id'], realm=realm) existing_mappers = kc.get_client_scope_protocolmappers(before_scope['id'], realm=realm)
existing_mapper_names = {m['name'] for m in existing_mappers} existing_mapper_names = {m['name'] for m in existing_mappers}
for mapper in protocol_mappers: for mapper in protocol_mappers:
@@ -312,9 +312,9 @@ def main():
'protocolMapper': mapper['protocolMapper'], 'protocolMapper': mapper['protocolMapper'],
'config': mapper['config'], 'config': mapper['config'],
} }
kc.create_clientscope_protocolmapper(before_scope['id'], mapper_rep, realm=realm) kc.create_client_scope_protocolmapper(before_scope['id'], mapper_rep, realm=realm)
after_scope = kc.get_clientscope_by_name(name, realm=realm) after_scope = kc.get_client_scope_by_name(name, realm=realm)
result['end_state'] = after_scope result['end_state'] = after_scope
if result['changed']: if result['changed']:

View File

@@ -6,16 +6,16 @@
from __future__ import annotations from __future__ import annotations
DOCUMENTATION = r""" DOCUMENTATION = r"""
module: keycloak_clientscope_rolemappings module: keycloak_client_scope_rolemappings
short_description: Allows administration of Keycloak clientscope scope mappings to restrict the usage of certain roles to short_description: Allows administration of Keycloak client scope scope mappings to restrict the usage of certain roles to
specific clientscopes specific client scopes
# Originally added in community.general 13.1.0 # Originally added in community.general 13.1.0
version_added: "3.0.0" version_added: "3.0.0"
description: description:
- This module allows you to add or remove Keycloak roles from clientscopes using the Keycloak REST API. It requires access - This module allows you to add or remove Keycloak roles from client scopes using the Keycloak REST API. It requires access
to the REST API using OpenID Connect; the user connecting and the client being used must have the requisite access rights. to the REST API using OpenID Connect; the user connecting and the client being used must have the requisite access rights.
In a default Keycloak installation, C(admin-cli) and an admin user would work, as would a separate client definition with In a default Keycloak installation, C(admin-cli) and an admin user would work, as would a separate client definition with
the scope tailored to your needs and a user having the expected roles. the scope tailored to your needs and a user having the expected roles.
@@ -49,11 +49,11 @@ options:
- The Keycloak realm under which clients resides. - The Keycloak realm under which clients resides.
default: 'master' default: 'master'
clientscope_id: client_scope_id:
required: true required: true
type: str type: str
description: description:
- Roles provided in O(role_names) will be added to this clientscope. - Roles provided in O(role_names) will be added to this client scope.
client_id: client_id:
type: str type: str
@@ -81,40 +81,40 @@ author:
""" """
EXAMPLES = r""" EXAMPLES = r"""
- name: Add roles to clientscope - name: Add roles to client scope
middleware_automation.keycloak.keycloak_clientscope_rolemappings: middleware_automation.keycloak.keycloak_client_scope_rolemappings:
auth_keycloak_url: https://auth.example.com auth_keycloak_url: https://auth.example.com
auth_realm: master auth_realm: master
auth_username: USERNAME auth_username: USERNAME
auth_password: PASSWORD auth_password: PASSWORD
realm: MyCustomRealm realm: MyCustomRealm
client_id: frontend-client-public client_id: frontend-client-public
clientscope_id: frontend-clientscope client_scope_id: frontend-client-scope
role_names: role_names:
- backend-role-admin - backend-role-admin
- backend-role-user - backend-role-user
- name: Remove roles from clientscope - name: Remove roles from client scope
middleware_automation.keycloak.keycloak_clientscope_rolemappings: middleware_automation.keycloak.keycloak_client_scope_rolemappings:
auth_keycloak_url: https://auth.example.com auth_keycloak_url: https://auth.example.com
auth_realm: master auth_realm: master
auth_username: USERNAME auth_username: USERNAME
auth_password: PASSWORD auth_password: PASSWORD
realm: MyCustomRealm realm: MyCustomRealm
client_id: frontend-client-public client_id: frontend-client-public
clientscope_id: frontend-clientscope client_scope_id: frontend-client-scope
role_names: role_names:
- backend-role-admin - backend-role-admin
state: absent state: absent
- name: Add realm roles to clientscope - name: Add realm roles to client scope
middleware_automation.keycloak.keycloak_clientscope_rolemappings: middleware_automation.keycloak.keycloak_client_scope_rolemappings:
auth_keycloak_url: https://auth.example.com auth_keycloak_url: https://auth.example.com
auth_realm: master auth_realm: master
auth_username: USERNAME auth_username: USERNAME
auth_password: PASSWORD auth_password: PASSWORD
realm: MyCustomRealm realm: MyCustomRealm
clientscope_id: frontend-clientscope client_scope_id: frontend-client-scope
role_names: role_names:
- realm-role-admin - realm-role-admin
- realm-role-user - realm-role-user
@@ -122,7 +122,7 @@ EXAMPLES = r"""
RETURN = r""" RETURN = r"""
end_state: end_state:
description: Representation of clientscope scope mappings after module execution. description: Representation of client scope scope mappings after module execution.
returned: on success returned: on success
type: list type: list
elements: dict elements: dict
@@ -164,7 +164,7 @@ def main():
meta_args = dict( meta_args = dict(
client_id=dict(type="str"), client_id=dict(type="str"),
clientscope_id=dict(type="str", required=True), client_scope_id=dict(type="str", required=True),
realm=dict(type="str", default="master"), realm=dict(type="str", default="master"),
role_names=dict(type="list", elements="str", required=True), role_names=dict(type="list", elements="str", required=True),
state=dict(type="str", default="present", choices=["present", "absent"]), state=dict(type="str", default="present", choices=["present", "absent"]),
@@ -186,7 +186,7 @@ def main():
realm = module.params["realm"] realm = module.params["realm"]
client_id = module.params["client_id"] client_id = module.params["client_id"]
clientscope_id = module.params["clientscope_id"] client_scope_id = module.params["client_scope_id"]
role_names = module.params["role_names"] role_names = module.params["role_names"]
state = module.params["state"] state = module.params["state"]
@@ -194,23 +194,23 @@ def main():
if not realm_object: if not realm_object:
module.fail_json(msg=f"Failed to retrieve realm '{realm}'") module.fail_json(msg=f"Failed to retrieve realm '{realm}'")
clientscope_object = kc.get_clientscope_by_name(clientscope_id, realm) client_scope_object = kc.get_client_scope_by_name(client_scope_id, realm)
if not clientscope_object: if not client_scope_object:
module.fail_json(msg=f"Failed to retrieve client-scope '{clientscope_id}'") module.fail_json(msg=f"Failed to retrieve client scope '{client_scope_id}'")
if client_id: if client_id:
# add client role # add client role
client_object = kc.get_client_by_clientid(client_id, realm) client_object = kc.get_client_by_client_id(client_id, realm)
if not client_object: if not client_object:
module.fail_json(msg=f"Failed to retrieve client '{realm}.{client_id}'") module.fail_json(msg=f"Failed to retrieve client '{realm}.{client_id}'")
if client_object["fullScopeAllowed"] and state == "present": if client_object["fullScopeAllowed"] and state == "present":
module.fail_json(msg=f"FullScopeAllowed is active for Client '{realm}.{client_id}'") module.fail_json(msg=f"FullScopeAllowed is active for Client '{realm}.{client_id}'")
before_roles = kc.get_clientscope_scope_mappings_client(clientscope_object["id"], client_object["id"], realm) before_roles = kc.get_client_scope_scope_mappings_client(client_scope_object["id"], client_object["id"], realm)
available_roles_by_name = kc.get_client_roles_by_id(client_object["id"], realm) available_roles_by_name = kc.get_client_roles_by_id(client_object["id"], realm)
else: else:
# add realm role # add realm role
before_roles = kc.get_clientscope_scope_mappings_realm(clientscope_object["id"], realm) before_roles = kc.get_client_scope_scope_mappings_realm(client_scope_object["id"], realm)
available_roles_by_name = kc.get_realm_roles(realm) available_roles_by_name = kc.get_realm_roles(realm)
# convert to indexed Dict by name # convert to indexed Dict by name
@@ -248,33 +248,33 @@ def main():
if not result["changed"]: if not result["changed"]:
# no changes # no changes
result["end_state"] = before_roles result["end_state"] = before_roles
result["msg"] = f"No changes required for clientscope {clientscope_id}." result["msg"] = f"No changes required for client scope {client_scope_id}."
elif state == "present": elif state == "present":
# doing update # doing update
if module.check_mode: if module.check_mode:
result["end_state"] = desired_role_mapping result["end_state"] = desired_role_mapping
elif client_id: elif client_id:
result["end_state"] = kc.update_clientscope_scope_mappings_client( result["end_state"] = kc.update_client_scope_scope_mappings_client(
changed_roles, clientscope_object["id"], client_object["id"], realm changed_roles, client_scope_object["id"], client_object["id"], realm
) )
else: else:
result["end_state"] = kc.update_clientscope_scope_mappings_realm( result["end_state"] = kc.update_client_scope_scope_mappings_realm(
changed_roles, clientscope_object["id"], realm changed_roles, client_scope_object["id"], realm
) )
result["msg"] = f"Clientscope scope mappings for {clientscope_id} have been updated" result["msg"] = f"Clientscope scope mappings for {client_scope_id} have been updated"
else: else:
# doing delete # doing delete
if module.check_mode: if module.check_mode:
result["end_state"] = desired_role_mapping result["end_state"] = desired_role_mapping
elif client_id: elif client_id:
result["end_state"] = kc.delete_clientscope_scope_mappings_client( result["end_state"] = kc.delete_client_scope_scope_mappings_client(
changed_roles, clientscope_object["id"], client_object["id"], realm changed_roles, client_scope_object["id"], client_object["id"], realm
) )
else: else:
result["end_state"] = kc.delete_clientscope_scope_mappings_realm( result["end_state"] = kc.delete_client_scope_scope_mappings_realm(
changed_roles, clientscope_object["id"], realm changed_roles, client_scope_object["id"], realm
) )
result["msg"] = f"Clientscope scope mappings for {clientscope_id} have been deleted" result["msg"] = f"Clientscope scope mappings for {client_scope_id} have been deleted"
module.exit_json(**result) module.exit_json(**result)

View File

@@ -6,15 +6,15 @@
from __future__ import annotations from __future__ import annotations
DOCUMENTATION = r""" DOCUMENTATION = r"""
module: keycloak_clientscope_type module: keycloak_client_scope_type
short_description: Set the type of aclientscope in realm or client using Keycloak API short_description: Set the type of a client scope in a realm or client using the Keycloak API
# Originally added in community.general 6.6.0 # Originally added in community.general 6.6.0
version_added: "3.0.0" version_added: "3.0.0"
description: description:
- This module allows you to set the type (optional, default) of clientscopes using the Keycloak REST API. It requires access - This module allows you to set the type (optional, default) of client scopes using the Keycloak REST API. It requires access
to the REST API using OpenID Connect; the user connecting and the client being used must have the requisite access rights. to the REST API using OpenID Connect; the user connecting and the client being used must have the requisite access rights.
In a default Keycloak installation, admin-cli and an admin user would work, as would a separate client definition with In a default Keycloak installation, admin-cli and an admin user would work, as would a separate client definition with
the scope tailored to your needs and a user having the expected roles. the scope tailored to your needs and a user having the expected roles.
@@ -36,18 +36,18 @@ options:
client_id: client_id:
description: description:
- The O(client_id) of the client. If not set the clientscope types are set as a default for the realm. - The O(client_id) of the client. If not set the client scope types are set as a default for the realm.
aliases: aliases:
- clientId - clientId
type: str type: str
default_clientscopes: default_client_scopes:
description: description:
- Client scopes that should be of type default. - Client scopes that should be of type default.
type: list type: list
elements: str elements: str
optional_clientscopes: optional_client_scopes:
description: description:
- Client scopes that should be of type optional. - Client scopes that should be of type optional.
type: list type: list
@@ -64,26 +64,26 @@ author:
EXAMPLES = r""" EXAMPLES = r"""
- name: Set default client scopes on realm level - name: Set default client scopes on realm level
middleware_automation.keycloak.keycloak_clientscope_type: middleware_automation.keycloak.keycloak_client_scope_type:
auth_client_id: admin-cli auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com auth_keycloak_url: https://auth.example.com
auth_realm: master auth_realm: master
auth_username: USERNAME auth_username: USERNAME
auth_password: PASSWORD auth_password: PASSWORD
realm: "MyCustomRealm" realm: "MyCustomRealm"
default_clientscopes: ['profile', 'roles'] default_client_scopes: ['profile', 'roles']
delegate_to: localhost delegate_to: localhost
- name: Set default and optional client scopes on client level with token auth - name: Set default and optional client scopes on client level with token auth
middleware_automation.keycloak.keycloak_clientscope_type: middleware_automation.keycloak.keycloak_client_scope_type:
auth_client_id: admin-cli auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com auth_keycloak_url: https://auth.example.com
token: TOKEN token: TOKEN
realm: "MyCustomRealm" realm: "MyCustomRealm"
client_id: "MyCustomClient" client_id: "MyCustomClient"
default_clientscopes: ['profile', 'roles'] default_client_scopes: ['profile', 'roles']
optional_clientscopes: ['phone'] optional_client_scopes: ['phone']
delegate_to: localhost delegate_to: localhost
""" """
@@ -94,16 +94,16 @@ msg:
type: str type: str
sample: "" sample: ""
proposed: proposed:
description: Representation of proposed client-scope types mapping. description: Representation of proposed client scope types mapping.
returned: always returned: always
type: dict type: dict
sample: sample:
{ {
"default_clientscopes": [ "default_client_scopes": [
"profile", "profile",
"role" "role"
], ],
"optional_clientscopes": [] "optional_client_scopes": []
} }
existing: existing:
description: description:
@@ -112,11 +112,11 @@ existing:
type: dict type: dict
sample: sample:
{ {
"default_clientscopes": [ "default_client_scopes": [
"profile", "profile",
"role" "role"
], ],
"optional_clientscopes": [ "optional_client_scopes": [
"phone" "phone"
] ]
} }
@@ -128,11 +128,11 @@ end_state:
type: dict type: dict
sample: sample:
{ {
"default_clientscopes": [ "default_client_scopes": [
"profile", "profile",
"role" "role"
], ],
"optional_clientscopes": [] "optional_client_scopes": []
} }
""" """
@@ -146,7 +146,7 @@ from ansible_collections.middleware_automation.keycloak.plugins.module_utils.ide
) )
def keycloak_clientscope_type_module(): def keycloak_client_scope_type_module():
""" """
Returns an AnsibleModule definition. Returns an AnsibleModule definition.
@@ -157,8 +157,8 @@ def keycloak_clientscope_type_module():
meta_args = dict( meta_args = dict(
realm=dict(default="master"), realm=dict(default="master"),
client_id=dict(type="str", aliases=["clientId"]), client_id=dict(type="str", aliases=["clientId"]),
default_clientscopes=dict(type="list", elements="str"), default_client_scopes=dict(type="list", elements="str"),
optional_clientscopes=dict(type="list", elements="str"), optional_client_scopes=dict(type="list", elements="str"),
) )
argument_spec.update(meta_args) argument_spec.update(meta_args)
@@ -169,7 +169,7 @@ def keycloak_clientscope_type_module():
required_one_of=( required_one_of=(
[ [
["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"], ["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"],
["default_clientscopes", "optional_clientscopes"], ["default_client_scopes", "optional_client_scopes"],
] ]
), ),
required_together=([["auth_username", "auth_password"]]), required_together=([["auth_username", "auth_password"]]),
@@ -180,21 +180,21 @@ def keycloak_clientscope_type_module():
return module return module
def clientscopes_to_add(existing, proposed): def client_scopes_to_add(existing, proposed):
to_add = [] to_add = []
existing_clientscope_ids = extract_field(existing, "id") existing_client_scope_ids = extract_field(existing, "id")
for clientscope in proposed: for client_scope in proposed:
if clientscope["id"] not in existing_clientscope_ids: if client_scope["id"] not in existing_client_scope_ids:
to_add.append(clientscope) to_add.append(client_scope)
return to_add return to_add
def clientscopes_to_delete(existing, proposed): def client_scopes_to_delete(existing, proposed):
to_delete = [] to_delete = []
proposed_clientscope_ids = extract_field(proposed, "id") proposed_client_scope_ids = extract_field(proposed, "id")
for clientscope in existing: for client_scope in existing:
if clientscope["id"] not in proposed_clientscope_ids: if client_scope["id"] not in proposed_client_scope_ids:
to_delete.append(clientscope) to_delete.append(client_scope)
return to_delete return to_delete
@@ -204,21 +204,21 @@ def extract_field(dictionary, field="name"):
def normalize_scopes(scopes): def normalize_scopes(scopes):
scopes_copy = scopes.copy() scopes_copy = scopes.copy()
if isinstance(scopes_copy.get("default_clientscopes"), list): if isinstance(scopes_copy.get("default_client_scopes"), list):
scopes_copy["default_clientscopes"] = sorted(scopes_copy["default_clientscopes"]) scopes_copy["default_client_scopes"] = sorted(scopes_copy["default_client_scopes"])
if isinstance(scopes_copy.get("optional_clientscopes"), list): if isinstance(scopes_copy.get("optional_client_scopes"), list):
scopes_copy["optional_clientscopes"] = sorted(scopes_copy["optional_clientscopes"]) scopes_copy["optional_client_scopes"] = sorted(scopes_copy["optional_client_scopes"])
return scopes_copy return scopes_copy
def main(): def main():
""" """
Module keycloak_clientscope_type Module keycloak_client_scope_type
:return: :return:
""" """
module = keycloak_clientscope_type_module() module = keycloak_client_scope_type_module()
# Obtain access token, initialize API # Obtain access token, initialize API
try: try:
@@ -230,81 +230,81 @@ def main():
realm = module.params.get("realm") realm = module.params.get("realm")
client_id = module.params.get("client_id") client_id = module.params.get("client_id")
default_clientscopes = module.params.get("default_clientscopes") default_client_scopes = module.params.get("default_client_scopes")
optional_clientscopes = module.params.get("optional_clientscopes") optional_client_scopes = module.params.get("optional_client_scopes")
result = dict(changed=False, msg="", proposed={}, existing={}, end_state={}) result = dict(changed=False, msg="", proposed={}, existing={}, end_state={})
all_clientscopes = kc.get_clientscopes(realm) all_client_scopes = kc.get_client_scopes(realm)
default_clientscopes_real = [] default_client_scopes_real = []
optional_clientscopes_real = [] optional_client_scopes_real = []
for client_scope in all_clientscopes: for client_scope in all_client_scopes:
if default_clientscopes is not None and client_scope["name"] in default_clientscopes: if default_client_scopes is not None and client_scope["name"] in default_client_scopes:
default_clientscopes_real.append(client_scope) default_client_scopes_real.append(client_scope)
if optional_clientscopes is not None and client_scope["name"] in optional_clientscopes: if optional_client_scopes is not None and client_scope["name"] in optional_client_scopes:
optional_clientscopes_real.append(client_scope) optional_client_scopes_real.append(client_scope)
if default_clientscopes is not None and len(default_clientscopes_real) != len(default_clientscopes): if default_client_scopes is not None and len(default_client_scopes_real) != len(default_client_scopes):
module.fail_json(msg="At least one of the default_clientscopes does not exist!") module.fail_json(msg="At least one of the default_client_scopes does not exist!")
if optional_clientscopes is not None and len(optional_clientscopes_real) != len(optional_clientscopes): if optional_client_scopes is not None and len(optional_client_scopes_real) != len(optional_client_scopes):
module.fail_json(msg="At least one of the optional_clientscopes does not exist!") module.fail_json(msg="At least one of the optional_client_scopes does not exist!")
result["proposed"].update( result["proposed"].update(
{ {
"default_clientscopes": "no-change" if default_clientscopes is None else default_clientscopes, "default_client_scopes": "no-change" if default_client_scopes is None else default_client_scopes,
"optional_clientscopes": "no-change" if optional_clientscopes is None else optional_clientscopes, "optional_client_scopes": "no-change" if optional_client_scopes is None else optional_client_scopes,
} }
) )
default_clientscopes_existing = kc.get_default_clientscopes(realm, client_id) default_client_scopes_existing = kc.get_default_client_scopes(realm, client_id)
optional_clientscopes_existing = kc.get_optional_clientscopes(realm, client_id) optional_client_scopes_existing = kc.get_optional_client_scopes(realm, client_id)
result["existing"].update( result["existing"].update(
{ {
"default_clientscopes": extract_field(default_clientscopes_existing), "default_client_scopes": extract_field(default_client_scopes_existing),
"optional_clientscopes": extract_field(optional_clientscopes_existing), "optional_client_scopes": extract_field(optional_client_scopes_existing),
} }
) )
if module._diff: if module._diff:
result["diff"] = dict(before=normalize_scopes(result["existing"]), after=normalize_scopes(result["proposed"])) result["diff"] = dict(before=normalize_scopes(result["existing"]), after=normalize_scopes(result["proposed"]))
default_clientscopes_add = clientscopes_to_add(default_clientscopes_existing, default_clientscopes_real) default_client_scopes_add = client_scopes_to_add(default_client_scopes_existing, default_client_scopes_real)
optional_clientscopes_add = clientscopes_to_add(optional_clientscopes_existing, optional_clientscopes_real) optional_client_scopes_add = client_scopes_to_add(optional_client_scopes_existing, optional_client_scopes_real)
default_clientscopes_delete = clientscopes_to_delete(default_clientscopes_existing, default_clientscopes_real) default_client_scopes_delete = client_scopes_to_delete(default_client_scopes_existing, default_client_scopes_real)
optional_clientscopes_delete = clientscopes_to_delete(optional_clientscopes_existing, optional_clientscopes_real) optional_client_scopes_delete = client_scopes_to_delete(optional_client_scopes_existing, optional_client_scopes_real)
result["changed"] = any( result["changed"] = any(
len(x) > 0 len(x) > 0
for x in [ for x in [
default_clientscopes_add, default_client_scopes_add,
optional_clientscopes_add, optional_client_scopes_add,
default_clientscopes_delete, default_client_scopes_delete,
optional_clientscopes_delete, optional_client_scopes_delete,
] ]
) )
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# first delete so clientscopes can change type # first delete so client_scopes can change type
for clientscope in default_clientscopes_delete: for client_scope in default_client_scopes_delete:
kc.delete_default_clientscope(clientscope["id"], realm, client_id) kc.delete_default_client_scope(client_scope["id"], realm, client_id)
for clientscope in optional_clientscopes_delete: for client_scope in optional_client_scopes_delete:
kc.delete_optional_clientscope(clientscope["id"], realm, client_id) kc.delete_optional_client_scope(client_scope["id"], realm, client_id)
for clientscope in default_clientscopes_add: for client_scope in default_client_scopes_add:
kc.add_default_clientscope(clientscope["id"], realm, client_id) kc.add_default_client_scope(client_scope["id"], realm, client_id)
for clientscope in optional_clientscopes_add: for client_scope in optional_client_scopes_add:
kc.add_optional_clientscope(clientscope["id"], realm, client_id) kc.add_optional_client_scope(client_scope["id"], realm, client_id)
result["end_state"].update( result["end_state"].update(
{ {
"default_clientscopes": extract_field(kc.get_default_clientscopes(realm, client_id)), "default_client_scopes": extract_field(kc.get_default_client_scopes(realm, client_id)),
"optional_clientscopes": extract_field(kc.get_optional_clientscopes(realm, client_id)), "optional_client_scopes": extract_field(kc.get_optional_client_scopes(realm, client_id)),
} }
) )

View File

@@ -2,4 +2,14 @@
collections: collections:
- name: middleware_automation.common - name: middleware_automation.common
version: ">=1.2.4" version: ">=1.2.4"
- name: middleware_automation.infinispan
- name: community.general
- name: ansible.posix - name: ansible.posix
- name: community.docker
version: ">=3.8.0"
- name: containers.podman
version: ">=1.8.1"
roles:
- name: elan.simple_nginx_reverse_proxy
version: "0.2.1"

View File

@@ -33,7 +33,7 @@ Role Defaults
| Variable | Description | Default | | Variable | Description | Default |
|:---------|:------------|:--------| |:---------|:------------|:--------|
|`keycloak_quarkus_version`| keycloak.org package version | `26.4.7` | |`keycloak_quarkus_version`| keycloak.org package version | `26.6.2` |
|`keycloak_quarkus_offline_install` | Perform an offline install | `False`| |`keycloak_quarkus_offline_install` | Perform an offline install | `False`|
|`keycloak_quarkus_dest`| Installation root path | `/opt/keycloak` | |`keycloak_quarkus_dest`| Installation root path | `/opt/keycloak` |
|`keycloak_quarkus_download_url` | Download URL for keycloak | `https://github.com/keycloak/keycloak/releases/download/{{ keycloak_quarkus_version }}/{{ keycloak_quarkus_archive }}` | |`keycloak_quarkus_download_url` | Download URL for keycloak | `https://github.com/keycloak/keycloak/releases/download/{{ keycloak_quarkus_version }}/{{ keycloak_quarkus_archive }}` |

View File

@@ -1,6 +1,6 @@
--- ---
### Configuration specific to keycloak ### Configuration specific to keycloak
keycloak_quarkus_version: 26.4.7 keycloak_quarkus_version: 26.6.2
keycloak_quarkus_archive: "keycloak-{{ keycloak_quarkus_version }}.zip" keycloak_quarkus_archive: "keycloak-{{ keycloak_quarkus_version }}.zip"
keycloak_quarkus_download_url: "https://github.com/keycloak/keycloak/releases/download/{{ keycloak_quarkus_version }}/{{ keycloak_quarkus_archive }}" keycloak_quarkus_download_url: "https://github.com/keycloak/keycloak/releases/download/{{ keycloak_quarkus_version }}/{{ keycloak_quarkus_archive }}"
keycloak_quarkus_installdir: "{{ keycloak_quarkus_dest }}/keycloak-{{ keycloak_quarkus_version }}" keycloak_quarkus_installdir: "{{ keycloak_quarkus_dest }}/keycloak-{{ keycloak_quarkus_version }}"

View File

@@ -2,7 +2,7 @@ argument_specs:
main: main:
options: options:
keycloak_quarkus_version: keycloak_quarkus_version:
default: "26.4.7" default: "26.6.2"
description: "keycloak.org package version" description: "keycloak.org package version"
type: "str" type: "str"
keycloak_quarkus_archive: keycloak_quarkus_archive:
@@ -519,7 +519,7 @@ argument_specs:
downstream: downstream:
options: options:
rhbk_version: rhbk_version:
default: "26.4.7" default: "26.4.11"
description: "Red Hat Build of Keycloak version" description: "Red Hat Build of Keycloak version"
type: "str" type: "str"
rhbk_archive: rhbk_archive:

View File

@@ -2,6 +2,8 @@
- name: Validate admin console password - name: Validate admin console password
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- keycloak_quarkus_bootstrap_admin_password is defined
- keycloak_quarkus_bootstrap_admin_password is not none
- keycloak_quarkus_bootstrap_admin_password | length > 12 - keycloak_quarkus_bootstrap_admin_password | length > 12
quiet: true quiet: true
fail_msg: "The console administrator password is empty or invalid. Please set the keycloak_quarkus_bootstrap_admin_password to a 12+ char long string" fail_msg: "The console administrator password is empty or invalid. Please set the keycloak_quarkus_bootstrap_admin_password to a 12+ char long string"

View File

@@ -0,0 +1,15 @@
{{ ansible_managed | comment }}
{% if not ansible_local.keycloak.general.bootstrapped | default(false) | bool %}
KC_BOOTSTRAP_ADMIN_USERNAME={{ keycloak_quarkus_bootstrap_admin_user }}
KC_BOOTSTRAP_ADMIN_PASSWORD='{{ keycloak_quarkus_bootstrap_admin_password }}'
{% else %}
{{ keycloak.bootstrap_mnemonic }}
{% endif %}
PATH="{{ keycloak_quarkus_java_home | default(keycloak_sys_pkg_java_home, true) }}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
JAVA_HOME="{{ keycloak_quarkus_java_home | default(keycloak_sys_pkg_java_home, true) }}"
JAVA_OPTS="{{ keycloak_quarkus_java_opts }}"
# Custom ENV variables
{% for env in keycloak_quarkus_additional_env_vars %}
{{ env.key }}={{ env.value }}
{% endfor %}

View File

@@ -0,0 +1,110 @@
{{ ansible_managed | comment }}
{% if keycloak_quarkus_db_enabled %}
# Database
db={{ keycloak_quarkus_db_engine }}
db-url={{ keycloak_quarkus_db_url }}
db-username={{ keycloak_quarkus_db_user }}
{% if not keycloak.config_key_store_enabled %}
db-password={{ keycloak_quarkus_db_pass }}
{% endif %}
{% endif %}
{% if keycloak.config_key_store_enabled %}
# Config store
config-keystore={{ keycloak_quarkus_config_key_store_file }}
config-keystore-password={{ keycloak_quarkus_config_key_store_password }}
{% endif %}
# Observability
metrics-enabled={{ keycloak_quarkus_metrics_enabled | lower }}
health-enabled={{ keycloak_quarkus_health_enabled | lower }}
# HTTP
http-enabled={{ keycloak_quarkus_http_enabled | lower }}
{% if keycloak_quarkus_http_enabled %}
http-port={{ keycloak_quarkus_http_port }}
{% endif %}
http-relative-path={{ keycloak_quarkus_http_relative_path }}
http-host={{ keycloak_quarkus_http_host }}
# Management
http-management-port={{ keycloak_quarkus_http_management_port }}
{% if keycloak_quarkus_http_management_relative_path is defined and keycloak_quarkus_http_management_relative_path | length > 0 %}
http-management-relative-path={{ keycloak_quarkus_http_management_relative_path }}
{% endif %}
# HTTPS
https-port={{ keycloak_quarkus_https_port }}
{% if keycloak_quarkus_https_key_file_enabled %}
https-certificate-file={{ keycloak_quarkus_cert_file}}
https-certificate-key-file={{ keycloak_quarkus_key_file }}
{% endif %}
{% if keycloak_quarkus_https_key_store_enabled %}
https-key-store-file={{ keycloak_quarkus_https_key_store_file }}
https-key-store-password={{ keycloak_quarkus_https_key_store_password }}
{% endif %}
{% if keycloak_quarkus_https_trust_store_enabled %}
https-trust-store-file={{ keycloak_quarkus_https_trust_store_file }}
https-trust-store-password={{ keycloak_quarkus_https_trust_store_password }}
{% endif %}
# Client URL configuration
hostname={{ keycloak_quarkus_hostname }}
hostname-admin={{ keycloak_quarkus_hostname_admin }}
hostname-strict={{ keycloak_quarkus_hostname_strict | lower }}
hostname-backchannel-dynamic={{ keycloak_quarkus_hostname_backchannel_dynamic | lower }}
# Cluster
{% if keycloak_quarkus_ha_enabled %}
cache=ispn
{% if keycloak_quarkus_cache_managed_infinispan_config %}
cache-config-file=cache-ispn.xml
{% endif %}
{% if keycloak_quarkus_cache_remote %}
cache-remote-username={{ keycloak_quarkus_cache_remote_username }}
cache-remote-password={{ keycloak_quarkus_cache_remote_password }}
cache-remote-host={{ keycloak_quarkus_cache_remote_host }}
cache-remote-port={{ keycloak_quarkus_cache_remote_port }}
cache-remote-tls-enabled={{ keycloak_quarkus_cache_remote_tls_enabled | lower }}
{% endif %}
{{ keycloak_quarkus_cache_embedded_properties }}
{% endif %}
{% if keycloak_quarkus_proxy_headers | length > 0 %}
proxy-headers={{ keycloak_quarkus_proxy_headers | lower }}
{% elif keycloak_quarkus_proxy_mode is defined and keycloak_quarkus_proxy_mode != "none" %}
# Deprecated Proxy configuration
proxy={{ keycloak_quarkus_proxy_mode }}
{% endif %}
spi-sticky-session-encoder-infinispan-should-attach-route={{ keycloak_quarkus_spi_sticky_session_encoder_infinispan_should_attach_route | d(true) | lower }}
# Transaction
transaction-xa-enabled={{ keycloak_quarkus_transaction_xa_enabled | lower }}
# Logging
#log-format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n
log={{ keycloak_quarkus_log }}
log-level={{ keycloak.log.level }}
log-file={{ keycloak.log.file }}
log-file-format={{ keycloak.log.format }}
# Vault
{% if keycloak_quarkus_ks_vault_enabled %}
vault=keystore
vault-file={{ keycloak_quarkus_ks_vault_file }}
vault-type={{ keycloak_quarkus_ks_vault_type }}
vault-pass={{ keycloak_quarkus_ks_vault_pass }}
{% endif %}
# Providers
{% for provider in keycloak_quarkus_providers %}
{% if provider.default is defined and provider.default %}
spi-{{ provider.spi }}-provider={{ provider.id }}
{% endif %}
{% if provider.properties is defined %}{% for property in provider.properties %}
spi-{{ provider.spi }}-{{ provider.id }}-{{ property.key }}={{ property.value }}
{% endfor %}{% endif %}
{% endfor %}

View File

@@ -0,0 +1,2 @@
[general]
bootstrapped={{ bootstrapped | lower }}

View File

@@ -0,0 +1,33 @@
{{ ansible_managed | comment }}
[Unit]
Description=Keycloak Server
After=network.target
[Service]
EnvironmentFile=-{{ keycloak_quarkus_sysconf_file }}
{% if keycloak_quarkus_start_dev %}
ExecStart={{ keycloak.home }}/bin/kc.sh start-dev
{% else %}
ExecStart={{ keycloak.home }}/bin/kc.sh start --optimized
{% endif %}
User={{ keycloak.service_user }}
Group={{ keycloak.service_group }}
SuccessExitStatus=0 143
{% if keycloak_quarkus_service_restart_always %}
Restart=always
{% elif keycloak_quarkus_service_restart_on_failure %}
Restart=on-failure
{% endif %}
RestartSec={{ keycloak_quarkus_service_restartsec }}
{% if keycloak_quarkus_http_port | int < 1024 or keycloak_quarkus_https_port | int < 1024 %}
AmbientCapabilities=CAP_NET_BIND_SERVICE
{% endif %}
{% if keycloak_quarkus_systemd_wait_for_port %}
ExecStartPost=/usr/bin/timeout {{ keycloak_quarkus_systemd_wait_for_timeout }} sh -c 'while ! ss -H -t -l -n sport = :{{ keycloak_quarkus_systemd_wait_for_port_number }} | grep -q "^LISTEN.*:{{ keycloak_quarkus_systemd_wait_for_port_number }}"; do sleep 1; done && /bin/sleep {{ keycloak_quarkus_systemd_wait_for_delay }}'
{% endif %}
{% if keycloak_quarkus_systemd_wait_for_log %}
ExecStartPost=/usr/bin/timeout {{ keycloak_quarkus_systemd_wait_for_timeout }} sh -c 'cat {{ keycloak.log.file }} | sed "/Profile.*activated/ q" && /bin/sleep {{ keycloak_quarkus_systemd_wait_for_delay }}'
{% endif %}
[Install]
WantedBy=multi-user.target