Vendor middleware_automation.keycloak collection
Needed for keycloak_authentication_v2, to manage Keycloak authentication flows (e.g. the CCCHH browser flow) as YAML. Chosen over community.general's own copy of the module (deprecated in favor of this collection, and limited to 4 sub-flow levels there vs. 10 here).
This commit is contained in:
parent
4540dc5516
commit
4832e5da5d
159 changed files with 34399 additions and 0 deletions
632
ansible_collections/middleware_automation/keycloak/CHANGELOG.rst
Normal file
632
ansible_collections/middleware_automation/keycloak/CHANGELOG.rst
Normal file
|
|
@ -0,0 +1,632 @@
|
|||
=============================================
|
||||
middleware\_automation.keycloak Release Notes
|
||||
=============================================
|
||||
|
||||
.. contents:: Topics
|
||||
|
||||
This changelog describes changes after version 0.2.6.
|
||||
|
||||
v3.0.11
|
||||
=======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add certification checks to the upstream repo `#378 <https://github.com/ansible-middleware/keycloak/pull/378>`_
|
||||
- Fix cert workflow `#389 <https://github.com/ansible-middleware/keycloak/pull/389>`_
|
||||
- Fix health check URL to include HTTP relative path `#364 <https://github.com/ansible-middleware/keycloak/pull/364>`_
|
||||
- fix(keycloak_quarkus): avoid leaking maven provider password in logs `#363 <https://github.com/ansible-middleware/keycloak/pull/363>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- AMW-592 AMW Github Actions are failing because of latest molecule `#366 <https://github.com/ansible-middleware/keycloak/pull/366>`_
|
||||
- AMW-594 Upstream release is failing due to recent rules change `#373 <https://github.com/ansible-middleware/keycloak/pull/373>`_
|
||||
|
||||
v3.0.10
|
||||
=======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Fixing linting issues `#357 <https://github.com/ansible-middleware/keycloak/pull/357>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- AMW-571 keycloak_identity_provider hide_on_login_page not working `#356 <https://github.com/ansible-middleware/keycloak/pull/356>`_
|
||||
- Fixing vars naming issue `#359 <https://github.com/ansible-middleware/keycloak/pull/359>`_
|
||||
- Removing from __future__ import annotations broke runtime type hints using Sequence and union syntax, causing sanity import failures and molecule module errors `#358 <https://github.com/ansible-middleware/keycloak/pull/358>`_
|
||||
|
||||
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
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Migrate Keycloak modules from the community.general collection to Keycloak collection. `#341 <https://github.com/ansible-middleware/keycloak/pull/341>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Fixing common module usage `#343 <https://github.com/ansible-middleware/keycloak/pull/343>`_
|
||||
- fix #336: https://github.com/ansible-middleware/common/pull/38 `#338 <https://github.com/ansible-middleware/keycloak/pull/338>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix molecule tests `#339 <https://github.com/ansible-middleware/keycloak/pull/339>`_
|
||||
|
||||
v3.0.6
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- AMW-540 Fix the upstream collection requirements with common v1.2.4 `#337 <https://github.com/ansible-middleware/keycloak/pull/337>`_
|
||||
|
||||
v3.0.5
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- AMW-528 Deployment fails in keycloak_quarkus due to missing escalation variables `#335 <https://github.com/ansible-middleware/keycloak/pull/335>`_
|
||||
|
||||
v3.0.4
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- AMW-467 Download keycloak binary from password protected HTTP location `#321 <https://github.com/ansible-middleware/keycloak/pull/321>`_
|
||||
- v26.4.x compability `#317 <https://github.com/ansible-middleware/keycloak/pull/317>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- AMW-518 Validating arguments against arg spec 'main' fails unexpectedly. `#324 <https://github.com/ansible-middleware/keycloak/pull/324>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Removing parseable from lint file as Additional properties are not allowed `#319 <https://github.com/ansible-middleware/keycloak/pull/319>`_
|
||||
|
||||
v3.0.3
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Update to keycloak 26.3.0 `#293 <https://github.com/ansible-middleware/keycloak/pull/293>`_
|
||||
- ansible-core 2.19 compatibility `#310 <https://github.com/ansible-middleware/keycloak/pull/310>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Allow to install provider jars from remote paths `#303 <https://github.com/ansible-middleware/keycloak/pull/303>`_
|
||||
- Declared proxy_mode as deprecated, updated quarkus and realm readme `#306 <https://github.com/ansible-middleware/keycloak/pull/306>`_
|
||||
- Fix config_key_store_file description to match variable name `#308 <https://github.com/ansible-middleware/keycloak/pull/308>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keycloak collection CI label is showing no status `#312 <https://github.com/ansible-middleware/keycloak/pull/312>`_
|
||||
- keycloak_realm: allow secret in keycloak_clients `#304 <https://github.com/ansible-middleware/keycloak/pull/304>`_
|
||||
|
||||
v3.0.2
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- New ``checksum`` property for keycloak_quarkus_providers `#280 <https://github.com/ansible-middleware/keycloak/pull/280>`_
|
||||
- New parameter to set the jgroups host IP address `#281 <https://github.com/ansible-middleware/keycloak/pull/281>`_
|
||||
- Session storage / distributed caches `#287 <https://github.com/ansible-middleware/keycloak/pull/287>`_
|
||||
- Update keycloak/RHBK to v26.2.4 `#283 <https://github.com/ansible-middleware/keycloak/pull/283>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix ``keycloak_quarkus_force_install`` parameter being ignored by install `#296 <https://github.com/ansible-middleware/keycloak/pull/296>`_
|
||||
- Fix alternate download location being ignored (JBossNeworkAPI always used) `#298 <https://github.com/ansible-middleware/keycloak/pull/298>`_
|
||||
- Run config rebuild after SPI providers update `#285 <https://github.com/ansible-middleware/keycloak/pull/285>`_
|
||||
- Use jdk21 as default in debian `#289 <https://github.com/ansible-middleware/keycloak/pull/289>`_
|
||||
- keycloak_realm: federation default provider type should be a string `#302 <https://github.com/ansible-middleware/keycloak/pull/302>`_
|
||||
|
||||
v3.0.1
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Version update to 26.0.8 / rhbk 26.0.11 `#277 <https://github.com/ansible-middleware/keycloak/pull/277>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Trigger rebuild handler on envvars file change `#276 <https://github.com/ansible-middleware/keycloak/pull/276>`_
|
||||
|
||||
v3.0.0
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add theme cache invalidation handler `#252 <https://github.com/ansible-middleware/keycloak/pull/252>`_
|
||||
- keycloak_realm: change url variables to defaults `#268 <https://github.com/ansible-middleware/keycloak/pull/268>`_
|
||||
|
||||
Breaking Changes / Porting Guide
|
||||
--------------------------------
|
||||
|
||||
- Bump major and ansible-core versions `#266 <https://github.com/ansible-middleware/keycloak/pull/266>`_
|
||||
- Rename parameters to follow upstream `#270 <https://github.com/ansible-middleware/keycloak/pull/270>`_
|
||||
- Update for keycloak v26 `#254 <https://github.com/ansible-middleware/keycloak/pull/254>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Access token lifespan is too short for ansible run `#251 <https://github.com/ansible-middleware/keycloak/pull/251>`_
|
||||
- Load environment vars during kc rebuild `#274 <https://github.com/ansible-middleware/keycloak/pull/274>`_
|
||||
- Rebuild config and restart service for local providers `#250 <https://github.com/ansible-middleware/keycloak/pull/250>`_
|
||||
- Rename and honour parameter ``keycloak_quarkus_http_host`` `#271 <https://github.com/ansible-middleware/keycloak/pull/271>`_
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
- middleware_automation.keycloak.keycloak_realm - Allows administration of Keycloak realm via Keycloak API
|
||||
|
||||
v2.4.3
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Update keycloak to 24.0.5 `#241 <https://github.com/ansible-middleware/keycloak/pull/241>`_
|
||||
|
||||
v2.4.2
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- New parameter ``keycloak_quarkus_download_path`` `#239 <https://github.com/ansible-middleware/keycloak/pull/239>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Add wait_for_port number parameter `#237 <https://github.com/ansible-middleware/keycloak/pull/237>`_
|
||||
|
||||
v2.4.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Internal release, documentation or test changes only.
|
||||
|
||||
v2.4.0
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Enable by default health check on restart `#234 <https://github.com/ansible-middleware/keycloak/pull/234>`_
|
||||
- Update minimum ansible-core version > 2.15 `#232 <https://github.com/ansible-middleware/keycloak/pull/232>`_
|
||||
|
||||
v2.3.0
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Allow for custom providers hosted on maven repositories `#223 <https://github.com/ansible-middleware/keycloak/pull/223>`_
|
||||
- Restart handler strategy behaviour `#231 <https://github.com/ansible-middleware/keycloak/pull/231>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add support for policy files `#225 <https://github.com/ansible-middleware/keycloak/pull/225>`_
|
||||
- Allow to add extra custom env vars in sysconfig file `#229 <https://github.com/ansible-middleware/keycloak/pull/229>`_
|
||||
- Download from alternate URL with optional http authentication `#220 <https://github.com/ansible-middleware/keycloak/pull/220>`_
|
||||
- Update Keycloak to version 24.0.4 `#218 <https://github.com/ansible-middleware/keycloak/pull/218>`_
|
||||
- ``proxy-header`` enhancement `#227 <https://github.com/ansible-middleware/keycloak/pull/227>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- ``kc.sh build`` uses configured jdk `#211 <https://github.com/ansible-middleware/keycloak/pull/211>`_
|
||||
|
||||
v2.2.2
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Copying of key material for TLS configuration `#210 <https://github.com/ansible-middleware/keycloak/pull/210>`_
|
||||
- Validate certs parameter for JDBC driver downloads `#207 <https://github.com/ansible-middleware/keycloak/pull/207>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Turn off controller privilege escalation `#209 <https://github.com/ansible-middleware/keycloak/pull/209>`_
|
||||
|
||||
v2.2.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Internal release, documentation or test changes only.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- JDBC provider: fix clause in argument validation `#204 <https://github.com/ansible-middleware/keycloak/pull/204>`_
|
||||
|
||||
v2.2.0
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Support java keystore for configuration of sensitive options `#189 <https://github.com/ansible-middleware/keycloak/pull/189>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add ``wait_for_port`` and ``wait_for_log`` systemd unit logic `#199 <https://github.com/ansible-middleware/keycloak/pull/199>`_
|
||||
- Customize jdbc driver downloads, optional authentication `#202 <https://github.com/ansible-middleware/keycloak/pull/202>`_
|
||||
- Keystore-based vault SPI configuration `#196 <https://github.com/ansible-middleware/keycloak/pull/196>`_
|
||||
- New ``keycloak_quarkus_hostname_strict_https`` parameter `#195 <https://github.com/ansible-middleware/keycloak/pull/195>`_
|
||||
- Providers config and custom providers `#201 <https://github.com/ansible-middleware/keycloak/pull/201>`_
|
||||
- Remove administrator credentials from files once keycloak is bootstrapped `#197 <https://github.com/ansible-middleware/keycloak/pull/197>`_
|
||||
- Update keycloak to 24.0 `#194 <https://github.com/ansible-middleware/keycloak/pull/194>`_
|
||||
|
||||
v2.1.2
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Internal release, documentation or test changes only.
|
||||
|
||||
v2.1.1
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add reverse ``proxy_headers`` config, supersedes ``proxy_mode`` `#187 <https://github.com/ansible-middleware/keycloak/pull/187>`_
|
||||
- Debian/Ubuntu compatibility `#178 <https://github.com/ansible-middleware/keycloak/pull/178>`_
|
||||
- Use ``keycloak_realm`` as default for sub-entities `#180 <https://github.com/ansible-middleware/keycloak/pull/180>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix permissions on controller-side downloaded artifacts `#184 <https://github.com/ansible-middleware/keycloak/pull/184>`_
|
||||
- JVM args moved to ``JAVA_OPTS`` envvar (instead of JAVA_OPTS_APPEND) `#186 <https://github.com/ansible-middleware/keycloak/pull/186>`_
|
||||
- Unrelax configuration file permissions `#191 <https://github.com/ansible-middleware/keycloak/pull/191>`_
|
||||
- Utilize comment filter for ``ansible_managed`` annotations `#176 <https://github.com/ansible-middleware/keycloak/pull/176>`_
|
||||
|
||||
v2.1.0
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Implement infinispan TCPPING discovery protocol `#159 <https://github.com/ansible-middleware/keycloak/pull/159>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Set enable-recovery when xa transactions are enabled `#167 <https://github.com/ansible-middleware/keycloak/pull/167>`_
|
||||
- keycloak_quarkus: Allow configuring log rotate options in quarkus configuration `#161 <https://github.com/ansible-middleware/keycloak/pull/161>`_
|
||||
- keycloak_quarkus: ``sticky-session`` for infinispan routes `#163 <https://github.com/ansible-middleware/keycloak/pull/163>`_
|
||||
|
||||
Breaking Changes / Porting Guide
|
||||
--------------------------------
|
||||
|
||||
- keycloak_quarkus: renamed infinispan host list configuration `#157 <https://github.com/ansible-middleware/keycloak/pull/157>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keycloak_quarkus: fix custom JAVA_HOME parameter name `#171 <https://github.com/ansible-middleware/keycloak/pull/171>`_
|
||||
|
||||
v2.0.2
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- keycloak_quarkus: Add support for sqlserver jdbc driver `#148 <https://github.com/ansible-middleware/keycloak/pull/148>`_
|
||||
- keycloak_quarkus: allow configuration of ``hostname-strict-backchannel`` `#152 <https://github.com/ansible-middleware/keycloak/pull/152>`_
|
||||
- keycloak_quarkus: systemd restart behavior `#145 <https://github.com/ansible-middleware/keycloak/pull/145>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keycloak_quarkus: Use ``keycloak_quarkus_java_opts`` `#154 <https://github.com/ansible-middleware/keycloak/pull/154>`_
|
||||
- keycloak_quarkus: allow ports <1024 (e.g. :443) in systemd unit `#150 <https://github.com/ansible-middleware/keycloak/pull/150>`_
|
||||
|
||||
v2.0.1
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- keycloak_quarkus: add hostname-strict parameter `#139 <https://github.com/ansible-middleware/keycloak/pull/139>`_
|
||||
- keycloak_quarkus: update to version 23.0.1 `#133 <https://github.com/ansible-middleware/keycloak/pull/133>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keycloak_quarkus: template requires lowercase boolean values `#138 <https://github.com/ansible-middleware/keycloak/pull/138>`_
|
||||
|
||||
v2.0.0
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add new parameter for port offset configuration `#124 <https://github.com/ansible-middleware/keycloak/pull/124>`_
|
||||
- Update Keycloak to version 22.0.5 `#122 <https://github.com/ansible-middleware/keycloak/pull/122>`_
|
||||
|
||||
Breaking Changes / Porting Guide
|
||||
--------------------------------
|
||||
|
||||
- Add support for more http-related configs `#115 <https://github.com/ansible-middleware/keycloak/pull/115>`_
|
||||
- Update minimum ansible-core version > 2.14 `#119 <https://github.com/ansible-middleware/keycloak/pull/119>`_
|
||||
- keycloak_quarkus: enable config of key store and trust store `#116 <https://github.com/ansible-middleware/keycloak/pull/116>`_
|
||||
|
||||
v1.3.0
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Run service as ``keycloak_service_user`` `#106 <https://github.com/ansible-middleware/keycloak/pull/106>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- keycloak_quarkus: Update Keycloak to version 22.0.3 `#112 <https://github.com/ansible-middleware/keycloak/pull/112>`_
|
||||
- keycloak_quarkus: fix admin console redirect when running locally `#111 <https://github.com/ansible-middleware/keycloak/pull/111>`_
|
||||
- keycloak_quarkus: skip proxy config if ``keycloak_quarkus_proxy_mode`` is ``none`` `#109 <https://github.com/ansible-middleware/keycloak/pull/109>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keycloak_quarkus: fix validation failure upon port configuration change `#113 <https://github.com/ansible-middleware/keycloak/pull/113>`_
|
||||
|
||||
v1.2.8
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- keycloak_quarkus: set openjdk 17 as default `#103 <https://github.com/ansible-middleware/keycloak/pull/103>`_
|
||||
- keycloak_quarkus: update to version 22.0.1 `#107 <https://github.com/ansible-middleware/keycloak/pull/107>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix incorrect checks for ``keycloak_jgroups_subnet`` `#98 <https://github.com/ansible-middleware/keycloak/pull/98>`_
|
||||
- Undefine ``keycloak_db_valid_conn_sql`` default `#91 <https://github.com/ansible-middleware/keycloak/pull/91>`_
|
||||
- Update bindep.txt package python3-devel to support RHEL9 `#105 <https://github.com/ansible-middleware/keycloak/pull/105>`_
|
||||
|
||||
v1.2.7
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Allow to override jgroups subnet `#93 <https://github.com/ansible-middleware/keycloak/pull/93>`_
|
||||
- keycloak-quarkus: update keycloakx to v21.1.1 `#92 <https://github.com/ansible-middleware/keycloak/pull/92>`_
|
||||
|
||||
v1.2.6
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add profile features enabling/disabling `#87 <https://github.com/ansible-middleware/keycloak/pull/87>`_
|
||||
- Improve service restart behavior configuration `#88 <https://github.com/ansible-middleware/keycloak/pull/88>`_
|
||||
- Update default xa_datasource_class value for mariadb jdbc configuration `#89 <https://github.com/ansible-middleware/keycloak/pull/89>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Handle WFLYCTL0117 when background validation millis is 0 `#90 <https://github.com/ansible-middleware/keycloak/pull/90>`_
|
||||
|
||||
v1.2.5
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add configuration for database connection pool validation `#85 <https://github.com/ansible-middleware/keycloak/pull/85>`_
|
||||
- Allow to configure administration endpoint URL `#86 <https://github.com/ansible-middleware/keycloak/pull/86>`_
|
||||
- Allow to force backend URLs to frontend URLs `#84 <https://github.com/ansible-middleware/keycloak/pull/84>`_
|
||||
- Introduce systemd unit restart behavior `#81 <https://github.com/ansible-middleware/keycloak/pull/81>`_
|
||||
|
||||
v1.2.4
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add ``sqlserver`` to keycloak role jdbc configurations `#78 <https://github.com/ansible-middleware/keycloak/pull/78>`_
|
||||
- Add configurability for XA transactions `#73 <https://github.com/ansible-middleware/keycloak/pull/73>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix deprecation warning for ``ipaddr`` `#77 <https://github.com/ansible-middleware/keycloak/pull/77>`_
|
||||
- Fix undefined facts when offline patching sso `#71 <https://github.com/ansible-middleware/keycloak/pull/71>`_
|
||||
|
||||
v1.2.1
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Allow to setup keycloak HA cluster without remote cache store `#68 <https://github.com/ansible-middleware/keycloak/pull/68>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Pass attributes to realm clients `#69 <https://github.com/ansible-middleware/keycloak/pull/69>`_
|
||||
|
||||
v1.2.0
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Provide config for multiple modcluster proxies `#60 <https://github.com/ansible-middleware/keycloak/pull/60>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Allow to configure TCPPING for cluster discovery `#62 <https://github.com/ansible-middleware/keycloak/pull/62>`_
|
||||
- Drop community.general from dependencies `#61 <https://github.com/ansible-middleware/keycloak/pull/61>`_
|
||||
- Switch middleware_automation.redhat_csp_download for middleware_automation.common `#63 <https://github.com/ansible-middleware/keycloak/pull/63>`_
|
||||
- Switch to middleware_automation.common for rh-sso patching `#64 <https://github.com/ansible-middleware/keycloak/pull/64>`_
|
||||
|
||||
v1.1.1
|
||||
======
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keycloak-quarkus: fix ``cache-config-file`` path in keycloak.conf.j2 template `#53 <https://github.com/ansible-middleware/keycloak/pull/53>`_
|
||||
|
||||
v1.1.0
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Update keycloak to 18.0.2 - sso to 7.6.1 `#46 <https://github.com/ansible-middleware/keycloak/pull/46>`_
|
||||
- Variable ``keycloak_no_log`` controls ansible ``no_log`` parameter (for debugging purposes) `#47 <https://github.com/ansible-middleware/keycloak/pull/47>`_
|
||||
- Variables to override service start retries and delay `#51 <https://github.com/ansible-middleware/keycloak/pull/51>`_
|
||||
- keycloak_quarkus: variable to enable development mode `#45 <https://github.com/ansible-middleware/keycloak/pull/45>`_
|
||||
|
||||
Breaking Changes / Porting Guide
|
||||
--------------------------------
|
||||
|
||||
- Rename variables from ``infinispan_`` prefix to ``keycloak_infinispan_`` `#42 <https://github.com/ansible-middleware/keycloak/pull/42>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keycloak_quarkus: fix /var/log/keycloak symlink to keycloak log directory `#44 <https://github.com/ansible-middleware/keycloak/pull/44>`_
|
||||
|
||||
v1.0.7
|
||||
======
|
||||
|
||||
Breaking Changes / Porting Guide
|
||||
--------------------------------
|
||||
|
||||
- keycloak_quarkus: use absolute path for certificate files `#39 <https://github.com/ansible-middleware/keycloak/pull/39>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keycloak_quarkus: use become for tasks that will otherwise fail `#38 <https://github.com/ansible-middleware/keycloak/pull/38>`_
|
||||
|
||||
v1.0.6
|
||||
======
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- keycloak_quarkus: add selected java to PATH in systemd unit `#34 <https://github.com/ansible-middleware/keycloak/pull/34>`_
|
||||
- keycloak_quarkus: set logfile path correctly under keycloak home `#35 <https://github.com/ansible-middleware/keycloak/pull/35>`_
|
||||
|
||||
v1.0.5
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Update config options: keycloak and quarkus `#32 <https://github.com/ansible-middleware/keycloak/pull/32>`_
|
||||
|
||||
v1.0.4
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Internal release, documentation or test changes only.
|
||||
|
||||
v1.0.3
|
||||
======
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- New role for installing keycloak >= 17.0.0 (quarkus) `#29 <https://github.com/ansible-middleware/keycloak/pull/29>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add ``keycloak_config_override_template`` parameter for passing a custom xml config template `#30 <https://github.com/ansible-middleware/keycloak/pull/30>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Make sure systemd unit starts with selected java JVM `#31 <https://github.com/ansible-middleware/keycloak/pull/31>`_
|
||||
|
||||
v1.0.2
|
||||
======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Make ``keycloak_admin_password`` a default with assert (was: role variable) `#26 <https://github.com/ansible-middleware/keycloak/pull/26>`_
|
||||
- Simplify dependency install logic and reduce play execution time `#19 <https://github.com/ansible-middleware/keycloak/pull/19>`_
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Set ``keycloak_frontend_url`` default according to other defaults `#25 <https://github.com/ansible-middleware/keycloak/pull/25>`_
|
||||
|
||||
v1.0.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Minor enhancements, bug and documentation fixes.
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Apply latest cumulative patch of RH-SSO automatically when new parameter ``keycloak_rhsso_apply_patches`` is ``true`` `#18 <https://github.com/ansible-middleware/keycloak/pull/18>`_
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Clustered installs now perform database initialization on first node to avoid locking issues `#17 <https://github.com/ansible-middleware/keycloak/pull/17>`_
|
||||
|
||||
v1.0.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
This is the first stable release of the ``middleware_automation.keycloak`` collection.
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
## Developing
|
||||
|
||||
### Build and install locally
|
||||
|
||||
Clone the repository, checkout the tag you want to build, or pick the main branch for the development version; then:
|
||||
|
||||
ansible-galaxy collection build .
|
||||
ansible-galaxy collection install middleware_automation-keycloak-*.tar.gz
|
||||
|
||||
|
||||
### Development environment
|
||||
|
||||
Make sure your development machine has avilable:
|
||||
|
||||
* python 3.11+
|
||||
* virtualenv
|
||||
* docker (or podman)
|
||||
|
||||
In order to run setup the development environment and run the molecule tests locally, after cloning the repository:
|
||||
|
||||
```
|
||||
# create new virtualenv using python 3
|
||||
virtualenv $PATH_TO_DEV_VIRTUALENV
|
||||
# activate the virtual env
|
||||
source $PATH_TO_DEV_VIRTUALENV/bin/activate
|
||||
# install ansible and tools onto the virtualenv
|
||||
pip install yamllint 'molecule>=6.0' 'molecule-plugins[docker]' 'ansible-core>=2.16' ansible-lint
|
||||
# install collection dependencies
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
# install python dependencies
|
||||
pip install -r requirements.txt molecule/requirements.txt
|
||||
# execute the tests (replace --all with -s subdirectory to run a single test)
|
||||
molecule test --all
|
||||
```
|
||||
|
||||
## Contributor's Guidelines
|
||||
|
||||
- All YAML files named with `.yml` extension
|
||||
- Use spaces around jinja variables. `{{ var }}` over `{{var}}`
|
||||
- Variables that are internal to the role should be lowercase and start with the role name
|
||||
- Keep roles self contained - Roles should avoid including tasks from other roles when possible
|
||||
- Plays should do nothing more than include a list of roles, except where `pre_tasks` and `post_tasks` are required, when possible
|
||||
- Separators - Use valid names, ie. underscores (e.g. `my_role` `my_playbook`) not dashes (`my-role`)
|
||||
- Paths - When defining paths, do not include trailing slashes (e.g. `my_path: /foo` not `my_path: /foo/`); when concatenating paths, follow the same convention (e.g. `{{ my_path }}/bar` not `{{ my_path }}bar`)
|
||||
- Indentation - Use 2 spaces for each indent
|
||||
- `vars/` vs `defaults/` - internal or interpolated variables that don't need to change or be overridden by user go in `vars/`, those that a user would likely override, go under `defaults/` directory
|
||||
- All role arguments have a specification in `meta/argument_specs.yml`
|
||||
- All playbooks/roles should be focused on compatibility with Ansible Automation Platform
|
||||
1342
ansible_collections/middleware_automation/keycloak/FILES.json
Normal file
1342
ansible_collections/middleware_automation/keycloak/FILES.json
Normal file
File diff suppressed because it is too large
Load diff
201
ansible_collections/middleware_automation/keycloak/LICENSE
Normal file
201
ansible_collections/middleware_automation/keycloak/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"collection_info": {
|
||||
"namespace": "middleware_automation",
|
||||
"name": "keycloak",
|
||||
"version": "3.0.11",
|
||||
"authors": [
|
||||
"Romain Pelisse <rpelisse@redhat.com>",
|
||||
"Guido Grazioli <ggraziol@redhat.com>",
|
||||
"Pavan Kumar Motaparthi <pmotapar@redhat.com>",
|
||||
"Helmut Wolf <hwo@world-direct.at>",
|
||||
"Harsha Cherukuri <hcheruku@redhat.com>"
|
||||
],
|
||||
"readme": "README.md",
|
||||
"tags": [
|
||||
"keycloak",
|
||||
"redhat",
|
||||
"rhel",
|
||||
"sso",
|
||||
"openid",
|
||||
"application",
|
||||
"identity",
|
||||
"security",
|
||||
"infrastructure",
|
||||
"authentication",
|
||||
"java",
|
||||
"runtimes",
|
||||
"middleware",
|
||||
"a4mw"
|
||||
],
|
||||
"description": "Install and configure a keycloak, or Red Hat Single Sign-on, service.",
|
||||
"license": [],
|
||||
"license_file": "LICENSE",
|
||||
"dependencies": {
|
||||
"middleware_automation.common": ">=1.2.1",
|
||||
"ansible.posix": ">=1.4.0"
|
||||
},
|
||||
"repository": "https://github.com/ansible-middleware/keycloak",
|
||||
"documentation": "https://ansible-middleware.github.io/keycloak",
|
||||
"homepage": "https://github.com/ansible-middleware/keycloak",
|
||||
"issues": "https://github.com/ansible-middleware/keycloak/issues"
|
||||
},
|
||||
"file_manifest_file": {
|
||||
"name": "FILES.json",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "2e1d81deb872cc75f3b57d336f05fe3cc7f76210e310d4e212a1565877fcdd68",
|
||||
"format": 1
|
||||
},
|
||||
"format": 1
|
||||
}
|
||||
225
ansible_collections/middleware_automation/keycloak/README.md
Normal file
225
ansible_collections/middleware_automation/keycloak/README.md
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
# Ansible Collection - middleware_automation.keycloak
|
||||
|
||||
<!--start build_status -->
|
||||
[](https://github.com/ansible-middleware/keycloak/actions/workflows/ci.yml)
|
||||
|
||||
> **_NOTE:_ If you are Red Hat customer, install `redhat.rhbk` (for Red Hat Build of Keycloak) or `redhat.sso` (for Red Hat Single Sign-On) from [Automation Hub](https://console.redhat.com/ansible/ansible-dashboard) as the certified version of this collection.**
|
||||
|
||||
<!--end build_status -->
|
||||
<!--start description -->
|
||||
Collection to install and configure [Keycloak](https://www.keycloak.org/) or [Red Hat Single Sign-On](https://access.redhat.com/products/red-hat-single-sign-on) / [Red Hat Build of Keycloak](https://access.redhat.com/products/red-hat-build-of-keycloak).
|
||||
<!--end description -->
|
||||
<!--start requires_ansible-->
|
||||
## Ansible version compatibility
|
||||
|
||||
This collection has been tested against following Ansible versions: **>=2.16.0**.
|
||||
|
||||
Plugins and modules within a collection may be tested with only specific Ansible versions. A collection may contain metadata that identifies these versions.
|
||||
<!--end requires_ansible-->
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
<!--start galaxy_download -->
|
||||
### Installing the Collection from Ansible Galaxy
|
||||
|
||||
Before using the collection, you need to install it with the Ansible Galaxy CLI:
|
||||
|
||||
ansible-galaxy collection install middleware_automation.keycloak
|
||||
|
||||
<!--end galaxy_download -->
|
||||
|
||||
You can also include it in a `requirements.yml` file and install it via `ansible-galaxy collection install -r requirements.yml`, using the format:
|
||||
|
||||
```yaml
|
||||
---
|
||||
collections:
|
||||
- name: middleware_automation.keycloak
|
||||
```
|
||||
|
||||
The keycloak collection also depends on the following python packages to be present on the controller host:
|
||||
|
||||
* netaddr
|
||||
* lxml
|
||||
|
||||
A requirement file is provided to install:
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
<!--start roles_paths -->
|
||||
### Included roles
|
||||
|
||||
* [`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`](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`](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak/README.md): role for installing legacy keycloak (<= 19.0, wildfly based).
|
||||
|
||||
<!--end roles_paths -->
|
||||
|
||||
### Included modules
|
||||
|
||||
All Keycloak administration modules from `community.general` are provided in this collection for Keycloak 17+ (Quarkus). Use `auth_keycloak_url` without the legacy `/auth` context path (for example `http://localhost:8080`). Set `keycloak_context` to `/auth` only when automating WildFly-based Keycloak with the `keycloak` role.
|
||||
|
||||
* `keycloak_authentication`: manage authentication flows and executions using Keycloak Admin REST API.
|
||||
* `keycloak_authentication_flow`: manage custom authentication flows and flow executions.
|
||||
* `keycloak_authentication_required_actions`: manage required actions available in realm authentication.
|
||||
* `keycloak_authentication_v2`: manage authentication flows with newer Keycloak API handling.
|
||||
* `keycloak_authz_authorization_scope`: manage authorization scopes for a client resource server.
|
||||
* `keycloak_authz_custom_policy`: manage custom authorization policies for a client resource server.
|
||||
* `keycloak_authz_permission`: manage authorization permissions for a client resource server.
|
||||
* `keycloak_authz_permission_info`: retrieve authorization permission information for a client resource server.
|
||||
* `keycloak_client`: manage Keycloak clients (create/update/delete).
|
||||
* `keycloak_client_rolemapping`: manage client role mappings for users and groups.
|
||||
* `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_type`: manage default and optional client scope assignments.
|
||||
* `keycloak_clientsecret_info`: retrieve client secret information.
|
||||
* `keycloak_clientsecret_regenerate`: regenerate a client secret.
|
||||
* `keycloak_clienttemplate`: manage legacy client templates.
|
||||
* `keycloak_component`: manage realm components.
|
||||
* `keycloak_component_info`: retrieve realm component information.
|
||||
* `keycloak_group`: manage realm groups and subgroups.
|
||||
* `keycloak_identity_provider`: manage identity provider instances and configuration.
|
||||
* `keycloak_realm`: manage realms (create/update/delete).
|
||||
* `keycloak_realm_info`: retrieve realm information.
|
||||
* `keycloak_realm_key`: manage realm key providers.
|
||||
* `keycloak_realm_keys_metadata_info`: retrieve realm keys metadata.
|
||||
* `keycloak_realm_localization`: manage realm localization texts.
|
||||
* `keycloak_realm_rolemapping`: manage realm role mappings for users and groups.
|
||||
* `keycloak_realm_users_info`: retrieve users from a realm.
|
||||
* `keycloak_role`: manage realm and client roles.
|
||||
* `keycloak_user`: manage users (create/update/delete).
|
||||
* `keycloak_user_execute_actions_email`: trigger execute-actions emails for users.
|
||||
* `keycloak_user_federation`: manage user federation providers (for example LDAP/AD).
|
||||
* `keycloak_user_rolemapping`: manage user role mappings.
|
||||
* `keycloak_userprofile`: manage user profile configuration.
|
||||
|
||||
## Usage
|
||||
|
||||
The collection provides roles to install Keycloak and modules to manage realms, clients, users, and related settings via the [Keycloak Admin REST API](https://www.keycloak.org/docs-api/latest/rest-api/index.html).
|
||||
|
||||
For Quarkus-based Keycloak (17+), set `auth_keycloak_url` to the server root URL without the legacy `/auth` path, for example `http://localhost:8080`. When using the legacy `keycloak` role with WildFly-based Keycloak, set `keycloak_context` to `/auth` in the `keycloak_realm` role.
|
||||
|
||||
### Install Keycloak
|
||||
|
||||
* [`playbooks/keycloak_quarkus.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/keycloak_quarkus.yml) installs Keycloak >= 17 using the `keycloak_quarkus` role.
|
||||
* [`playbooks/keycloak.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/keycloak.yml) installs legacy Keycloak (<= 19) using the `keycloak` role.
|
||||
|
||||
For full service configuration details, refer to the [keycloak_quarkus role README](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak_quarkus/README.md) or the [keycloak role README](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak/README.md).
|
||||
|
||||
#### Install from controller node (offline)
|
||||
|
||||
Making the keycloak zip archive available to the playbook working directory, and setting `keycloak_offline_install` to `true`, allows to skip
|
||||
the download tasks. The local path for the archive does match the downloaded archive path, so that it is also used as a cache when multiple hosts are provisioned in a cluster.
|
||||
|
||||
```yaml
|
||||
keycloak_offline_install: true
|
||||
```
|
||||
|
||||
|
||||
<!--start rhn_credentials -->
|
||||
<!--end rhn_credentials -->
|
||||
|
||||
|
||||
#### Install from alternate sources (like corporate Nexus, artifactory, proxy, etc)
|
||||
|
||||
It is possible to perform downloads from alternate sources, using the `keycloak_download_url` variable; make sure the final downloaded filename matches with the source filename (ie. keycloak-legacy-x.y.zip or rh-sso-x.y.z-server-dist.zip).
|
||||
|
||||
|
||||
#### Example installation command
|
||||
|
||||
Execute the following command from the source root directory:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i <ansible_hosts> playbooks/keycloak_quarkus.yml -e keycloak_quarkus_bootstrap_admin_password=<changeme>
|
||||
```
|
||||
|
||||
- `keycloak_quarkus_bootstrap_admin_password` password for the administration console user account.
|
||||
- `ansible_hosts` is the inventory, below is an example inventory for deploying to localhost
|
||||
|
||||
```
|
||||
[keycloak]
|
||||
localhost ansible_connection=local
|
||||
```
|
||||
|
||||
Note: when deploying clustered configurations, all hosts belonging to the cluster must be present in `ansible_play_batch`; ie. they must be targeted by the same ansible-playbook execution.
|
||||
|
||||
### Configure with roles
|
||||
|
||||
<!--start rhbk_realm_playbook -->
|
||||
* [`playbooks/keycloak_realm.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/keycloak_realm.yml) creates or updates provided realm, user federation(s), client(s), client role(s) and client user(s).
|
||||
<!--end rhbk_realm_playbook -->
|
||||
* [`playbooks/keycloak_realm_client.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/keycloak_realm_client.yml) creates a realm with clients, roles and users using the `keycloak_realm` role.
|
||||
* [`playbooks/keycloak_federation.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/keycloak_federation.yml) configures user federation providers.
|
||||
|
||||
#### Example configuration command
|
||||
|
||||
Execute the following command from the source root directory:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i <ansible_hosts> playbooks/keycloak_realm.yml -e keycloak_realm_admin_password=<changeme> -e keycloak_realm_realm=test
|
||||
```
|
||||
|
||||
- `keycloak_realm_admin_password` password for the administration console user account.
|
||||
- `keycloak_realm_realm` name of the realm to be created/used.
|
||||
- `ansible_hosts` is the inventory, below is an example inventory for deploying to localhost
|
||||
|
||||
```
|
||||
[keycloak]
|
||||
localhost ansible_connection=local
|
||||
```
|
||||
<!--start rhbk_realm_readme -->
|
||||
For full configuration details, refer to the [keycloak_realm role README](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak_realm/README.md).
|
||||
<!--end rhbk_realm_readme -->
|
||||
|
||||
### Configure with modules
|
||||
|
||||
Module playbooks target an already running Keycloak instance. All modules use the `middleware_automation.keycloak` collection namespace.
|
||||
|
||||
* [`playbooks/keycloak_client_scope.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/keycloak_client_scope.yml) creates a client scope with protocol mappers using the `keycloak_client_scope` module.
|
||||
* [`playbooks/keycloak_authentication_flow.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/keycloak_authentication_flow.yml) creates a custom authentication flow with execution steps using the `keycloak_authentication_flow` module.
|
||||
|
||||
Example task using shared authentication defaults:
|
||||
|
||||
```yaml
|
||||
- hosts: localhost
|
||||
module_defaults:
|
||||
group/middleware_automation.keycloak.keycloak:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: "{{ keycloak_admin_password }}"
|
||||
tasks:
|
||||
- name: Create a user in a realm
|
||||
middleware_automation.keycloak.keycloak_user:
|
||||
realm: TestRealm
|
||||
username: testuser
|
||||
first_name: Test
|
||||
last_name: User
|
||||
email: testuser@example.com
|
||||
enabled: true
|
||||
state: present
|
||||
```
|
||||
|
||||
When migrating from `community.general`, replace the collection prefix in playbooks (for example `community.general.keycloak_user` becomes `middleware_automation.keycloak.keycloak_user`) and use `keycloak_client_scope` instead of `keycloak_clientscope`.
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
<!--start support -->
|
||||
|
||||
For bug reports and feature requests, use [GitHub Issues](https://github.com/ansible-middleware/keycloak/issues).
|
||||
|
||||
<!--end support -->
|
||||
|
||||
|
||||
## Release and Upgrade Notes
|
||||
|
||||
For details on changes between versions, please see the [CHANGELOG](https://github.com/ansible-middleware/keycloak/blob/main/CHANGELOG.rst) for this collection.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Apache License v2.0 or later
|
||||
<!--start license -->
|
||||
See [LICENSE](https://github.com/ansible-middleware/keycloak/blob/main/LICENSE) to view the full text.
|
||||
<!--end license -->
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
python3-dev [compile platform:dpkg]
|
||||
python3-devel [compile platform:rpm]
|
||||
python39-devel [compile platform:centos-8 platform:rhel-8]
|
||||
git-lfs [platform:rpm platform:dpkg]
|
||||
python3-netaddr [platform:rpm platform:dpkg]
|
||||
python3-lxml [platform:rpm platform:dpkg]
|
||||
python3-jmespath [platform:rpm platform:dpkg]
|
||||
python3-requests [platform:rpm platform:dpkg]
|
||||
podman [platform:rpm platform:dpkg]
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
../CHANGELOG.rst
|
||||
|
|
@ -0,0 +1 @@
|
|||
../README.md
|
||||
|
|
@ -0,0 +1 @@
|
|||
../CONTRIBUTING.md
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
.. Red Hat middleware_automation Keycloak Ansible Collection documentation main file
|
||||
|
||||
Welcome to Keycloak Collection documentation
|
||||
============================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: User documentation
|
||||
|
||||
README
|
||||
plugins/index
|
||||
roles/index
|
||||
Changelog <CHANGELOG>
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Developer documentation
|
||||
|
||||
Developing <developing>
|
||||
Testing <testing>
|
||||
Releasing <releasing>
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Middleware collections
|
||||
|
||||
Keycloak / Red Hat Single Sign-On <https://ansible-middleware.github.io/keycloak/main/>
|
||||
Infinispan / Red Hat Data Grid <https://ansible-middleware.github.io/infinispan/main/>
|
||||
Wildfly / Red Hat JBoss EAP <https://ansible-middleware.github.io/wildfly/main/>
|
||||
Tomcat / Red Hat JWS <https://ansible-middleware.github.io/jws/main/>
|
||||
ActiveMQ / Red Hat AMQ Broker <https://ansible-middleware.github.io/amq/main/>
|
||||
Kafka / Red Hat AMQ Streams <https://ansible-middleware.github.io/amq_streams/main/>
|
||||
Ansible Middleware utilities <https://ansible-middleware.github.io/common/main/>
|
||||
JCliff <https://ansible-middleware.github.io/ansible_collections_jcliff/main/>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# Collection Versioning Strategy
|
||||
|
||||
Each supported collection maintained by Ansible follows Semantic Versioning 2.0.0 (https://semver.org/), for example:
|
||||
Given a version number MAJOR.MINOR.PATCH, the following is incremented:
|
||||
|
||||
MAJOR version: when making incompatible API changes (see Feature Release scenarios below for examples)
|
||||
|
||||
MINOR version: when adding features or functionality in a backwards compatible manner, or updating testing matrix and/or metadata (deprecation)
|
||||
|
||||
PATCH version: when adding backwards compatible bug fixes or security fixes (strict).
|
||||
|
||||
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
|
||||
|
||||
The first version of a generally available supported collection on Ansible Automation Hub shall be version 1.0.0. NOTE: By default, all newly created collections may begin with a smaller default version of 0.1.0, and therefore a version of 1.0.0 should be explicitly stated by the collection maintainer.
|
||||
|
||||
## New content is added to an existing collection
|
||||
|
||||
Assuming the current release is 1.0.0, and a new module is ready to be added to the collection, the minor version would be incremented to 1.1.0. The change in the MINOR version indicates an additive change was made while maintaining backward compatibility for existing content within the collection.
|
||||
|
||||
|
||||
## New feature to existing plugin or role within a collection (backwards compatible)
|
||||
|
||||
Assuming the current release is 1.0.0, and new features for an existing module are ready for release . We would increment the MINOR version to 1.1.0. The change in the MINOR version indicates an additive change was made while maintaining backward compatibility for existing content within the collection.
|
||||
|
||||
|
||||
## Bug fix or security fix to existing content within a collection
|
||||
|
||||
Assuming the current release is 1.0.0 and a bug is fixed prior to the next minor release, the PATCH version would be incremented to 1.0.1. The patch indicates only a bug was fixed within a current version. The PATCH release does not contain new content, nor was functionality removed. Bug fixes may be included in a MINOR or MAJOR feature release if the timing allows, eliminating the need for a PATCH dedicated to the fix.
|
||||
|
||||
|
||||
## Breaking change to any content within a collection
|
||||
|
||||
Assuming the current release is 1.0.0, and a breaking change (API or module) is introduced for a user or developer. The MAJOR version would be incremented to 2.0.0.
|
||||
|
||||
Examples of breaking changes within a collection may include but are not limited to:
|
||||
|
||||
- Argspec changes for a module that require either inventory structure or playbook changes.
|
||||
- A change in the shape of either the inbound or returned payload of a filter plugin.
|
||||
- Changes to a connection plugin that require additional inventory parameters or ansible.cfg entries.
|
||||
- New functionality added to a module that changes the outcome of that module as released in previous versions.
|
||||
- The removal of plugins from a collection.
|
||||
|
||||
|
||||
## Content removed from a collection
|
||||
|
||||
Deleting a module or API is a breaking change. Please see the 'Breaking change' section for how to version this.
|
||||
|
||||
|
||||
## A typographical error was fixed in the documentation for a collection
|
||||
|
||||
A correction to the README would be considered a bug fix and the PATCH incremented. See 'Bug fix' above.
|
||||
|
||||
|
||||
## Documentation added/removed/modified within a collection
|
||||
|
||||
Only the PATCH version should be increased for a release that contains changes limited to revised documentation.
|
||||
|
||||
|
||||
## Release automation
|
||||
|
||||
New releases are triggered by annotated git tags named after semantic versioning. The automation publishes the built artifacts to ansible-galaxy and github releases page.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
antsibull>=0.17.0
|
||||
antsibull-docs
|
||||
antsibull-changelog
|
||||
ansible-core>=2.16.0
|
||||
ansible-pygments
|
||||
sphinx-rtd-theme
|
||||
myst-parser
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Testing
|
||||
|
||||
## Continuous integration
|
||||
|
||||
The collection is tested with a [molecule](https://github.com/ansible-community/molecule) setup covering the included roles and verifying correct installation and idempotency.
|
||||
In order to run the molecule tests locally with python 3.9 available, after cloning the repository:
|
||||
The test scenarios are available on the source code repository each on his own subdirectory under [molecule/](https://github.com/ansible-middleware/keycloak/molecule).
|
||||
|
||||
|
||||
## Test playbooks
|
||||
|
||||
Sample playbooks are provided in the `playbooks/` directory; to run the playbooks locally (requires a rhel system with python 3.9+, ansible, and systemd) the steps are as follows:
|
||||
|
||||
```
|
||||
# setup environment as in developing
|
||||
# create inventory for localhost
|
||||
cat << EOF > inventory
|
||||
[keycloak]
|
||||
localhost ansible_connection=local
|
||||
EOF
|
||||
# run the playbook
|
||||
ansible-playbook -i inventory playbooks/keycloak.yml
|
||||
```
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
v3.0.11
|
||||
=======
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Add certification checks to the upstream repo #378
|
||||
- Fix cert workflow #389
|
||||
- Fix health check URL to include HTTP relative path #364
|
||||
- fix(keycloak_quarkus): avoid leaking maven provider password in logs #363
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- AMW-592 AMW Github Actions are failing because of latest molecule #366
|
||||
- AMW-594 Upstream release is failing due to recent rules change #373
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
version: 1
|
||||
build_arg_defaults:
|
||||
EE_BASE_IMAGE: 'quay.io/ansible/ansible-runner:stable-2.12-devel'
|
||||
dependencies:
|
||||
galaxy: requirements.yml
|
||||
python: requirements.txt
|
||||
system: bindep.txt
|
||||
additional_build_steps:
|
||||
append:
|
||||
- RUN alternatives --set python /usr/bin/python3
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
requires_ansible: ">=2.16.0"
|
||||
action_groups:
|
||||
keycloak:
|
||||
- keycloak_authentication
|
||||
- keycloak_authentication_flow
|
||||
- keycloak_authentication_required_actions
|
||||
- keycloak_authentication_v2
|
||||
- keycloak_authz_authorization_scope
|
||||
- keycloak_authz_custom_policy
|
||||
- keycloak_authz_permission
|
||||
- keycloak_authz_permission_info
|
||||
- keycloak_client
|
||||
- keycloak_client_rolemapping
|
||||
- keycloak_client_rolescope
|
||||
- keycloak_client_scope
|
||||
- keycloak_client_scope_type
|
||||
- keycloak_client_scope_rolemappings
|
||||
- keycloak_clientsecret_info
|
||||
- keycloak_clientsecret_regenerate
|
||||
- keycloak_clienttemplate
|
||||
- keycloak_component
|
||||
- keycloak_component_info
|
||||
- keycloak_group
|
||||
- keycloak_identity_provider
|
||||
- keycloak_organization
|
||||
- keycloak_organization_info
|
||||
- keycloak_realm
|
||||
- keycloak_realm_info
|
||||
- keycloak_realm_key
|
||||
- keycloak_realm_keys_metadata_info
|
||||
- keycloak_realm_localization
|
||||
- keycloak_realm_rolemapping
|
||||
- keycloak_realm_users_info
|
||||
- keycloak_role
|
||||
- keycloak_user
|
||||
- keycloak_user_federation
|
||||
- keycloak_user_rolemapping
|
||||
- keycloak_userprofile
|
||||
- keycloak_user_execute_actions_email
|
||||
plugin_routing:
|
||||
modules:
|
||||
keycloak_clientscope:
|
||||
redirect: middleware_automation.keycloak.keycloak_client_scope
|
||||
deprecation:
|
||||
removal_version: 5.0.0
|
||||
warning_text: >-
|
||||
The module has been renamed to keycloak_client_scope for Keycloak 17+ (Quarkus).
|
||||
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.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
- name: Playbook for Keycloak Hosts
|
||||
hosts: all
|
||||
vars:
|
||||
keycloak_admin_password: "remembertochangeme"
|
||||
roles:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
- name: Playbook for Keycloak Authentication Flow Configuration
|
||||
hosts: all
|
||||
vars:
|
||||
keycloak_admin_user: admin
|
||||
keycloak_admin_password: "remembertochangeme"
|
||||
keycloak_url: "http://localhost:8080"
|
||||
keycloak_realm: TestRealm
|
||||
tasks:
|
||||
- name: Create authentication flow with executions
|
||||
middleware_automation.keycloak.keycloak_authentication_flow:
|
||||
auth_keycloak_url: "{{ keycloak_url }}"
|
||||
auth_realm: master
|
||||
auth_username: "{{ keycloak_admin_user }}"
|
||||
auth_password: "{{ keycloak_admin_password }}"
|
||||
realm: "{{ keycloak_realm }}"
|
||||
alias: my-browser-flow
|
||||
description: "Custom browser authentication flow"
|
||||
provider_id: basic-flow
|
||||
executions:
|
||||
- provider_id: auth-cookie
|
||||
requirement: ALTERNATIVE
|
||||
- provider_id: auth-password
|
||||
requirement: REQUIRED
|
||||
- provider_id: auth-otp-form
|
||||
requirement: ALTERNATIVE
|
||||
state: present
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
- name: Playbook for Keycloak Client Scope Configuration
|
||||
hosts: all
|
||||
vars:
|
||||
keycloak_admin_user: admin
|
||||
keycloak_admin_password: "remembertochangeme"
|
||||
keycloak_url: "http://localhost:8080"
|
||||
keycloak_realm: TestRealm
|
||||
tasks:
|
||||
- name: Create client scope with protocol mappers
|
||||
middleware_automation.keycloak.keycloak_client_scope:
|
||||
auth_keycloak_url: "{{ keycloak_url }}"
|
||||
auth_realm: master
|
||||
auth_username: "{{ keycloak_admin_user }}"
|
||||
auth_password: "{{ keycloak_admin_password }}"
|
||||
realm: "{{ keycloak_realm }}"
|
||||
name: TestClientScope
|
||||
description: "Client scope created via Ansible"
|
||||
protocol: openid-connect
|
||||
protocol_mappers:
|
||||
- name: email
|
||||
protocolMapper: oidc-usermodel-attribute-mapper
|
||||
config:
|
||||
user.attribute: email
|
||||
claim.name: email
|
||||
jsonType.label: String
|
||||
id.token.claim: "true"
|
||||
access.token.claim: "true"
|
||||
userinfo.token.claim: "true"
|
||||
- name: firstName
|
||||
protocolMapper: oidc-usermodel-attribute-mapper
|
||||
config:
|
||||
user.attribute: firstName
|
||||
claim.name: given_name
|
||||
jsonType.label: String
|
||||
id.token.claim: "true"
|
||||
access.token.claim: "true"
|
||||
userinfo.token.claim: "true"
|
||||
- name: username
|
||||
protocolMapper: oidc-usermodel-attribute-mapper
|
||||
config:
|
||||
user.attribute: username
|
||||
claim.name: preferred_username
|
||||
jsonType.label: String
|
||||
id.token.claim: "true"
|
||||
access.token.claim: "true"
|
||||
userinfo.token.claim: "true"
|
||||
state: present
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
- name: Playbook for Keycloak Hosts
|
||||
hosts: all
|
||||
tasks:
|
||||
- name: Keycloak Realm Role
|
||||
ansible.builtin.include_role:
|
||||
name: keycloak_realm
|
||||
vars:
|
||||
keycloak_realm_admin_password: "remembertochangeme"
|
||||
keycloak_realm_realm: TestRealm
|
||||
keycloak_realm_user_federation:
|
||||
- realm: TestRealm
|
||||
name: my-ldap
|
||||
provider_id: ldap
|
||||
provider_type: org.keycloak.storage.UserStorageProvider
|
||||
config:
|
||||
priority: '0'
|
||||
enabled: true
|
||||
cachePolicy: DEFAULT
|
||||
batchSizeForSync: '1000'
|
||||
editMode: READ_ONLY
|
||||
importEnabled: true
|
||||
syncRegistrations: false
|
||||
vendor: other
|
||||
usernameLDAPAttribute: uid
|
||||
rdnLDAPAttribute: uid
|
||||
uuidLDAPAttribute: entryUUID
|
||||
userObjectClasses: inetOrgPerson, organizationalPerson
|
||||
connectionUrl: ldaps://ldap.example.com:636
|
||||
usersDn: ou=Users,dc=example,dc=com
|
||||
authType: simple
|
||||
bindDn: cn=directory reader
|
||||
bindCredential: password
|
||||
searchScope: '1'
|
||||
validatePasswordPolicy: false
|
||||
trustEmail: false
|
||||
useTruststoreSpi: ldapsOnly
|
||||
connectionPooling: true
|
||||
pagination: true
|
||||
allowKerberosAuthentication: false
|
||||
debug: false
|
||||
useKerberosForPasswordAuthentication: false
|
||||
mappers:
|
||||
- name: "full name"
|
||||
providerId: "full-name-ldap-mapper"
|
||||
providerType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
|
||||
config:
|
||||
ldap.full.name.attribute: cn
|
||||
read.only: true
|
||||
write.only: false
|
||||
keycloak_realm_clients:
|
||||
- name: TestClient1
|
||||
client_id: TestClient1
|
||||
roles:
|
||||
- TestClient1Admin
|
||||
- TestClient1User
|
||||
realm: "{{ keycloak_realm_realm }}"
|
||||
public_client: true
|
||||
web_origins:
|
||||
- http://testclient1origin/application
|
||||
- http://testclient1origin/other
|
||||
users:
|
||||
- username: TestUser
|
||||
password: password
|
||||
client_roles:
|
||||
- client: TestClient1
|
||||
role: TestClient1User
|
||||
realm: "{{ keycloak_realm_realm }}"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
- name: Playbook for Keycloak X Hosts with HTTPS enabled
|
||||
hosts: all
|
||||
vars:
|
||||
keycloak_quarkus_bootstrap_admin_password: "remembertochangeme"
|
||||
keycloak_quarkus_hostname: http://localhost
|
||||
keycloak_quarkus_port: 8443
|
||||
keycloak_quarkus_log: file
|
||||
keycloak_quarkus_proxy_mode: none
|
||||
roles:
|
||||
- middleware_automation.keycloak.keycloak_quarkus
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
- name: Playbook for Keycloak X Hosts in develop mode
|
||||
hosts: all
|
||||
vars:
|
||||
keycloak_quarkus_bootstrap_admin_password: "remembertochangeme"
|
||||
keycloak_quarkus_hostname: http://localhost
|
||||
keycloak_quarkus_port: 8080
|
||||
keycloak_quarkus_log: file
|
||||
keycloak_quarkus_start_dev: true
|
||||
keycloak_quarkus_proxy_mode: none
|
||||
roles:
|
||||
- middleware_automation.keycloak.keycloak_quarkus
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
- name: Playbook for Keycloak Hosts
|
||||
hosts: all
|
||||
vars:
|
||||
keycloak_realm_admin_password: "remembertochangeme"
|
||||
keycloak_realm_clients:
|
||||
- name: TestClient1
|
||||
client_id: TestClient1
|
||||
roles:
|
||||
- TestClient1Admin
|
||||
- TestClient1User
|
||||
realm: TestRealm
|
||||
public_client: true
|
||||
web_origins:
|
||||
- http://testclient1origin/application
|
||||
- http://testclient1origin/other
|
||||
users:
|
||||
- username: TestUser
|
||||
password: password
|
||||
client_roles:
|
||||
- client: TestClient1
|
||||
role: TestClient1User
|
||||
realm: TestRealm
|
||||
roles:
|
||||
- role: middleware_automation.keycloak.keycloak_realm
|
||||
keycloak_realm_realm: TestRealm
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
- name: Playbook for Keycloak Realm and Client Configuration
|
||||
hosts: all
|
||||
tasks:
|
||||
- name: Keycloak Realm Role
|
||||
ansible.builtin.include_role:
|
||||
name: middleware_automation.keycloak.keycloak_realm
|
||||
vars:
|
||||
keycloak_realm_admin_password: "remembertochangeme"
|
||||
keycloak_realm_realm: TestRealm
|
||||
keycloak_realm_client_default_roles:
|
||||
- TestRoleAdmin
|
||||
- TestRoleUser
|
||||
keycloak_realm_client_users:
|
||||
- username: TestUser
|
||||
password: password
|
||||
client_roles:
|
||||
- client: TestClient1
|
||||
role: TestRoleUser
|
||||
realm: TestRealm
|
||||
- username: TestAdmin
|
||||
password: password
|
||||
client_roles:
|
||||
- client: TestClient1
|
||||
role: TestRoleUser
|
||||
realm: TestRealm
|
||||
- client: TestClient1
|
||||
role: TestRoleAdmin
|
||||
realm: TestRealm
|
||||
keycloak_realm_clients:
|
||||
- name: TestClient1
|
||||
client_id: TestClient1
|
||||
roles: "{{ keycloak_realm_client_default_roles }}"
|
||||
realm: TestRealm
|
||||
public_client: true
|
||||
web_origins:
|
||||
- http://testclient1origin/application
|
||||
- http://testclient1origin/other
|
||||
users: "{{ keycloak_realm_client_users }}"
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
- name: Playbook for Red Hat SSO Hosts
|
||||
hosts: sso
|
||||
vars:
|
||||
keycloak_admin_password: "remembertochangeme"
|
||||
sso_enable: true
|
||||
roles:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
|
|
@ -0,0 +1 @@
|
|||
../roles
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
options: {}
|
||||
attributes:
|
||||
action_group:
|
||||
description: Use C(group/middleware_automation.keycloak.keycloak) in C(module_defaults) to set defaults for this module.
|
||||
support: full
|
||||
membership:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
'''
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
# Standard documentation fragment
|
||||
DOCUMENTATION = r'''
|
||||
options: {}
|
||||
attributes:
|
||||
check_mode:
|
||||
description: Can run in C(check_mode) and return changed status prediction without modifying target.
|
||||
diff_mode:
|
||||
description: Will return details on what has changed (or possibly needs changing in C(check_mode)), when in diff mode.
|
||||
'''
|
||||
|
||||
PLATFORM = r'''
|
||||
options: {}
|
||||
attributes:
|
||||
platform:
|
||||
description: Target OS/families that can be operated against.
|
||||
support: N/A
|
||||
'''
|
||||
|
||||
# Should be used together with the standard fragment
|
||||
INFO_MODULE = r'''
|
||||
options: {}
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
details:
|
||||
- This action does not modify state.
|
||||
diff_mode:
|
||||
support: N/A
|
||||
details:
|
||||
- This action does not modify state.
|
||||
'''
|
||||
|
||||
CONN = r'''
|
||||
options: {}
|
||||
attributes:
|
||||
become:
|
||||
description: Is usable alongside C(become) keywords.
|
||||
connection:
|
||||
description: Uses the target's configured connection information to execute code on it.
|
||||
delegation:
|
||||
description: Can be used in conjunction with C(delegate_to) and related keywords.
|
||||
'''
|
||||
|
||||
FACTS = r'''
|
||||
options: {}
|
||||
attributes:
|
||||
facts:
|
||||
description: Action returns an C(ansible_facts) dictionary that will update existing host facts.
|
||||
'''
|
||||
|
||||
# Should be used together with the standard fragment and the FACTS fragment
|
||||
FACTS_MODULE = r'''
|
||||
options: {}
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
details:
|
||||
- This action does not modify state.
|
||||
diff_mode:
|
||||
support: N/A
|
||||
details:
|
||||
- This action does not modify state.
|
||||
facts:
|
||||
support: full
|
||||
'''
|
||||
|
||||
FILES = r'''
|
||||
options: {}
|
||||
attributes:
|
||||
safe_file_operations:
|
||||
description: Uses Ansible's strict file operation functions to ensure proper permissions and avoid data corruption.
|
||||
'''
|
||||
|
||||
FLOW = r'''
|
||||
options: {}
|
||||
attributes:
|
||||
action:
|
||||
description: Indicates this has a corresponding action plugin so some parts of the options can be executed on the controller.
|
||||
async:
|
||||
description: Supports being used with the C(async) keyword.
|
||||
'''
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
# Standard documentation fragment
|
||||
DOCUMENTATION = r'''
|
||||
options:
|
||||
auth_keycloak_url:
|
||||
description:
|
||||
- URL to the Keycloak instance.
|
||||
type: str
|
||||
required: true
|
||||
aliases:
|
||||
- url
|
||||
|
||||
auth_client_id:
|
||||
description:
|
||||
- OpenID Connect I(client_id) to authenticate to the API with.
|
||||
type: str
|
||||
default: admin-cli
|
||||
|
||||
auth_realm:
|
||||
description:
|
||||
- Keycloak realm name to authenticate to for API access.
|
||||
type: str
|
||||
|
||||
auth_client_secret:
|
||||
description:
|
||||
- Client Secret to use in conjunction with I(auth_client_id) (if required).
|
||||
type: str
|
||||
|
||||
auth_username:
|
||||
description:
|
||||
- Username to authenticate for API access with.
|
||||
type: str
|
||||
aliases:
|
||||
- username
|
||||
|
||||
auth_password:
|
||||
description:
|
||||
- Password to authenticate for API access with.
|
||||
type: str
|
||||
aliases:
|
||||
- password
|
||||
|
||||
token:
|
||||
description:
|
||||
- Authentication token for Keycloak API.
|
||||
type: str
|
||||
|
||||
refresh_token:
|
||||
description:
|
||||
- Authentication refresh token for Keycloak API.
|
||||
type: str
|
||||
|
||||
validate_certs:
|
||||
description:
|
||||
- Verify TLS certificates (do not disable this in production).
|
||||
type: bool
|
||||
default: true
|
||||
|
||||
connection_timeout:
|
||||
description:
|
||||
- Controls the HTTP connections timeout period (in seconds) to Keycloak API.
|
||||
type: int
|
||||
default: 10
|
||||
http_agent:
|
||||
description:
|
||||
- Configures the HTTP User-Agent header.
|
||||
type: str
|
||||
default: Ansible
|
||||
'''
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
|
||||
# Do not use this from other collections or standalone plugins/modules!
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
import typing as t
|
||||
|
||||
|
||||
def merge_settings_without_absent_nulls(
|
||||
existing_settings: dict[str, t.Any], desired_settings: dict[str, t.Any]
|
||||
) -> dict[str, t.Any]:
|
||||
"""
|
||||
Merges existing and desired settings into a new dictionary while excluding null values in desired settings that are absent in the existing settings.
|
||||
This ensures idempotency by treating absent keys in existing settings and null values in desired settings as equivalent, preventing unnecessary updates.
|
||||
|
||||
Args:
|
||||
existing_settings (dict): Dictionary representing the current settings in Keycloak
|
||||
desired_settings (dict): Dictionary representing the desired settings
|
||||
|
||||
Returns:
|
||||
dict: A new dictionary containing all entries from existing_settings and desired_settings,
|
||||
excluding null values in desired_settings whose corresponding keys are not present in existing_settings
|
||||
"""
|
||||
|
||||
existing = existing_settings or {}
|
||||
desired = desired_settings or {}
|
||||
|
||||
return {**existing, **{k: v for k, v in desired.items() if v is not None or k in existing}}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,78 @@
|
|||
# Copyright (c) 2022, John Cant <a.johncant@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
import typing as t
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def keycloak_clientsecret_module() -> AnsibleModule:
|
||||
"""
|
||||
Returns an AnsibleModule definition for modules that interact with a client
|
||||
secret.
|
||||
|
||||
:return: argument_spec dict
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
realm=dict(default="master"),
|
||||
id=dict(type="str"),
|
||||
client_id=dict(type="str", aliases=["clientId"]),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[
|
||||
["id", "client_id"],
|
||||
["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"],
|
||||
]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
mutually_exclusive=[["token", "auth_realm"], ["token", "auth_username"], ["token", "auth_password"]],
|
||||
)
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def keycloak_clientsecret_module_resolve_params(module: AnsibleModule, kc: KeycloakAPI) -> tuple[str, dict[str, t.Any]]:
|
||||
"""
|
||||
Given an AnsibleModule definition for keycloak_clientsecret_*, and a
|
||||
KeycloakAPI client, resolve the params needed to interact with the Keycloak
|
||||
client secret, looking up the client by clientId if necessary via an API
|
||||
call.
|
||||
|
||||
:return: tuple of id, realm
|
||||
"""
|
||||
|
||||
realm = module.params.get("realm")
|
||||
id = module.params.get("id")
|
||||
client_id = module.params.get("client_id")
|
||||
|
||||
# only lookup the client_id if id isn't provided.
|
||||
# in the case that both are provided, prefer the ID, since it is one
|
||||
# less lookup.
|
||||
if id is None:
|
||||
# Due to the required_one_of spec, client_id is guaranteed to not be None
|
||||
client = kc.get_client_by_client_id(client_id, realm=realm)
|
||||
|
||||
if client is None:
|
||||
module.fail_json(msg=f"Client does not exist {client_id}")
|
||||
|
||||
id = client["id"]
|
||||
|
||||
return id, realm
|
||||
|
|
@ -0,0 +1,520 @@
|
|||
# Copyright (c) 2019, INSPQ <philippe.gauthier@inspq.qc.ca>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_authentication
|
||||
|
||||
short_description: Configure authentication in Keycloak
|
||||
|
||||
description:
|
||||
- This module actually can only make a copy of an existing authentication flow, add an execution to it and configure it.
|
||||
- It can also delete the flow.
|
||||
# Originally added in community.general 3.3.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
realm:
|
||||
description:
|
||||
- The name of the realm in which is the authentication.
|
||||
required: true
|
||||
type: str
|
||||
alias:
|
||||
description:
|
||||
- Alias for the authentication flow.
|
||||
required: true
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- Description of the flow.
|
||||
type: str
|
||||
providerId:
|
||||
description:
|
||||
- C(providerId) for the new flow when not copied from an existing flow.
|
||||
choices: ["basic-flow", "client-flow"]
|
||||
type: str
|
||||
copyFrom:
|
||||
description:
|
||||
- C(flowAlias) of the authentication flow to use for the copy.
|
||||
type: str
|
||||
authenticationExecutions:
|
||||
description:
|
||||
- Configuration structure for the executions.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
providerId:
|
||||
description:
|
||||
- C(providerID) for the new flow when not copied from an existing flow.
|
||||
type: str
|
||||
displayName:
|
||||
description:
|
||||
- Name of the execution or subflow to create or update.
|
||||
type: str
|
||||
requirement:
|
||||
description:
|
||||
- Control status of the subflow or execution.
|
||||
choices: ["REQUIRED", "ALTERNATIVE", "DISABLED", "CONDITIONAL"]
|
||||
type: str
|
||||
flowAlias:
|
||||
description:
|
||||
- Alias of parent flow.
|
||||
type: str
|
||||
authenticationConfig:
|
||||
description:
|
||||
- Describe the config of the authentication.
|
||||
type: dict
|
||||
index:
|
||||
description:
|
||||
- Priority order of the execution.
|
||||
type: int
|
||||
subFlowType:
|
||||
description:
|
||||
- For new subflows, optionally specify the type.
|
||||
- Is only used at creation.
|
||||
choices: ["basic-flow", "form-flow"]
|
||||
default: "basic-flow"
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- Control if the authentication flow must exists or not.
|
||||
choices: ["present", "absent"]
|
||||
default: present
|
||||
type: str
|
||||
force:
|
||||
type: bool
|
||||
default: false
|
||||
description:
|
||||
- If V(true), allows to remove the authentication flow and recreate it.
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Philippe Gauthier (@elfelip)
|
||||
- Gaëtan Daubresse (@Gaetan2907)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create an authentication flow from first broker login and add an execution to it.
|
||||
middleware_automation.keycloak.keycloak_authentication:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: master
|
||||
alias: "Copy of first broker login"
|
||||
copyFrom: "first broker login"
|
||||
authenticationExecutions:
|
||||
- providerId: "test-execution1"
|
||||
requirement: "REQUIRED"
|
||||
authenticationConfig:
|
||||
alias: "test.execution1.property"
|
||||
config:
|
||||
test1.property: "value"
|
||||
- providerId: "test-execution2"
|
||||
requirement: "REQUIRED"
|
||||
authenticationConfig:
|
||||
alias: "test.execution2.property"
|
||||
config:
|
||||
test2.property: "value"
|
||||
state: present
|
||||
|
||||
- name: Re-create the authentication flow
|
||||
middleware_automation.keycloak.keycloak_authentication:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: master
|
||||
alias: "Copy of first broker login"
|
||||
copyFrom: "first broker login"
|
||||
authenticationExecutions:
|
||||
- providerId: "test-provisioning"
|
||||
requirement: "REQUIRED"
|
||||
authenticationConfig:
|
||||
alias: "test.provisioning.property"
|
||||
config:
|
||||
test.provisioning.property: "value"
|
||||
state: present
|
||||
force: true
|
||||
|
||||
- name: Create an authentication flow with subflow containing an execution.
|
||||
middleware_automation.keycloak.keycloak_authentication:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: master
|
||||
alias: "Copy of first broker login"
|
||||
copyFrom: "first broker login"
|
||||
authenticationExecutions:
|
||||
- providerId: "test-execution1"
|
||||
requirement: "REQUIRED"
|
||||
- displayName: "New Subflow"
|
||||
requirement: "REQUIRED"
|
||||
- providerId: "auth-cookie"
|
||||
requirement: "REQUIRED"
|
||||
flowAlias: "New Sublow"
|
||||
state: present
|
||||
|
||||
- name: Remove authentication.
|
||||
middleware_automation.keycloak.keycloak_authentication:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: master
|
||||
alias: "Copy of first broker login"
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
end_state:
|
||||
description: Representation of the authentication after module execution.
|
||||
returned: on success
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"alias": "Copy of first broker login",
|
||||
"authenticationExecutions": [
|
||||
{
|
||||
"alias": "review profile config",
|
||||
"authenticationConfig": {
|
||||
"alias": "review profile config",
|
||||
"config": {
|
||||
"update.profile.on.first.login": "missing"
|
||||
},
|
||||
"id": "6f09e4fb-aad4-496a-b873-7fa9779df6d7"
|
||||
},
|
||||
"configurable": true,
|
||||
"displayName": "Review Profile",
|
||||
"id": "8f77dab8-2008-416f-989e-88b09ccf0b4c",
|
||||
"index": 0,
|
||||
"level": 0,
|
||||
"providerId": "idp-review-profile",
|
||||
"requirement": "REQUIRED",
|
||||
"requirementChoices": [
|
||||
"REQUIRED",
|
||||
"ALTERNATIVE",
|
||||
"DISABLED"
|
||||
]
|
||||
}
|
||||
],
|
||||
"builtIn": false,
|
||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||
"id": "bc228863-5887-4297-b898-4d988f8eaa5c",
|
||||
"providerId": "basic-flow",
|
||||
"topLevel": true
|
||||
}
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
is_struct_included,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def find_exec_in_executions(searched_exec, executions):
|
||||
"""
|
||||
Search if exec is contained in the executions.
|
||||
:param searched_exec: Execution to search for.
|
||||
:param executions: List of executions.
|
||||
:return: Index of the execution, -1 if not found..
|
||||
"""
|
||||
for i, existing_exec in enumerate(executions, start=0):
|
||||
if (
|
||||
"providerId" in existing_exec
|
||||
and "providerId" in searched_exec
|
||||
and existing_exec["providerId"] == searched_exec["providerId"]
|
||||
or "displayName" in existing_exec
|
||||
and "displayName" in searched_exec
|
||||
and existing_exec["displayName"] == searched_exec["displayName"]
|
||||
):
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def create_or_update_executions(kc, config, realm="master"):
|
||||
"""
|
||||
Create or update executions for an authentication flow.
|
||||
:param kc: Keycloak API access.
|
||||
:param config: Representation of the authentication flow including its executions.
|
||||
:param realm: Realm
|
||||
:return: tuple (changed, dict(before, after)
|
||||
WHERE
|
||||
bool changed indicates if changes have been made
|
||||
dict(str, str) shows state before and after creation/update
|
||||
"""
|
||||
try:
|
||||
changed = False
|
||||
after = ""
|
||||
before = ""
|
||||
execution = None
|
||||
if config.get("authenticationExecutions") is not None:
|
||||
# Get existing executions on the Keycloak server for this alias
|
||||
existing_executions = kc.get_executions_representation(config, realm=realm)
|
||||
for new_exec_index, new_exec in enumerate(config["authenticationExecutions"], start=0):
|
||||
if new_exec["index"] is not None:
|
||||
new_exec_index = new_exec["index"]
|
||||
exec_found = False
|
||||
# Get flowalias parent if given
|
||||
if new_exec["flowAlias"] is not None:
|
||||
flow_alias_parent = new_exec["flowAlias"]
|
||||
else:
|
||||
flow_alias_parent = config["alias"]
|
||||
# Check if same providerId or displayName name between existing and new execution
|
||||
exec_index = find_exec_in_executions(new_exec, existing_executions)
|
||||
if exec_index != -1:
|
||||
# Remove key that doesn't need to be compared with existing_exec
|
||||
exclude_key = ["flowAlias", "subFlowType"]
|
||||
for key in new_exec:
|
||||
if new_exec[key] is None:
|
||||
exclude_key.append(key)
|
||||
# Compare the executions to see if it need changes
|
||||
if (
|
||||
not is_struct_included(new_exec, existing_executions[exec_index], exclude_key)
|
||||
or exec_index != new_exec_index
|
||||
):
|
||||
exec_found = True
|
||||
if new_exec["index"] is None:
|
||||
new_exec_index = exec_index
|
||||
before += f"{existing_executions[exec_index]}\n"
|
||||
execution = existing_executions[exec_index].copy()
|
||||
# Remove exec from list in case 2 exec with same name
|
||||
existing_executions[exec_index].clear()
|
||||
elif new_exec["providerId"] is not None:
|
||||
kc.create_execution(new_exec, flowAlias=flow_alias_parent, realm=realm)
|
||||
execution = kc.get_executions_representation(config, realm=realm)[exec_index]
|
||||
exec_found = True
|
||||
exec_index = new_exec_index
|
||||
after += f"{new_exec}\n"
|
||||
elif new_exec["displayName"] is not None:
|
||||
kc.create_subflow(
|
||||
new_exec["displayName"], flow_alias_parent, realm=realm, flowType=new_exec["subFlowType"]
|
||||
)
|
||||
execution = kc.get_executions_representation(config, realm=realm)[exec_index]
|
||||
exec_found = True
|
||||
exec_index = new_exec_index
|
||||
after += f"{new_exec}\n"
|
||||
if exec_found:
|
||||
changed = True
|
||||
if exec_index != -1:
|
||||
# Update the existing execution
|
||||
updated_exec = {"id": execution["id"]}
|
||||
# add the execution configuration
|
||||
if new_exec["authenticationConfig"] is not None:
|
||||
if "authenticationConfig" in execution and "id" in execution["authenticationConfig"]:
|
||||
kc.delete_authentication_config(execution["authenticationConfig"]["id"], realm=realm)
|
||||
kc.add_authenticationConfig_to_execution(
|
||||
updated_exec["id"], new_exec["authenticationConfig"], realm=realm
|
||||
)
|
||||
for key in new_exec:
|
||||
# remove unwanted key for the next API call
|
||||
if key not in ("flowAlias", "authenticationConfig", "subFlowType"):
|
||||
updated_exec[key] = new_exec[key]
|
||||
if new_exec["requirement"] is not None:
|
||||
if "priority" in execution:
|
||||
updated_exec["priority"] = execution["priority"]
|
||||
kc.update_authentication_executions(flow_alias_parent, updated_exec, realm=realm)
|
||||
diff = exec_index - new_exec_index
|
||||
kc.change_execution_priority(updated_exec["id"], diff, realm=realm)
|
||||
after += f"{kc.get_executions_representation(config, realm=realm)[new_exec_index]}\n"
|
||||
return changed, dict(before=before, after=after)
|
||||
except Exception as e:
|
||||
kc.module.fail_json(
|
||||
msg=f"Could not create or update executions for authentication flow {config['alias']} in realm {realm}: {e}"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
realm=dict(type="str", required=True),
|
||||
alias=dict(type="str", required=True),
|
||||
providerId=dict(type="str", choices=["basic-flow", "client-flow"]),
|
||||
description=dict(type="str"),
|
||||
copyFrom=dict(type="str"),
|
||||
authenticationExecutions=dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=dict(
|
||||
providerId=dict(type="str"),
|
||||
displayName=dict(type="str"),
|
||||
requirement=dict(choices=["REQUIRED", "ALTERNATIVE", "DISABLED", "CONDITIONAL"], type="str"),
|
||||
flowAlias=dict(type="str"),
|
||||
authenticationConfig=dict(type="dict"),
|
||||
index=dict(type="int"),
|
||||
subFlowType=dict(choices=["basic-flow", "form-flow"], default="basic-flow", type="str"),
|
||||
),
|
||||
),
|
||||
state=dict(choices=["absent", "present"], default="present"),
|
||||
force=dict(type="bool", default=False),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", flow={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
state = module.params.get("state")
|
||||
force = module.params.get("force")
|
||||
|
||||
new_auth_repr = {
|
||||
"alias": module.params.get("alias"),
|
||||
"copyFrom": module.params.get("copyFrom"),
|
||||
"providerId": module.params.get("providerId"),
|
||||
"authenticationExecutions": module.params.get("authenticationExecutions"),
|
||||
"description": module.params.get("description"),
|
||||
"builtIn": module.params.get("builtIn"),
|
||||
"subflow": module.params.get("subflow"),
|
||||
}
|
||||
|
||||
auth_repr = kc.get_authentication_flow_by_alias(alias=new_auth_repr["alias"], realm=realm)
|
||||
|
||||
# Cater for when it doesn't exist (an empty dict)
|
||||
if not auth_repr:
|
||||
if state == "absent":
|
||||
# Do nothing and exit
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after="")
|
||||
result["changed"] = False
|
||||
result["end_state"] = {}
|
||||
result["msg"] = f"{new_auth_repr['alias']} absent"
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == "present":
|
||||
# Process a creation
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after=new_auth_repr)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# If copyFrom is defined, create authentication flow from a copy
|
||||
if "copyFrom" in new_auth_repr and new_auth_repr["copyFrom"] is not None:
|
||||
auth_repr = kc.copy_auth_flow(config=new_auth_repr, realm=realm)
|
||||
else: # Create an empty authentication flow
|
||||
auth_repr = kc.create_empty_auth_flow(config=new_auth_repr, realm=realm)
|
||||
|
||||
# If the authentication still not exist on the server, raise an exception.
|
||||
if auth_repr is None:
|
||||
result["msg"] = f"Authentication just created not found: {new_auth_repr}"
|
||||
module.fail_json(**result)
|
||||
|
||||
# Configure the executions for the flow
|
||||
create_or_update_executions(kc=kc, config=new_auth_repr, realm=realm)
|
||||
|
||||
# Get executions created
|
||||
exec_repr = kc.get_executions_representation(config=new_auth_repr, realm=realm)
|
||||
if exec_repr is not None:
|
||||
auth_repr["authenticationExecutions"] = exec_repr
|
||||
result["end_state"] = auth_repr
|
||||
|
||||
else:
|
||||
if state == "present":
|
||||
# Process an update
|
||||
|
||||
if force: # If force option is true
|
||||
# Delete the actual authentication flow
|
||||
result["changed"] = True
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=auth_repr, after=new_auth_repr)
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
kc.delete_authentication_flow_by_id(id=auth_repr["id"], realm=realm)
|
||||
# If copyFrom is defined, create authentication flow from a copy
|
||||
if "copyFrom" in new_auth_repr and new_auth_repr["copyFrom"] is not None:
|
||||
auth_repr = kc.copy_auth_flow(config=new_auth_repr, realm=realm)
|
||||
else: # Create an empty authentication flow
|
||||
auth_repr = kc.create_empty_auth_flow(config=new_auth_repr, realm=realm)
|
||||
# If the authentication still not exist on the server, raise an exception.
|
||||
if auth_repr is None:
|
||||
result["msg"] = f"Authentication just created not found: {new_auth_repr}"
|
||||
module.fail_json(**result)
|
||||
# Configure the executions for the flow
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
changed, diff = create_or_update_executions(kc=kc, config=new_auth_repr, realm=realm)
|
||||
result["changed"] |= changed
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = diff
|
||||
|
||||
# Get executions created
|
||||
exec_repr = kc.get_executions_representation(config=new_auth_repr, realm=realm)
|
||||
if exec_repr is not None:
|
||||
auth_repr["authenticationExecutions"] = exec_repr
|
||||
result["end_state"] = auth_repr
|
||||
|
||||
else:
|
||||
# Process a deletion (because state was not 'present')
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=auth_repr, after="")
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# delete it
|
||||
kc.delete_authentication_flow_by_id(id=auth_repr["id"], realm=realm)
|
||||
|
||||
result["msg"] = f"Authentication flow: {new_auth_repr['alias']} id: {auth_repr['id']} is deleted"
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024, Contributors to the middleware_automation.keycloak collection
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: keycloak_authentication_flow
|
||||
|
||||
short_description: Allows administration of Keycloak authentication flows via Keycloak API
|
||||
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to add, remove or modify Keycloak authentication flows via the Keycloak REST API.
|
||||
It requires access to the REST API via 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 the scope tailored
|
||||
to your needs and a user having the expected roles.
|
||||
|
||||
- This module supports creating new top-level authentication flows, copying existing flows,
|
||||
and adding execution steps to a flow.
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the authentication flow.
|
||||
- On V(present), the flow will be created if it does not yet exist.
|
||||
- On V(absent), the flow will be removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
alias:
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
- Alias (name) of the authentication flow.
|
||||
|
||||
description:
|
||||
type: str
|
||||
description:
|
||||
- Description of the authentication flow.
|
||||
default: ''
|
||||
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- The Keycloak realm under which this authentication flow resides.
|
||||
default: 'master'
|
||||
|
||||
provider_id:
|
||||
type: str
|
||||
description:
|
||||
- The provider ID for the flow.
|
||||
default: 'basic-flow'
|
||||
aliases:
|
||||
- providerId
|
||||
|
||||
copy_from:
|
||||
type: str
|
||||
description:
|
||||
- If set, the new flow is created as a copy of the flow with this alias.
|
||||
- Cannot be used together with O(executions).
|
||||
aliases:
|
||||
- copyFrom
|
||||
|
||||
executions:
|
||||
type: list
|
||||
elements: dict
|
||||
description:
|
||||
- A list of executions (authenticator steps) to add to the flow.
|
||||
- Each execution is a dict with keys C(provider_id) (or C(providerId)) and C(requirement).
|
||||
- Executions are only added when the flow is first created.
|
||||
default: []
|
||||
suboptions:
|
||||
provider_id:
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
- The authenticator provider ID (e.g. V(auth-cookie), V(auth-password), V(auth-otp-form)).
|
||||
aliases:
|
||||
- providerId
|
||||
requirement:
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
- The requirement level for this execution.
|
||||
choices:
|
||||
- REQUIRED
|
||||
- ALTERNATIVE
|
||||
- DISABLED
|
||||
- CONDITIONAL
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Paulo Menon (@paulomenon)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create an authentication flow with executions
|
||||
middleware_automation.keycloak.keycloak_authentication_flow:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: TestRealm
|
||||
alias: my-browser-flow
|
||||
description: "Custom browser flow"
|
||||
provider_id: basic-flow
|
||||
executions:
|
||||
- provider_id: auth-cookie
|
||||
requirement: ALTERNATIVE
|
||||
- provider_id: auth-password
|
||||
requirement: REQUIRED
|
||||
- provider_id: auth-otp-form
|
||||
requirement: ALTERNATIVE
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create an authentication flow by copying an existing one
|
||||
middleware_automation.keycloak.keycloak_authentication_flow:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: TestRealm
|
||||
alias: my-copy-of-browser
|
||||
copy_from: browser
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a flow using token authentication
|
||||
middleware_automation.keycloak.keycloak_authentication_flow:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
token: MY_TOKEN
|
||||
realm: TestRealm
|
||||
alias: my-flow
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete an authentication flow
|
||||
middleware_automation.keycloak.keycloak_authentication_flow:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: TestRealm
|
||||
alias: my-browser-flow
|
||||
state: absent
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Authentication flow my-browser-flow has been created"
|
||||
|
||||
end_state:
|
||||
description: Representation of the authentication flow after module execution.
|
||||
returned: on success
|
||||
type: dict
|
||||
sample: {
|
||||
"id": "uuid-here",
|
||||
"alias": "my-browser-flow",
|
||||
"providerId": "basic-flow",
|
||||
"topLevel": true,
|
||||
"builtIn": false
|
||||
}
|
||||
'''
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \
|
||||
keycloak_argument_spec, get_token, KeycloakError
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
execution_spec = dict(
|
||||
provider_id=dict(type='str', required=True, aliases=['providerId']),
|
||||
requirement=dict(type='str', required=True, choices=['REQUIRED', 'ALTERNATIVE', 'DISABLED', 'CONDITIONAL']),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
alias=dict(type='str', required=True),
|
||||
description=dict(type='str', default=''),
|
||||
realm=dict(type='str', default='master'),
|
||||
provider_id=dict(type='str', default='basic-flow', aliases=['providerId']),
|
||||
copy_from=dict(type='str', aliases=['copyFrom']),
|
||||
executions=dict(type='list', default=[], options=execution_spec, elements='dict'),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=([['token', 'auth_realm', 'auth_username', 'auth_password']]),
|
||||
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
|
||||
mutually_exclusive=[['copy_from', 'executions']])
|
||||
|
||||
result = dict(changed=False, msg='', diff={}, end_state={})
|
||||
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get('realm')
|
||||
alias = module.params.get('alias')
|
||||
state = module.params.get('state')
|
||||
description = module.params.get('description')
|
||||
provider_id = module.params.get('provider_id')
|
||||
copy_from = module.params.get('copy_from')
|
||||
executions = module.params.get('executions')
|
||||
|
||||
before_flow = kc.get_authentication_flow_by_alias(alias, realm=realm)
|
||||
flow_exists = bool(before_flow)
|
||||
|
||||
if state == 'absent':
|
||||
if flow_exists:
|
||||
result['changed'] = True
|
||||
if module._diff:
|
||||
result['diff'] = dict(before=before_flow, after='')
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
kc.delete_authentication_flow_by_id(before_flow['id'], realm=realm)
|
||||
result['msg'] = "Authentication flow {alias} has been deleted".format(alias=alias)
|
||||
else:
|
||||
result['msg'] = "Authentication flow {alias} does not exist, doing nothing".format(alias=alias)
|
||||
result['end_state'] = {}
|
||||
module.exit_json(**result)
|
||||
|
||||
if flow_exists:
|
||||
result['changed'] = False
|
||||
result['end_state'] = before_flow
|
||||
result['msg'] = "Authentication flow {alias} already exists".format(alias=alias)
|
||||
module.exit_json(**result)
|
||||
|
||||
result['changed'] = True
|
||||
|
||||
flow_config = {
|
||||
'alias': alias,
|
||||
'description': description,
|
||||
'providerId': provider_id,
|
||||
}
|
||||
|
||||
if module._diff:
|
||||
result['diff'] = dict(before='', after=flow_config)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
if copy_from:
|
||||
flow_config['copyFrom'] = copy_from
|
||||
after_flow = kc.copy_auth_flow(flow_config, realm=realm)
|
||||
result['msg'] = "Authentication flow {alias} has been created (copied from {src})".format(alias=alias, src=copy_from)
|
||||
else:
|
||||
after_flow = kc.create_empty_auth_flow(flow_config, realm=realm)
|
||||
|
||||
if executions:
|
||||
for execution in executions:
|
||||
exec_rep = {
|
||||
'providerId': execution['provider_id'],
|
||||
'requirement': execution['requirement'],
|
||||
}
|
||||
kc.create_execution(exec_rep, alias, realm=realm)
|
||||
|
||||
result['msg'] = "Authentication flow {alias} has been created".format(alias=alias)
|
||||
|
||||
after_flow = kc.get_authentication_flow_by_alias(alias, realm=realm)
|
||||
result['end_state'] = after_flow
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,465 @@
|
|||
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
# Copyright (c) 2021, Christophe Gilles <christophe.gilles54@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
|
||||
# https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_authentication_required_actions
|
||||
|
||||
short_description: Allows administration of Keycloak authentication required actions
|
||||
|
||||
description:
|
||||
- This module can register, update and delete required actions.
|
||||
- It also filters out any duplicate required actions by their alias. The first occurrence is preserved.
|
||||
# Originally added in community.general 7.1.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
realm:
|
||||
description:
|
||||
- The name of the realm in which are the authentication required actions.
|
||||
required: true
|
||||
type: str
|
||||
required_actions:
|
||||
elements: dict
|
||||
description:
|
||||
- Authentication required action.
|
||||
suboptions:
|
||||
alias:
|
||||
description:
|
||||
- Unique name of the required action.
|
||||
required: true
|
||||
type: str
|
||||
config:
|
||||
description:
|
||||
- Configuration for the required action.
|
||||
type: dict
|
||||
defaultAction:
|
||||
description:
|
||||
- Indicates whether new users have the required action assigned to them.
|
||||
type: bool
|
||||
enabled:
|
||||
description:
|
||||
- Indicates, if the required action is enabled or not.
|
||||
type: bool
|
||||
name:
|
||||
description:
|
||||
- Displayed name of the required action. Required for registration.
|
||||
type: str
|
||||
priority:
|
||||
description:
|
||||
- Priority of the required action.
|
||||
type: int
|
||||
providerId:
|
||||
description:
|
||||
- Provider ID of the required action. Required for registration.
|
||||
type: str
|
||||
type: list
|
||||
state:
|
||||
choices: ["absent", "present"]
|
||||
description:
|
||||
- Control if the realm authentication required actions are going to be registered/updated (V(present)) or deleted (V(absent)).
|
||||
required: true
|
||||
type: str
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Skrekulko (@Skrekulko)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Register a new required action.
|
||||
middleware_automation.keycloak.keycloak_authentication_required_actions:
|
||||
auth_client_id: "admin-cli"
|
||||
auth_keycloak_url: "http://localhost:8080"
|
||||
auth_password: "password"
|
||||
auth_realm: "master"
|
||||
auth_username: "admin"
|
||||
realm: "master"
|
||||
required_actions:
|
||||
- alias: "TERMS_AND_CONDITIONS"
|
||||
name: "Terms and conditions"
|
||||
providerId: "TERMS_AND_CONDITIONS"
|
||||
enabled: true
|
||||
state: "present"
|
||||
|
||||
- name: Update the newly registered required action.
|
||||
middleware_automation.keycloak.keycloak_authentication_required_actions:
|
||||
auth_client_id: "admin-cli"
|
||||
auth_keycloak_url: "http://localhost:8080"
|
||||
auth_password: "password"
|
||||
auth_realm: "master"
|
||||
auth_username: "admin"
|
||||
realm: "master"
|
||||
required_actions:
|
||||
- alias: "TERMS_AND_CONDITIONS"
|
||||
enabled: false
|
||||
state: "present"
|
||||
|
||||
- name: Delete the updated registered required action.
|
||||
middleware_automation.keycloak.keycloak_authentication_required_actions:
|
||||
auth_client_id: "admin-cli"
|
||||
auth_keycloak_url: "http://localhost:8080"
|
||||
auth_password: "password"
|
||||
auth_realm: "master"
|
||||
auth_username: "admin"
|
||||
realm: "master"
|
||||
required_actions:
|
||||
- alias: "TERMS_AND_CONDITIONS"
|
||||
state: "absent"
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
end_state:
|
||||
description: Representation of the authentication required actions after module execution.
|
||||
returned: on success
|
||||
type: complex
|
||||
contains:
|
||||
alias:
|
||||
description:
|
||||
- Unique name of the required action.
|
||||
sample: test-provider-id
|
||||
type: str
|
||||
config:
|
||||
description:
|
||||
- Configuration for the required action.
|
||||
sample: {}
|
||||
type: dict
|
||||
defaultAction:
|
||||
description:
|
||||
- Indicates whether new users have the required action assigned to them.
|
||||
sample: false
|
||||
type: bool
|
||||
enabled:
|
||||
description:
|
||||
- Indicates, if the required action is enabled or not.
|
||||
sample: false
|
||||
type: bool
|
||||
name:
|
||||
description:
|
||||
- Displayed name of the required action. Required for registration.
|
||||
sample: Test provider ID
|
||||
type: str
|
||||
priority:
|
||||
description:
|
||||
- Priority of the required action.
|
||||
sample: 90
|
||||
type: int
|
||||
providerId:
|
||||
description:
|
||||
- Provider ID of the required action. Required for registration.
|
||||
sample: test-provider-id
|
||||
type: str
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def sanitize_required_actions(objects):
|
||||
for obj in objects:
|
||||
alias = obj["alias"]
|
||||
name = obj["name"]
|
||||
provider_id = obj["providerId"]
|
||||
|
||||
if not name:
|
||||
obj["name"] = alias
|
||||
|
||||
if provider_id != alias:
|
||||
obj["providerId"] = alias
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
def filter_duplicates(objects):
|
||||
filtered_objects = {}
|
||||
|
||||
for obj in objects:
|
||||
alias = obj["alias"]
|
||||
|
||||
if alias not in filtered_objects:
|
||||
filtered_objects[alias] = obj
|
||||
|
||||
return list(filtered_objects.values())
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
realm=dict(type="str", required=True),
|
||||
required_actions=dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=dict(
|
||||
alias=dict(type="str", required=True),
|
||||
config=dict(type="dict"),
|
||||
defaultAction=dict(type="bool"),
|
||||
enabled=dict(type="bool"),
|
||||
name=dict(type="str"),
|
||||
priority=dict(type="int"),
|
||||
providerId=dict(type="str"),
|
||||
),
|
||||
),
|
||||
state=dict(type="str", choices=["present", "absent"], required=True),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", end_state={}, diff=dict(before={}, after={}))
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
# Convenience variables
|
||||
realm = module.params.get("realm")
|
||||
desired_required_actions = module.params.get("required_actions")
|
||||
state = module.params.get("state")
|
||||
|
||||
# Sanitize required actions
|
||||
desired_required_actions = sanitize_required_actions(desired_required_actions)
|
||||
|
||||
# Filter out duplicate required actions
|
||||
desired_required_actions = filter_duplicates(desired_required_actions)
|
||||
|
||||
# Get required actions
|
||||
before_required_actions = kc.get_required_actions(realm=realm)
|
||||
|
||||
if state == "present":
|
||||
# Initialize empty lists to hold the required actions that need to be
|
||||
# registered, updated, and original ones of the updated one
|
||||
register_required_actions = []
|
||||
before_updated_required_actions = []
|
||||
updated_required_actions = []
|
||||
|
||||
# Loop through the desired required actions and check if they exist in the before required actions
|
||||
for desired_required_action in desired_required_actions:
|
||||
found = False
|
||||
|
||||
# Loop through the before required actions and check if the aliases match
|
||||
for before_required_action in before_required_actions:
|
||||
if desired_required_action["alias"] == before_required_action["alias"]:
|
||||
update_required = False
|
||||
|
||||
# Fill in the parameters
|
||||
for k, v in before_required_action.items():
|
||||
if k not in desired_required_action or desired_required_action[k] is None:
|
||||
desired_required_action[k] = v
|
||||
|
||||
# Loop through the keys of the desired and before required actions
|
||||
# and check if there are any differences between them
|
||||
for key in desired_required_action.keys():
|
||||
if (
|
||||
key in before_required_action
|
||||
and desired_required_action[key] != before_required_action[key]
|
||||
):
|
||||
update_required = True
|
||||
break
|
||||
|
||||
# If there are differences, add the before and desired required actions
|
||||
# to their respective lists for updating
|
||||
if update_required:
|
||||
before_updated_required_actions.append(before_required_action)
|
||||
updated_required_actions.append(desired_required_action)
|
||||
found = True
|
||||
break
|
||||
# If the desired required action is not found in the before required actions,
|
||||
# add it to the list of required actions to register
|
||||
if not found:
|
||||
# Check if name is provided
|
||||
if "name" not in desired_required_action or desired_required_action["name"] is None:
|
||||
module.fail_json(
|
||||
msg=f"Unable to register required action {desired_required_action['alias']} in realm {realm}: name not included"
|
||||
)
|
||||
|
||||
# Check if provider ID is provided
|
||||
if "providerId" not in desired_required_action or desired_required_action["providerId"] is None:
|
||||
module.fail_json(
|
||||
msg=f"Unable to register required action {desired_required_action['alias']} in realm {realm}: providerId not included"
|
||||
)
|
||||
|
||||
register_required_actions.append(desired_required_action)
|
||||
|
||||
# Handle diff
|
||||
if module._diff:
|
||||
diff_required_actions = updated_required_actions.copy()
|
||||
diff_required_actions.extend(register_required_actions)
|
||||
|
||||
result["diff"] = dict(before=before_updated_required_actions, after=diff_required_actions)
|
||||
|
||||
# Handle changed
|
||||
if register_required_actions or updated_required_actions:
|
||||
result["changed"] = True
|
||||
|
||||
# Handle check mode
|
||||
if module.check_mode:
|
||||
if register_required_actions or updated_required_actions:
|
||||
result["change"] = True
|
||||
result["msg"] = "Required actions would be registered/updated"
|
||||
else:
|
||||
result["change"] = False
|
||||
result["msg"] = "Required actions would not be registered/updated"
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
# Register required actions
|
||||
if register_required_actions:
|
||||
for register_required_action in register_required_actions:
|
||||
kc.register_required_action(realm=realm, rep=register_required_action)
|
||||
kc.update_required_action(
|
||||
alias=register_required_action["alias"], realm=realm, rep=register_required_action
|
||||
)
|
||||
|
||||
# Update required actions
|
||||
if updated_required_actions:
|
||||
for updated_required_action in updated_required_actions:
|
||||
kc.update_required_action(
|
||||
alias=updated_required_action["alias"], realm=realm, rep=updated_required_action
|
||||
)
|
||||
|
||||
# Initialize the final list of required actions
|
||||
final_required_actions = []
|
||||
|
||||
# Iterate over the before_required_actions
|
||||
for before_required_action in before_required_actions:
|
||||
# Check if there is an updated_required_action with the same alias
|
||||
updated_required_action_found = False
|
||||
|
||||
for updated_required_action in updated_required_actions:
|
||||
if updated_required_action["alias"] == before_required_action["alias"]:
|
||||
# Merge the two dictionaries, favoring the values from updated_required_action
|
||||
merged_dict = {}
|
||||
for key in before_required_action.keys():
|
||||
if key in updated_required_action:
|
||||
merged_dict[key] = updated_required_action[key]
|
||||
else:
|
||||
merged_dict[key] = before_required_action[key]
|
||||
|
||||
for key in updated_required_action.keys():
|
||||
if key not in before_required_action:
|
||||
merged_dict[key] = updated_required_action[key]
|
||||
|
||||
# Add the merged dictionary to the final list of required actions
|
||||
final_required_actions.append(merged_dict)
|
||||
|
||||
# Mark the updated_required_action as found
|
||||
updated_required_action_found = True
|
||||
|
||||
# Stop looking for updated_required_action
|
||||
break
|
||||
|
||||
# If no matching updated_required_action was found, add the before_required_action to the final list of required actions
|
||||
if not updated_required_action_found:
|
||||
final_required_actions.append(before_required_action)
|
||||
|
||||
# Append any remaining updated_required_actions that were not merged
|
||||
for updated_required_action in updated_required_actions:
|
||||
if not any(updated_required_action["alias"] == action["alias"] for action in final_required_actions):
|
||||
final_required_actions.append(updated_required_action)
|
||||
|
||||
# Append newly registered required actions
|
||||
final_required_actions.extend(register_required_actions)
|
||||
|
||||
# Handle message and end state
|
||||
result["msg"] = "Required actions registered/updated"
|
||||
result["end_state"] = final_required_actions
|
||||
else:
|
||||
# Filter out the deleted required actions
|
||||
final_required_actions = []
|
||||
delete_required_actions = []
|
||||
|
||||
for before_required_action in before_required_actions:
|
||||
delete_action = False
|
||||
|
||||
for desired_required_action in desired_required_actions:
|
||||
if before_required_action["alias"] == desired_required_action["alias"]:
|
||||
delete_action = True
|
||||
break
|
||||
|
||||
if not delete_action:
|
||||
final_required_actions.append(before_required_action)
|
||||
else:
|
||||
delete_required_actions.append(before_required_action)
|
||||
|
||||
# Handle diff
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_required_actions, after=final_required_actions)
|
||||
|
||||
# Handle changed
|
||||
if delete_required_actions:
|
||||
result["changed"] = True
|
||||
|
||||
# Handle check mode
|
||||
if module.check_mode:
|
||||
if final_required_actions:
|
||||
result["change"] = True
|
||||
result["msg"] = "Required actions would be deleted"
|
||||
else:
|
||||
result["change"] = False
|
||||
result["msg"] = "Required actions would not be deleted"
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
# Delete required actions
|
||||
if delete_required_actions:
|
||||
for delete_required_action in delete_required_actions:
|
||||
kc.delete_required_action(alias=delete_required_action["alias"], realm=realm)
|
||||
|
||||
# Handle message and end state
|
||||
result["msg"] = "Required actions deleted"
|
||||
result["end_state"] = final_required_actions
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,282 @@
|
|||
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
# Copyright (c) 2021, Christophe Gilles <christophe.gilles54@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
|
||||
# https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_authz_authorization_scope
|
||||
|
||||
short_description: Allows administration of Keycloak client authorization scopes using Keycloak API
|
||||
|
||||
# Originally added in community.general 6.6.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows the administration of Keycloak client Authorization Scopes using the Keycloak REST API. Authorization
|
||||
Scopes are only available if a client has Authorization enabled.
|
||||
- This module requires access to the REST API using OpenID Connect; the user connecting and the realm 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
|
||||
realm definition with the scope tailored to your needs and a user having the expected roles.
|
||||
- The names of module options are snake_cased versions of the camelCase options used by Keycloak. The Authorization Services
|
||||
paths and payloads have not officially been documented by the Keycloak project.
|
||||
U(https://www.puppeteers.net/blog/keycloak-authorization-services-rest-api-paths-and-payload/).
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the authorization scope.
|
||||
- On V(present), the authorization scope is created (or updated if it exists already).
|
||||
- On V(absent), the authorization scope is removed if it exists.
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Name of the authorization scope to create.
|
||||
type: str
|
||||
required: true
|
||||
display_name:
|
||||
description:
|
||||
- The display name of the authorization scope.
|
||||
type: str
|
||||
icon_uri:
|
||||
description:
|
||||
- The icon URI for the authorization scope.
|
||||
type: str
|
||||
client_id:
|
||||
description:
|
||||
- The C(clientId) of the Keycloak client that should have the authorization scope.
|
||||
- This is usually a human-readable name of the Keycloak client.
|
||||
type: str
|
||||
required: true
|
||||
realm:
|
||||
description:
|
||||
- The name of the Keycloak realm the Keycloak client is in.
|
||||
type: str
|
||||
required: true
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Samuli Seppänen (@mattock)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Manage Keycloak file:delete authorization scope
|
||||
keycloak_authz_authorization_scope:
|
||||
name: file:delete
|
||||
state: present
|
||||
display_name: File delete
|
||||
client_id: myclient
|
||||
realm: myrealm
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: keycloak
|
||||
auth_password: keycloak
|
||||
auth_realm: master
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
end_state:
|
||||
description: Representation of the authorization scope after module execution.
|
||||
returned: on success
|
||||
type: complex
|
||||
contains:
|
||||
id:
|
||||
description: ID of the authorization scope.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: a6ab1cf2-1001-40ec-9f39-48f23b6a0a41
|
||||
name:
|
||||
description: Name of the authorization scope.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: file:delete
|
||||
display_name:
|
||||
description: Display name of the authorization scope.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: File delete
|
||||
icon_uri:
|
||||
description: Icon URI for the authorization scope.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: http://localhost/icon.png
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
name=dict(type="str", required=True),
|
||||
display_name=dict(type="str"),
|
||||
icon_uri=dict(type="str"),
|
||||
client_id=dict(type="str", required=True),
|
||||
realm=dict(type="str", required=True),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", end_state={}, diff=dict(before={}, after={}))
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
# Convenience variables
|
||||
state = module.params.get("state")
|
||||
name = module.params.get("name")
|
||||
display_name = module.params.get("display_name")
|
||||
icon_uri = module.params.get("icon_uri")
|
||||
client_id = module.params.get("client_id")
|
||||
realm = module.params.get("realm")
|
||||
|
||||
# Get the "id" of the client based on the usually more human-readable
|
||||
# "clientId"
|
||||
cid = kc.get_client_id(client_id, realm=realm)
|
||||
if not cid:
|
||||
module.fail_json(msg=f"Invalid client {client_id} for realm {realm}")
|
||||
|
||||
# Get current state of the Authorization Scope using its name as the search
|
||||
# filter. This returns False if it is not found.
|
||||
before_authz_scope = kc.get_authz_authorization_scope_by_name(name=name, client_id=cid, realm=realm)
|
||||
|
||||
# Generate a JSON payload for Keycloak Admin API. This is needed for
|
||||
# "create" and "update" operations.
|
||||
desired_authz_scope = {}
|
||||
desired_authz_scope["name"] = name
|
||||
desired_authz_scope["displayName"] = display_name
|
||||
desired_authz_scope["iconUri"] = icon_uri
|
||||
|
||||
# Add "id" to payload for modify operations
|
||||
if before_authz_scope:
|
||||
desired_authz_scope["id"] = before_authz_scope["id"]
|
||||
|
||||
# Ensure that undefined (null) optional parameters are presented as empty
|
||||
# strings in the desired state. This makes comparisons with current state
|
||||
# much easier.
|
||||
for k, v in desired_authz_scope.items():
|
||||
if not v:
|
||||
desired_authz_scope[k] = ""
|
||||
|
||||
# Do the above for the current state
|
||||
if before_authz_scope:
|
||||
for k in ["displayName", "iconUri"]:
|
||||
if k not in before_authz_scope:
|
||||
before_authz_scope[k] = ""
|
||||
|
||||
if before_authz_scope and state == "present":
|
||||
changes = False
|
||||
for k, v in desired_authz_scope.items():
|
||||
if before_authz_scope[k] != v:
|
||||
changes = True
|
||||
# At this point we know we have to update the object anyways,
|
||||
# so there's no need to do more work.
|
||||
break
|
||||
|
||||
if changes:
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_authz_scope, after=desired_authz_scope)
|
||||
|
||||
if module.check_mode:
|
||||
result["changed"] = True
|
||||
result["msg"] = "Authorization scope would be updated"
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
kc.update_authz_authorization_scope(
|
||||
payload=desired_authz_scope, id=before_authz_scope["id"], client_id=cid, realm=realm
|
||||
)
|
||||
result["changed"] = True
|
||||
result["msg"] = "Authorization scope updated"
|
||||
else:
|
||||
result["changed"] = False
|
||||
result["msg"] = "Authorization scope not updated"
|
||||
|
||||
result["end_state"] = desired_authz_scope
|
||||
elif not before_authz_scope and state == "present":
|
||||
if module._diff:
|
||||
result["diff"] = dict(before={}, after=desired_authz_scope)
|
||||
|
||||
if module.check_mode:
|
||||
result["changed"] = True
|
||||
result["msg"] = "Authorization scope would be created"
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
kc.create_authz_authorization_scope(payload=desired_authz_scope, client_id=cid, realm=realm)
|
||||
result["changed"] = True
|
||||
result["msg"] = "Authorization scope created"
|
||||
result["end_state"] = desired_authz_scope
|
||||
elif before_authz_scope and state == "absent":
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_authz_scope, after={})
|
||||
|
||||
if module.check_mode:
|
||||
result["changed"] = True
|
||||
result["msg"] = "Authorization scope would be removed"
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
kc.remove_authz_authorization_scope(id=before_authz_scope["id"], client_id=cid, realm=realm)
|
||||
result["changed"] = True
|
||||
result["msg"] = "Authorization scope removed"
|
||||
elif not before_authz_scope and state == "absent":
|
||||
result["changed"] = False
|
||||
else:
|
||||
module.fail_json(
|
||||
msg=f"Unable to determine what to do with authorization scope {name} of client {client_id} in realm {realm}"
|
||||
)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
# Copyright (c) 2021, Christophe Gilles <christophe.gilles54@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
|
||||
# https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_authz_custom_policy
|
||||
|
||||
short_description: Allows administration of Keycloak client custom Javascript policies using Keycloak API
|
||||
|
||||
# Originally added in community.general 7.5.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows the administration of Keycloak client custom Javascript using the Keycloak REST API. Custom Javascript
|
||||
policies are only available if a client has Authorization enabled and if they have been deployed to the Keycloak server
|
||||
as JAR files.
|
||||
- This module requires access to the REST API using OpenID Connect; the user connecting and the realm 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
|
||||
realm definition with the scope tailored to your needs and a user having the expected roles.
|
||||
- The names of module options are snake_cased versions of the camelCase options used by Keycloak. The Authorization Services
|
||||
paths and payloads have not officially been documented by the Keycloak project.
|
||||
U(https://www.puppeteers.net/blog/keycloak-authorization-services-rest-api-paths-and-payload/).
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the custom policy.
|
||||
- On V(present), the custom policy is created (or updated if it exists already).
|
||||
- On V(absent), the custom policy is removed if it exists.
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Name of the custom policy to create.
|
||||
type: str
|
||||
required: true
|
||||
policy_type:
|
||||
description:
|
||||
- The type of the policy. This must match the name of the custom policy deployed to the server.
|
||||
- Multiple policies pointing to the same policy type can be created, but their names have to differ.
|
||||
type: str
|
||||
required: true
|
||||
client_id:
|
||||
description:
|
||||
- The V(clientId) of the Keycloak client that should have the custom policy attached to it.
|
||||
- This is usually a human-readable name of the Keycloak client.
|
||||
type: str
|
||||
required: true
|
||||
realm:
|
||||
description:
|
||||
- The name of the Keycloak realm the Keycloak client is in.
|
||||
type: str
|
||||
required: true
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Samuli Seppänen (@mattock)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Manage Keycloak custom authorization policy
|
||||
middleware_automation.keycloak.keycloak_authz_custom_policy:
|
||||
name: OnlyOwner
|
||||
state: present
|
||||
policy_type: script-policy.js
|
||||
client_id: myclient
|
||||
realm: myrealm
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: keycloak
|
||||
auth_password: keycloak
|
||||
auth_realm: master
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
end_state:
|
||||
description: Representation of the custom policy after module execution.
|
||||
returned: on success
|
||||
type: dict
|
||||
contains:
|
||||
name:
|
||||
description: Name of the custom policy.
|
||||
type: str
|
||||
returned: when I(state=present)
|
||||
sample: file:delete
|
||||
policy_type:
|
||||
description: Type of custom policy.
|
||||
type: str
|
||||
returned: when I(state=present)
|
||||
sample: File delete
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
name=dict(type="str", required=True),
|
||||
policy_type=dict(type="str", required=True),
|
||||
client_id=dict(type="str", required=True),
|
||||
realm=dict(type="str", required=True),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
# Convenience variables
|
||||
state = module.params.get("state")
|
||||
name = module.params.get("name")
|
||||
policy_type = module.params.get("policy_type")
|
||||
client_id = module.params.get("client_id")
|
||||
realm = module.params.get("realm")
|
||||
|
||||
cid = kc.get_client_id(client_id, realm=realm)
|
||||
if not cid:
|
||||
module.fail_json(msg=f"Invalid client {client_id} for realm {realm}")
|
||||
|
||||
before_authz_custom_policy = kc.get_authz_policy_by_name(name=name, client_id=cid, realm=realm)
|
||||
|
||||
desired_authz_custom_policy = {}
|
||||
desired_authz_custom_policy["name"] = name
|
||||
desired_authz_custom_policy["type"] = policy_type
|
||||
|
||||
# Modifying existing custom policies is not possible
|
||||
if before_authz_custom_policy and state == "present":
|
||||
result["msg"] = f"Custom policy {name} already exists"
|
||||
result["changed"] = False
|
||||
result["end_state"] = desired_authz_custom_policy
|
||||
elif not before_authz_custom_policy and state == "present":
|
||||
if module.check_mode:
|
||||
result["msg"] = f"Would create custom policy {name}"
|
||||
else:
|
||||
kc.create_authz_custom_policy(
|
||||
payload=desired_authz_custom_policy, policy_type=policy_type, client_id=cid, realm=realm
|
||||
)
|
||||
result["msg"] = f"Custom policy {name} created"
|
||||
|
||||
result["changed"] = True
|
||||
result["end_state"] = desired_authz_custom_policy
|
||||
elif before_authz_custom_policy and state == "absent":
|
||||
if module.check_mode:
|
||||
result["msg"] = f"Would remove custom policy {name}"
|
||||
else:
|
||||
kc.remove_authz_custom_policy(policy_id=before_authz_custom_policy["id"], client_id=cid, realm=realm)
|
||||
result["msg"] = f"Custom policy {name} removed"
|
||||
|
||||
result["changed"] = True
|
||||
result["end_state"] = {}
|
||||
elif not before_authz_custom_policy and state == "absent":
|
||||
result["msg"] = f"Custom policy {name} does not exist"
|
||||
result["changed"] = False
|
||||
result["end_state"] = {}
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,445 @@
|
|||
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
# Copyright (c) 2021, Christophe Gilles <christophe.gilles54@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
|
||||
# https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_authz_permission
|
||||
|
||||
# Originally added in community.general 7.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
short_description: Allows administration of Keycloak client authorization permissions using Keycloak API
|
||||
|
||||
description:
|
||||
- This module allows the administration of Keycloak client authorization permissions using the Keycloak REST API. Authorization
|
||||
permissions are only available if a client has Authorization enabled.
|
||||
- There are some peculiarities in JSON paths and payloads for authorization permissions. In particular POST and PUT operations
|
||||
are targeted at permission endpoints, whereas GET requests go to policies endpoint. To make matters more interesting the
|
||||
JSON responses from GET requests return data in a different format than what is expected for POST and PUT. The end result
|
||||
is that it is not possible to detect changes to things like policies, scopes or resources - at least not without a large
|
||||
number of additional API calls. Therefore this module always updates authorization permissions instead of attempting to
|
||||
determine if changes are truly needed.
|
||||
- This module requires access to the REST API using OpenID Connect; the user connecting and the realm 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
|
||||
realm definition with the scope tailored to your needs and a user having the expected roles.
|
||||
- The names of module options are snake_cased versions of the camelCase options used by Keycloak. The Authorization Services
|
||||
paths and payloads have not officially been documented by the Keycloak project.
|
||||
U(https://www.puppeteers.net/blog/keycloak-authorization-services-rest-api-paths-and-payload/).
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the authorization permission.
|
||||
- On V(present), the authorization permission is created (or updated if it exists already).
|
||||
- On V(absent), the authorization permission is removed if it exists.
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Name of the authorization permission to create.
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
description:
|
||||
- The description of the authorization permission.
|
||||
type: str
|
||||
permission_type:
|
||||
description:
|
||||
- The type of authorization permission.
|
||||
- On V(scope) create a scope-based permission.
|
||||
- On V(resource) create a resource-based permission.
|
||||
type: str
|
||||
required: true
|
||||
choices:
|
||||
- resource
|
||||
- scope
|
||||
decision_strategy:
|
||||
description:
|
||||
- The decision strategy to use with this permission.
|
||||
type: str
|
||||
default: UNANIMOUS
|
||||
choices:
|
||||
- UNANIMOUS
|
||||
- AFFIRMATIVE
|
||||
- CONSENSUS
|
||||
resources:
|
||||
description:
|
||||
- Resource names to attach to this permission.
|
||||
- Scope-based permissions can only include one resource.
|
||||
- Resource-based permissions can include multiple resources.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
scopes:
|
||||
description:
|
||||
- Scope names to attach to this permission.
|
||||
- Resource-based permissions cannot have scopes attached to them.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
policies:
|
||||
description:
|
||||
- Policy names to attach to this permission.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
client_id:
|
||||
description:
|
||||
- The clientId of the keycloak client that should have the authorization scope.
|
||||
- This is usually a human-readable name of the Keycloak client.
|
||||
type: str
|
||||
required: true
|
||||
realm:
|
||||
description:
|
||||
- The name of the Keycloak realm the Keycloak client is in.
|
||||
type: str
|
||||
required: true
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Samuli Seppänen (@mattock)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Manage scope-based Keycloak authorization permission
|
||||
middleware_automation.keycloak.keycloak_authz_permission:
|
||||
name: ScopePermission
|
||||
state: present
|
||||
description: Scope permission
|
||||
permission_type: scope
|
||||
scopes:
|
||||
- file:delete
|
||||
policies:
|
||||
- Default Policy
|
||||
client_id: myclient
|
||||
realm: myrealm
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: keycloak
|
||||
auth_password: keycloak
|
||||
auth_realm: master
|
||||
|
||||
- name: Manage resource-based Keycloak authorization permission
|
||||
middleware_automation.keycloak.keycloak_authz_permission:
|
||||
name: ResourcePermission
|
||||
state: present
|
||||
description: Resource permission
|
||||
permission_type: resource
|
||||
resources:
|
||||
- Default Resource
|
||||
policies:
|
||||
- Default Policy
|
||||
client_id: myclient
|
||||
realm: myrealm
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: keycloak
|
||||
auth_password: keycloak
|
||||
auth_realm: master
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
end_state:
|
||||
description: Representation of the authorization permission after module execution.
|
||||
returned: on success
|
||||
type: complex
|
||||
contains:
|
||||
id:
|
||||
description: ID of the authorization permission.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: 9da05cd2-b273-4354-bbd8-0c133918a454
|
||||
name:
|
||||
description: Name of the authorization permission.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: ResourcePermission
|
||||
description:
|
||||
description: Description of the authorization permission.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: Resource Permission
|
||||
type:
|
||||
description: Type of the authorization permission.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: resource
|
||||
decisionStrategy:
|
||||
description: The decision strategy to use.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: UNANIMOUS
|
||||
logic:
|
||||
description: The logic used for the permission (part of the payload, but has a fixed value).
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: POSITIVE
|
||||
resources:
|
||||
description: IDs of resources attached to this permission.
|
||||
type: list
|
||||
returned: when O(state=present)
|
||||
sample:
|
||||
- 49e052ff-100d-4b79-a9dd-52669ed3c11d
|
||||
scopes:
|
||||
description: IDs of scopes attached to this permission.
|
||||
type: list
|
||||
returned: when O(state=present)
|
||||
sample:
|
||||
- 9da05cd2-b273-4354-bbd8-0c133918a454
|
||||
policies:
|
||||
description: IDs of policies attached to this permission.
|
||||
type: list
|
||||
returned: when O(state=present)
|
||||
sample:
|
||||
- 9da05cd2-b273-4354-bbd8-0c133918a454
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
name=dict(type="str", required=True),
|
||||
description=dict(type="str"),
|
||||
permission_type=dict(type="str", choices=["scope", "resource"], required=True),
|
||||
decision_strategy=dict(type="str", default="UNANIMOUS", choices=["UNANIMOUS", "AFFIRMATIVE", "CONSENSUS"]),
|
||||
resources=dict(type="list", elements="str", default=[]),
|
||||
scopes=dict(type="list", elements="str", default=[]),
|
||||
policies=dict(type="list", elements="str", default=[]),
|
||||
client_id=dict(type="str", required=True),
|
||||
realm=dict(type="str", required=True),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
# Convenience variables
|
||||
state = module.params.get("state")
|
||||
name = module.params.get("name")
|
||||
description = module.params.get("description")
|
||||
permission_type = module.params.get("permission_type")
|
||||
decision_strategy = module.params.get("decision_strategy")
|
||||
realm = module.params.get("realm")
|
||||
client_id = module.params.get("client_id")
|
||||
realm = module.params.get("realm")
|
||||
resources = module.params.get("resources")
|
||||
scopes = module.params.get("scopes")
|
||||
policies = module.params.get("policies")
|
||||
|
||||
if permission_type == "scope" and state == "present":
|
||||
if scopes == []:
|
||||
module.fail_json(msg="Scopes need to defined when permission type is set to scope!")
|
||||
if len(resources) > 1:
|
||||
module.fail_json(msg="Only one resource can be defined for a scope permission!")
|
||||
|
||||
if permission_type == "resource" and state == "present":
|
||||
if resources == []:
|
||||
module.fail_json(msg="A resource need to defined when permission type is set to resource!")
|
||||
if scopes != []:
|
||||
module.fail_json(msg="Scopes cannot be defined when permission type is set to resource!")
|
||||
|
||||
result = dict(changed=False, msg="", end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
# Get id of the client based on client_id
|
||||
cid = kc.get_client_id(client_id, realm=realm)
|
||||
if not cid:
|
||||
module.fail_json(msg=f"Invalid client {client_id} for realm {realm}")
|
||||
|
||||
# Get current state of the permission using its name as the search
|
||||
# filter. This returns False if it is not found.
|
||||
permission = kc.get_authz_permission_by_name(name=name, client_id=cid, realm=realm)
|
||||
|
||||
# Generate a JSON payload for Keycloak Admin API. This is needed for
|
||||
# "create" and "update" operations.
|
||||
payload = {}
|
||||
payload["name"] = name
|
||||
payload["description"] = description
|
||||
payload["type"] = permission_type
|
||||
payload["decisionStrategy"] = decision_strategy
|
||||
payload["logic"] = "POSITIVE"
|
||||
payload["scopes"] = []
|
||||
payload["resources"] = []
|
||||
payload["policies"] = []
|
||||
|
||||
if permission_type == "scope":
|
||||
# Add the resource id, if any, to the payload. While the data type is a
|
||||
# list, it is only possible to have one entry in it based on what Keycloak
|
||||
# Admin Console does.
|
||||
r = False
|
||||
resource_scopes = []
|
||||
|
||||
if resources:
|
||||
r = kc.get_authz_resource_by_name(resources[0], cid, realm)
|
||||
if not r:
|
||||
module.fail_json(
|
||||
msg=f"Unable to find authorization resource with name {resources[0]} for client {cid} in realm {realm}"
|
||||
)
|
||||
else:
|
||||
payload["resources"].append(r["_id"])
|
||||
|
||||
for rs in r["scopes"]:
|
||||
resource_scopes.append(rs["id"])
|
||||
|
||||
# Generate a list of scope ids based on scope names. Fail if the
|
||||
# defined resource does not include all those scopes.
|
||||
for scope in scopes:
|
||||
s = kc.get_authz_authorization_scope_by_name(scope, cid, realm)
|
||||
if r and s["id"] not in resource_scopes:
|
||||
module.fail_json(
|
||||
msg=f"Resource {resources[0]} does not include scope {scope} for client {client_id} in realm {realm}"
|
||||
)
|
||||
else:
|
||||
payload["scopes"].append(s["id"])
|
||||
|
||||
elif permission_type == "resource":
|
||||
if resources:
|
||||
for resource in resources:
|
||||
r = kc.get_authz_resource_by_name(resource, cid, realm)
|
||||
if not r:
|
||||
module.fail_json(
|
||||
msg=f"Unable to find authorization resource with name {resource} for client {cid} in realm {realm}"
|
||||
)
|
||||
else:
|
||||
payload["resources"].append(r["_id"])
|
||||
|
||||
# Add policy ids, if any, to the payload.
|
||||
if policies:
|
||||
for policy in policies:
|
||||
p = kc.get_authz_policy_by_name(policy, cid, realm)
|
||||
|
||||
if p:
|
||||
payload["policies"].append(p["id"])
|
||||
else:
|
||||
module.fail_json(
|
||||
msg=f"Unable to find authorization policy with name {policy} for client {client_id} in realm {realm}"
|
||||
)
|
||||
|
||||
# Add "id" to payload for update operations
|
||||
if permission:
|
||||
payload["id"] = permission["id"]
|
||||
|
||||
# Handle the special case where the user attempts to change an already
|
||||
# existing permission's type - something that can't be done without a
|
||||
# full delete -> (re)create cycle.
|
||||
if permission["type"] != payload["type"]:
|
||||
module.fail_json(
|
||||
msg=(
|
||||
f"Modifying the type of permission (scope/resource) is not supported: "
|
||||
f"permission {permission['id']} of client {cid} in realm {realm} unchanged"
|
||||
)
|
||||
)
|
||||
|
||||
# Updating an authorization permission is tricky for several reasons.
|
||||
# Firstly, the current permission is retrieved using a _policy_ endpoint,
|
||||
# not from a permission endpoint. Also, the data that is returned is in a
|
||||
# different format than what is expected by the payload. So, comparing the
|
||||
# current state attribute by attribute to the payload is not possible. For
|
||||
# example the data contains a JSON object "config" which may contain the
|
||||
# authorization type, but which is no required in the payload. Moreover,
|
||||
# information about resources, scopes and policies is _not_ present in the
|
||||
# data. So, there is no way to determine if any of those fields have
|
||||
# changed. Therefore the best options we have are
|
||||
#
|
||||
# a) Always apply the payload without checking the current state
|
||||
# b) Refuse to make any changes to any settings (only support create and delete)
|
||||
#
|
||||
# The approach taken here is a).
|
||||
#
|
||||
if permission and state == "present":
|
||||
if module.check_mode:
|
||||
result["msg"] = "Notice: unable to check current resources, scopes and policies for permission. \
|
||||
Would apply desired state without checking the current state."
|
||||
else:
|
||||
kc.update_authz_permission(
|
||||
payload=payload, permission_type=permission_type, id=permission["id"], client_id=cid, realm=realm
|
||||
)
|
||||
result["msg"] = "Notice: unable to check current resources, scopes and policies for permission. \
|
||||
Applying desired state without checking the current state."
|
||||
|
||||
# Assume that something changed, although we don't know if that is the case.
|
||||
result["changed"] = True
|
||||
result["end_state"] = payload
|
||||
elif not permission and state == "present":
|
||||
if module.check_mode:
|
||||
result["msg"] = "Would create permission"
|
||||
else:
|
||||
kc.create_authz_permission(payload=payload, permission_type=permission_type, client_id=cid, realm=realm)
|
||||
result["msg"] = "Permission created"
|
||||
|
||||
result["changed"] = True
|
||||
result["end_state"] = payload
|
||||
elif permission and state == "absent":
|
||||
if module.check_mode:
|
||||
result["msg"] = "Would remove permission"
|
||||
else:
|
||||
kc.remove_authz_permission(id=permission["id"], client_id=cid, realm=realm)
|
||||
result["msg"] = "Permission removed"
|
||||
|
||||
result["changed"] = True
|
||||
|
||||
elif not permission and state == "absent":
|
||||
result["changed"] = False
|
||||
else:
|
||||
module.fail_json(
|
||||
msg=f"Unable to determine what to do with permission {name} of client {client_id} in realm {realm}"
|
||||
)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
# Copyright (c) 2021, Christophe Gilles <christophe.gilles54@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
|
||||
# https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_authz_permission_info
|
||||
|
||||
# Originally added in community.general 7.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
short_description: Query Keycloak client authorization permissions information
|
||||
|
||||
description:
|
||||
- This module allows querying information about Keycloak client authorization permissions from the resources endpoint using
|
||||
the Keycloak REST API. Authorization permissions are only available if a client has Authorization enabled.
|
||||
- This module requires access to the REST API using OpenID Connect; the user connecting and the realm 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
|
||||
realm definition with the scope tailored to your needs and a user having the expected roles.
|
||||
- The names of module options are snake_cased versions of the camelCase options used by Keycloak. The Authorization Services
|
||||
paths and payloads have not officially been documented by the Keycloak project.
|
||||
U(https://www.puppeteers.net/blog/keycloak-authorization-services-rest-api-paths-and-payload/).
|
||||
attributes:
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the authorization permission to create.
|
||||
type: str
|
||||
required: true
|
||||
client_id:
|
||||
description:
|
||||
- The clientId of the keycloak client that should have the authorization scope.
|
||||
- This is usually a human-readable name of the Keycloak client.
|
||||
type: str
|
||||
required: true
|
||||
realm:
|
||||
description:
|
||||
- The name of the Keycloak realm the Keycloak client is in.
|
||||
type: str
|
||||
required: true
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
- middleware_automation.keycloak.attributes.info_module
|
||||
|
||||
author:
|
||||
- Samuli Seppänen (@mattock)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Query Keycloak authorization permission
|
||||
middleware_automation.keycloak.keycloak_authz_permission_info:
|
||||
name: ScopePermission
|
||||
client_id: myclient
|
||||
realm: myrealm
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: keycloak
|
||||
auth_password: keycloak
|
||||
auth_realm: master
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
queried_state:
|
||||
description: State of the resource (a policy) as seen by Keycloak.
|
||||
returned: on success
|
||||
type: complex
|
||||
contains:
|
||||
id:
|
||||
description: ID of the authorization permission.
|
||||
type: str
|
||||
sample: 9da05cd2-b273-4354-bbd8-0c133918a454
|
||||
name:
|
||||
description: Name of the authorization permission.
|
||||
type: str
|
||||
sample: ResourcePermission
|
||||
description:
|
||||
description: Description of the authorization permission.
|
||||
type: str
|
||||
sample: Resource Permission
|
||||
type:
|
||||
description: Type of the authorization permission.
|
||||
type: str
|
||||
sample: resource
|
||||
decisionStrategy:
|
||||
description: The decision strategy.
|
||||
type: str
|
||||
sample: UNANIMOUS
|
||||
logic:
|
||||
description: The logic used for the permission (part of the payload, but has a fixed value).
|
||||
type: str
|
||||
sample: POSITIVE
|
||||
config:
|
||||
description: Configuration of the permission (empty in all observed cases).
|
||||
type: dict
|
||||
sample: {}
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
name=dict(type="str", required=True),
|
||||
client_id=dict(type="str", required=True),
|
||||
realm=dict(type="str", required=True),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
# Convenience variables
|
||||
name = module.params.get("name")
|
||||
client_id = module.params.get("client_id")
|
||||
realm = module.params.get("realm")
|
||||
|
||||
result = dict(changed=False, msg="", queried_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
# Get id of the client based on client_id
|
||||
cid = kc.get_client_id(client_id, realm=realm)
|
||||
if not cid:
|
||||
module.fail_json(msg=f"Invalid client {client_id} for realm {realm}")
|
||||
|
||||
# Get current state of the permission using its name as the search
|
||||
# filter. This returns False if it is not found.
|
||||
permission = kc.get_authz_permission_by_name(name=name, client_id=cid, realm=realm)
|
||||
|
||||
result["queried_state"] = permission
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,416 @@
|
|||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_client_rolemapping
|
||||
|
||||
short_description: Allows administration of Keycloak client_rolemapping with the Keycloak API
|
||||
|
||||
# Originally added in community.general 3.5.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to add, remove or modify Keycloak client_rolemapping with 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.
|
||||
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 names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/latest/rest-api/index.html).
|
||||
- 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
|
||||
suitable for the API.
|
||||
- When updating a client_rolemapping, where possible provide the role ID to the module. This removes a lookup to the API
|
||||
to translate the name into the role ID.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the client_rolemapping.
|
||||
- On V(present), the client_rolemapping is created if it does not yet exist, or updated with the parameters
|
||||
you provide.
|
||||
- On V(absent), the client_rolemapping is removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- They Keycloak realm under which this role_representation resides.
|
||||
default: 'master'
|
||||
|
||||
group_name:
|
||||
type: str
|
||||
description:
|
||||
- Name of the group to be mapped.
|
||||
- This parameter is required (can be replaced by gid for less API call).
|
||||
parents:
|
||||
type: list
|
||||
description:
|
||||
- List of parent groups for the group to handle sorted top to bottom.
|
||||
- Set this if your group is a subgroup and you do not provide the GID in O(gid).
|
||||
elements: dict
|
||||
suboptions:
|
||||
id:
|
||||
type: str
|
||||
description:
|
||||
- Identify parent by ID.
|
||||
- Needs less API calls than using O(parents[].name).
|
||||
- A deep parent chain can be started at any point when first given parent is given as ID.
|
||||
- Note that in principle both ID and name can be specified at the same time but current implementation only always
|
||||
use just one of them, with ID being preferred.
|
||||
name:
|
||||
type: str
|
||||
description:
|
||||
- Identify parent by name.
|
||||
- Needs more internal API calls than using O(parents[].id) to map names to ID's under the hood.
|
||||
- When giving a parent chain with only names it must be complete up to the top.
|
||||
- Note that in principle both ID and name can be specified at the same time but current implementation only always
|
||||
use just one of them, with ID being preferred.
|
||||
gid:
|
||||
type: str
|
||||
description:
|
||||
- ID of the group to be mapped.
|
||||
- This parameter is not required for updating or deleting the rolemapping but providing it reduces the number of API
|
||||
calls required.
|
||||
client_id:
|
||||
type: str
|
||||
description:
|
||||
- Name of the client to be mapped (different than O(cid)).
|
||||
- This parameter is required (can be replaced by cid for less API call).
|
||||
cid:
|
||||
type: str
|
||||
description:
|
||||
- ID of the client to be mapped.
|
||||
- This parameter is not required for updating or deleting the rolemapping but providing it reduces the number of API
|
||||
calls required.
|
||||
roles:
|
||||
description:
|
||||
- Roles to be mapped to the group.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
description:
|
||||
- Name of the role_representation.
|
||||
- This parameter is required only when creating or updating the role_representation.
|
||||
id:
|
||||
type: str
|
||||
description:
|
||||
- The unique identifier for this role_representation.
|
||||
- This parameter is not required for updating or deleting a role_representation but providing it reduces the number
|
||||
of API calls required.
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Gaëtan Daubresse (@Gaetan2907)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Map a client role to a group, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_client_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
state: present
|
||||
client_id: client1
|
||||
group_name: group1
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Map a client role to a group, authentication with token
|
||||
middleware_automation.keycloak.keycloak_client_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
state: present
|
||||
client_id: client1
|
||||
group_name: group1
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Map a client role to a subgroup, authentication with token
|
||||
middleware_automation.keycloak.keycloak_client_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
state: present
|
||||
client_id: client1
|
||||
group_name: subgroup1
|
||||
parents:
|
||||
- name: parent-group
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Unmap client role from a group
|
||||
middleware_automation.keycloak.keycloak_client_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
state: absent
|
||||
client_id: client1
|
||||
group_name: group1
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Role role1 assigned to group group1."
|
||||
|
||||
proposed:
|
||||
description: Representation of proposed client role mapping.
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {"clientId": "test"}
|
||||
|
||||
existing:
|
||||
description:
|
||||
- Representation of existing client role mapping.
|
||||
- The sample is truncated.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"adminUrl": "http://www.example.com/admin_url",
|
||||
"attributes": {
|
||||
"request.object.signature.alg": "RS256"
|
||||
}
|
||||
}
|
||||
|
||||
end_state:
|
||||
description:
|
||||
- Representation of client role mapping after module execution.
|
||||
- The sample is truncated.
|
||||
returned: on success
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"adminUrl": "http://www.example.com/admin_url",
|
||||
"attributes": {
|
||||
"request.object.signature.alg": "RS256"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
roles_spec = dict(
|
||||
name=dict(type="str"),
|
||||
id=dict(type="str"),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(default="present", choices=["present", "absent"]),
|
||||
realm=dict(default="master"),
|
||||
gid=dict(type="str"),
|
||||
group_name=dict(type="str"),
|
||||
parents=dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=dict(id=dict(type="str"), name=dict(type="str")),
|
||||
),
|
||||
cid=dict(type="str"),
|
||||
client_id=dict(type="str"),
|
||||
roles=dict(type="list", elements="dict", options=roles_spec),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, proposed={}, existing={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
state = module.params.get("state")
|
||||
cid = module.params.get("cid")
|
||||
client_id = module.params.get("client_id")
|
||||
gid = module.params.get("gid")
|
||||
group_name = module.params.get("group_name")
|
||||
roles = module.params.get("roles")
|
||||
parents = module.params.get("parents")
|
||||
|
||||
# Check the parameters
|
||||
if cid is None and client_id is None:
|
||||
module.fail_json(msg="Either the `client_id` or `cid` has to be specified.")
|
||||
if gid is None and group_name is None:
|
||||
module.fail_json(msg="Either the `group_name` or `gid` has to be specified.")
|
||||
|
||||
# Get the potential missing parameters
|
||||
if gid is None:
|
||||
group_rep = kc.get_group_by_name(group_name, realm=realm, parents=parents)
|
||||
if group_rep is not None:
|
||||
gid = group_rep["id"]
|
||||
else:
|
||||
module.fail_json(msg=f"Could not fetch group {group_name}:")
|
||||
if cid is None:
|
||||
cid = kc.get_client_id(client_id, realm=realm)
|
||||
if cid is None:
|
||||
module.fail_json(msg=f"Could not fetch client {client_id}:")
|
||||
if roles is None:
|
||||
module.exit_json(msg="Nothing to do (no roles specified).")
|
||||
else:
|
||||
for role in roles:
|
||||
if role["name"] is None and role["id"] is None:
|
||||
module.fail_json(msg="Either the `name` or `id` has to be specified on each role.")
|
||||
# Fetch missing role_id
|
||||
if role["id"] is None:
|
||||
role_id = kc.get_client_role_id_by_name(cid, role["name"], realm=realm)
|
||||
if role_id is not None:
|
||||
role["id"] = role_id
|
||||
else:
|
||||
module.fail_json(msg=f"Could not fetch role {role['name']}:")
|
||||
# Fetch missing role_name
|
||||
else:
|
||||
role["name"] = kc.get_client_group_rolemapping_by_id(gid, cid, role["id"], realm=realm)["name"]
|
||||
if role["name"] is None:
|
||||
module.fail_json(msg=f"Could not fetch role {role['id']}")
|
||||
|
||||
# Get effective client-level role mappings
|
||||
available_roles_before = kc.get_client_group_available_rolemappings(gid, cid, realm=realm)
|
||||
assigned_roles_before = kc.get_client_group_composite_rolemappings(gid, cid, realm=realm)
|
||||
|
||||
result["existing"] = assigned_roles_before
|
||||
result["proposed"] = list(assigned_roles_before) if assigned_roles_before else []
|
||||
|
||||
update_roles = []
|
||||
for role in roles:
|
||||
# Fetch roles to assign if state present
|
||||
if state == "present":
|
||||
for available_role in available_roles_before:
|
||||
if role["name"] == available_role["name"]:
|
||||
update_roles.append(
|
||||
{
|
||||
"id": role["id"],
|
||||
"name": role["name"],
|
||||
}
|
||||
)
|
||||
result["proposed"].append(available_role)
|
||||
# Fetch roles to remove if state absent
|
||||
else:
|
||||
for assigned_role in assigned_roles_before:
|
||||
if role["name"] == assigned_role["name"]:
|
||||
update_roles.append(
|
||||
{
|
||||
"id": role["id"],
|
||||
"name": role["name"],
|
||||
}
|
||||
)
|
||||
if assigned_role in result["proposed"]: # Handle double removal
|
||||
result["proposed"].remove(assigned_role)
|
||||
|
||||
if len(update_roles):
|
||||
if state == "present":
|
||||
# Assign roles
|
||||
result["changed"] = True
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=assigned_roles_before, after=result["proposed"])
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
kc.add_group_rolemapping(gid, cid, update_roles, realm=realm)
|
||||
result["msg"] = f"Roles {update_roles} assigned to group {group_name}."
|
||||
assigned_roles_after = kc.get_client_group_composite_rolemappings(gid, cid, realm=realm)
|
||||
result["end_state"] = assigned_roles_after
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
# Remove mapping of role
|
||||
result["changed"] = True
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=assigned_roles_before, after=result["proposed"])
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
kc.delete_group_rolemapping(gid, cid, update_roles, realm=realm)
|
||||
result["msg"] = f"Roles {update_roles} removed from group {group_name}."
|
||||
assigned_roles_after = kc.get_client_group_composite_rolemappings(gid, cid, realm=realm)
|
||||
result["end_state"] = assigned_roles_after
|
||||
module.exit_json(**result)
|
||||
# Do nothing
|
||||
else:
|
||||
result["changed"] = False
|
||||
result["msg"] = (
|
||||
f"Nothing to do, roles {roles} are {'mapped' if state == 'present' else 'not mapped'} with group {group_name}."
|
||||
)
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_client_rolescope
|
||||
|
||||
short_description: Allows administration of Keycloak client roles scope to restrict the usage of certain roles to a other
|
||||
specific client applications
|
||||
|
||||
# Originally added in community.general 8.6.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to add or remove Keycloak roles from clients scope 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.
|
||||
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.
|
||||
- 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
|
||||
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.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the role mapping.
|
||||
- On V(present), all roles in O(role_names) are mapped if not exist yet.
|
||||
- On V(absent), all roles mapping in O(role_names) are removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- The Keycloak realm under which clients resides.
|
||||
default: 'master'
|
||||
|
||||
target_client_id:
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
- Roles provided in O(role_names) while be added to this client scope.
|
||||
role_owner_client_id:
|
||||
type: str
|
||||
description:
|
||||
- If the O(role_names) are client role, the client ID under which it resides.
|
||||
- If this parameter is absent, the roles are considered a realm role.
|
||||
role_names:
|
||||
required: true
|
||||
type: list
|
||||
elements: str
|
||||
description:
|
||||
- Names of roles to manipulate.
|
||||
- If O(role_owner_client_id) is present, all roles must be under this client.
|
||||
- If O(role_owner_client_id) is absent, all roles must be under the realm.
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Andre Desrosiers (@desand01)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Add roles to public client scope
|
||||
middleware_automation.keycloak.keycloak_client_rolescope:
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: MyCustomRealm
|
||||
target_client_id: frontend-client-public
|
||||
role_owner_client_id: backend-client-private
|
||||
role_names:
|
||||
- backend-role-admin
|
||||
- backend-role-user
|
||||
|
||||
- name: Remove roles from public client scope
|
||||
middleware_automation.keycloak.keycloak_client_rolescope:
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: MyCustomRealm
|
||||
target_client_id: frontend-client-public
|
||||
role_owner_client_id: backend-client-private
|
||||
role_names:
|
||||
- backend-role-admin
|
||||
state: absent
|
||||
|
||||
- name: Add realm roles to public client scope
|
||||
middleware_automation.keycloak.keycloak_client_rolescope:
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: MyCustomRealm
|
||||
target_client_id: frontend-client-public
|
||||
role_names:
|
||||
- realm-role-admin
|
||||
- realm-role-user
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Client role scope for frontend-client-public has been updated"
|
||||
|
||||
end_state:
|
||||
description: Representation of role role scope after module execution.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: dict
|
||||
sample:
|
||||
[
|
||||
{
|
||||
"clientRole": false,
|
||||
"composite": false,
|
||||
"containerId": "MyCustomRealm",
|
||||
"id": "47293104-59a6-46f0-b460-2e9e3c9c424c",
|
||||
"name": "backend-role-admin"
|
||||
},
|
||||
{
|
||||
"clientRole": false,
|
||||
"composite": false,
|
||||
"containerId": "MyCustomRealm",
|
||||
"id": "39c62a6d-542c-4715-92d2-41021eb33967",
|
||||
"name": "backend-role-user"
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
target_client_id=dict(type="str", required=True),
|
||||
role_owner_client_id=dict(type="str"),
|
||||
realm=dict(type="str", default="master"),
|
||||
role_names=dict(type="list", elements="str", required=True),
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
target_client_id = module.params.get("target_client_id")
|
||||
role_owner_client_id = module.params.get("role_owner_client_id")
|
||||
role_names = module.params.get("role_names")
|
||||
state = module.params.get("state")
|
||||
|
||||
objRealm = kc.get_realm_by_id(realm)
|
||||
if not objRealm:
|
||||
module.fail_json(msg=f"Failed to retrive realm '{realm}'")
|
||||
|
||||
objClient = kc.get_client_by_client_id(target_client_id, realm)
|
||||
if not objClient:
|
||||
module.fail_json(msg=f"Failed to retrive client '{realm}.{target_client_id}'")
|
||||
if objClient["fullScopeAllowed"] and state == "present":
|
||||
module.fail_json(msg=f"FullScopeAllowed is active for Client '{realm}.{target_client_id}'")
|
||||
|
||||
if role_owner_client_id:
|
||||
role_owner_client = kc.get_client_by_client_id(role_owner_client_id, realm)
|
||||
if not role_owner_client:
|
||||
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"], role_owner_client["id"], realm)
|
||||
else:
|
||||
before_role_mapping = kc.get_client_role_scope_from_realm(objClient["id"], realm)
|
||||
|
||||
if role_owner_client_id:
|
||||
# retrive all role from client_scope
|
||||
client_scope_roles_by_name = kc.get_client_roles_by_id(role_owner_client["id"], realm)
|
||||
else:
|
||||
# retrive all role from realm
|
||||
client_scope_roles_by_name = kc.get_realm_roles(realm)
|
||||
|
||||
# convert to indexed Dict by name
|
||||
client_scope_roles_by_name = {role["name"]: role for role in client_scope_roles_by_name}
|
||||
role_mapping_by_name = {role["name"]: role for role in before_role_mapping}
|
||||
role_mapping_to_manipulate = []
|
||||
|
||||
if state == "present":
|
||||
# update desired
|
||||
for role_name in role_names:
|
||||
if role_name not in client_scope_roles_by_name:
|
||||
if role_owner_client_id:
|
||||
module.fail_json(msg=f"Failed to retrive role '{realm}.{role_owner_client_id}.{role_name}'")
|
||||
else:
|
||||
module.fail_json(msg=f"Failed to retrive role '{realm}.{role_name}'")
|
||||
if role_name not in role_mapping_by_name:
|
||||
role_mapping_to_manipulate.append(client_scope_roles_by_name[role_name])
|
||||
role_mapping_by_name[role_name] = client_scope_roles_by_name[role_name]
|
||||
else:
|
||||
# remove role if present
|
||||
for role_name in role_names:
|
||||
if role_name in role_mapping_by_name:
|
||||
role_mapping_to_manipulate.append(role_mapping_by_name[role_name])
|
||||
del role_mapping_by_name[role_name]
|
||||
|
||||
before_role_mapping = sorted(before_role_mapping, key=lambda d: d["name"])
|
||||
desired_role_mapping = sorted(role_mapping_by_name.values(), key=lambda d: d["name"])
|
||||
|
||||
result["changed"] = len(role_mapping_to_manipulate) > 0
|
||||
|
||||
if result["changed"]:
|
||||
result["diff"] = dict(before=before_role_mapping, after=desired_role_mapping)
|
||||
|
||||
if not result["changed"]:
|
||||
# no changes
|
||||
result["end_state"] = before_role_mapping
|
||||
result["msg"] = f"No changes required for client role scope {target_client_id}."
|
||||
elif state == "present":
|
||||
# doing update
|
||||
if module.check_mode:
|
||||
result["end_state"] = desired_role_mapping
|
||||
elif role_owner_client_id:
|
||||
result["end_state"] = kc.update_client_role_scope_from_client(
|
||||
role_mapping_to_manipulate, objClient["id"], role_owner_client["id"], realm
|
||||
)
|
||||
else:
|
||||
result["end_state"] = kc.update_client_role_scope_from_realm(
|
||||
role_mapping_to_manipulate, objClient["id"], realm
|
||||
)
|
||||
result["msg"] = f"Client role scope for {target_client_id} has been updated"
|
||||
else:
|
||||
# doing delete
|
||||
if module.check_mode:
|
||||
result["end_state"] = desired_role_mapping
|
||||
elif role_owner_client_id:
|
||||
result["end_state"] = kc.delete_client_role_scope_from_client(
|
||||
role_mapping_to_manipulate, objClient["id"], role_owner_client["id"], realm
|
||||
)
|
||||
else:
|
||||
result["end_state"] = kc.delete_client_role_scope_from_realm(
|
||||
role_mapping_to_manipulate, objClient["id"], realm
|
||||
)
|
||||
result["msg"] = f"Client role scope for {target_client_id} has been deleted"
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024, Contributors to the middleware_automation.keycloak collection
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: keycloak_client_scope
|
||||
|
||||
short_description: Allows administration of Keycloak client scopes via Keycloak API
|
||||
|
||||
# Originally added in community.general 3.4.0 as keycloak_clientscope
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to add, remove or modify Keycloak client scopes via the Keycloak REST API.
|
||||
It requires access to the REST API via 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 the scope tailored
|
||||
to your needs and a user having the expected roles.
|
||||
|
||||
- This module also supports managing protocol mappers within a client scope.
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the client scope.
|
||||
- On V(present), the client scope will be created if it does not yet exist, or updated with the parameters you provide.
|
||||
- On V(absent), the client scope will be removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
- Name of the client scope.
|
||||
|
||||
description:
|
||||
type: str
|
||||
default: ''
|
||||
description:
|
||||
- Description of the client scope.
|
||||
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- The Keycloak realm under which this client scope resides.
|
||||
default: 'master'
|
||||
|
||||
protocol:
|
||||
type: str
|
||||
description:
|
||||
- The protocol associated with the client scope.
|
||||
default: 'openid-connect'
|
||||
choices:
|
||||
- openid-connect
|
||||
- saml
|
||||
|
||||
attributes:
|
||||
type: dict
|
||||
description:
|
||||
- A dict of key/value pairs to set as attributes for the client scope.
|
||||
|
||||
protocol_mappers:
|
||||
type: list
|
||||
elements: dict
|
||||
description:
|
||||
- A list of protocol mappers to associate with the client scope.
|
||||
- Each mapper is a dict with the keys C(name), C(protocol), C(protocolMapper), and C(config).
|
||||
default: []
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
- Name of the protocol mapper.
|
||||
protocol:
|
||||
type: str
|
||||
description:
|
||||
- Protocol for the mapper.
|
||||
default: 'openid-connect'
|
||||
protocolMapper:
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
- The mapper type (e.g. V(oidc-usermodel-attribute-mapper), V(oidc-audience-mapper)).
|
||||
aliases:
|
||||
- protocol_mapper_type
|
||||
config:
|
||||
type: dict
|
||||
required: true
|
||||
description:
|
||||
- Configuration for the protocol mapper.
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Paulo Menon (@paulomenon)
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create a client scope with protocol mappers
|
||||
middleware_automation.keycloak.keycloak_client_scope:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: TestRealm
|
||||
name: my-client-scope
|
||||
description: "A custom client scope"
|
||||
protocol: openid-connect
|
||||
protocol_mappers:
|
||||
- name: email
|
||||
protocol: openid-connect
|
||||
protocolMapper: oidc-usermodel-attribute-mapper
|
||||
config:
|
||||
user.attribute: email
|
||||
claim.name: email
|
||||
jsonType.label: String
|
||||
id.token.claim: "true"
|
||||
access.token.claim: "true"
|
||||
userinfo.token.claim: "true"
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a client scope using token authentication
|
||||
middleware_automation.keycloak.keycloak_client_scope:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
token: MY_TOKEN
|
||||
realm: TestRealm
|
||||
name: my-scope
|
||||
state: present
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete a client scope
|
||||
middleware_automation.keycloak.keycloak_client_scope:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: TestRealm
|
||||
name: my-client-scope
|
||||
state: absent
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Client scope my-scope has been created"
|
||||
|
||||
end_state:
|
||||
description: Representation of the client scope after module execution.
|
||||
returned: on success
|
||||
type: dict
|
||||
sample: {
|
||||
"id": "uuid-here",
|
||||
"name": "my-scope",
|
||||
"protocol": "openid-connect",
|
||||
"description": "A custom scope"
|
||||
}
|
||||
'''
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \
|
||||
keycloak_argument_spec, get_token, KeycloakError
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
mapper_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
protocol=dict(type='str', default='openid-connect'),
|
||||
protocolMapper=dict(type='str', required=True, aliases=['protocol_mapper_type']),
|
||||
config=dict(type='dict', required=True),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
name=dict(type='str', required=True),
|
||||
description=dict(type='str', default=''),
|
||||
realm=dict(type='str', default='master'),
|
||||
protocol=dict(type='str', default='openid-connect', choices=['openid-connect', 'saml']),
|
||||
attributes=dict(type='dict'),
|
||||
protocol_mappers=dict(type='list', default=[], options=mapper_spec, elements='dict'),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=([['token', 'auth_realm', 'auth_username', 'auth_password']]),
|
||||
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
|
||||
|
||||
result = dict(changed=False, msg='', diff={}, end_state={})
|
||||
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get('realm')
|
||||
name = module.params.get('name')
|
||||
state = module.params.get('state')
|
||||
protocol = module.params.get('protocol')
|
||||
description = module.params.get('description')
|
||||
attributes = module.params.get('attributes')
|
||||
protocol_mappers = module.params.get('protocol_mappers')
|
||||
|
||||
before_scope = kc.get_client_scope_by_name(name, realm=realm)
|
||||
|
||||
if state == 'absent':
|
||||
if before_scope:
|
||||
result['changed'] = True
|
||||
if module._diff:
|
||||
result['diff'] = dict(before=before_scope, after='')
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
kc.delete_client_scope(cid=before_scope['id'], realm=realm)
|
||||
result['msg'] = "Client scope {name} has been deleted".format(name=name)
|
||||
else:
|
||||
result['msg'] = "Client scope {name} does not exist, doing nothing".format(name=name)
|
||||
result['end_state'] = {}
|
||||
module.exit_json(**result)
|
||||
|
||||
scope_rep = {
|
||||
'name': name,
|
||||
'protocol': protocol,
|
||||
'description': description,
|
||||
}
|
||||
if attributes:
|
||||
scope_rep['attributes'] = attributes
|
||||
|
||||
if not before_scope:
|
||||
result['changed'] = True
|
||||
if module._diff:
|
||||
result['diff'] = dict(before='', after=scope_rep)
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
kc.create_client_scope(scope_rep, realm=realm)
|
||||
after_scope = kc.get_client_scope_by_name(name, realm=realm)
|
||||
|
||||
if protocol_mappers:
|
||||
for mapper in protocol_mappers:
|
||||
mapper_rep = {
|
||||
'name': mapper['name'],
|
||||
'protocol': mapper.get('protocol', protocol),
|
||||
'protocolMapper': mapper['protocolMapper'],
|
||||
'config': mapper['config'],
|
||||
}
|
||||
kc.create_client_scope_protocolmapper(after_scope['id'], mapper_rep, realm=realm)
|
||||
after_scope = kc.get_client_scope_by_name(name, realm=realm)
|
||||
|
||||
result['end_state'] = after_scope
|
||||
result['msg'] = "Client scope {name} has been created".format(name=name)
|
||||
module.exit_json(**result)
|
||||
|
||||
else:
|
||||
changed = False
|
||||
for key in ('protocol', 'description'):
|
||||
if scope_rep.get(key) and scope_rep[key] != before_scope.get(key):
|
||||
changed = True
|
||||
break
|
||||
|
||||
if attributes and attributes != before_scope.get('attributes', {}):
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
result['changed'] = True
|
||||
scope_rep['id'] = before_scope['id']
|
||||
if module._diff:
|
||||
result['diff'] = dict(before=before_scope, after=scope_rep)
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
kc.update_client_scope(scope_rep, realm=realm)
|
||||
|
||||
if protocol_mappers:
|
||||
existing_mappers = kc.get_client_scope_protocolmappers(before_scope['id'], realm=realm)
|
||||
existing_mapper_names = {m['name'] for m in existing_mappers}
|
||||
|
||||
for mapper in protocol_mappers:
|
||||
if mapper['name'] not in existing_mapper_names:
|
||||
result['changed'] = True
|
||||
if not module.check_mode:
|
||||
mapper_rep = {
|
||||
'name': mapper['name'],
|
||||
'protocol': mapper.get('protocol', protocol),
|
||||
'protocolMapper': mapper['protocolMapper'],
|
||||
'config': mapper['config'],
|
||||
}
|
||||
kc.create_client_scope_protocolmapper(before_scope['id'], mapper_rep, realm=realm)
|
||||
|
||||
after_scope = kc.get_client_scope_by_name(name, realm=realm)
|
||||
result['end_state'] = after_scope
|
||||
|
||||
if result['changed']:
|
||||
result['msg'] = "Client scope {name} has been updated".format(name=name)
|
||||
else:
|
||||
result['msg'] = "No changes required to client scope {name}".format(name=name)
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_client_scope_rolemappings
|
||||
|
||||
short_description: Allows administration of Keycloak client scope scope mappings to restrict the usage of certain roles to
|
||||
specific client scopes
|
||||
|
||||
# Originally added in community.general 13.1.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- 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.
|
||||
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.
|
||||
- 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
|
||||
suitable for the API.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 13.1.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the role mapping.
|
||||
- On V(present), all roles in O(role_names) are mapped if not exist yet.
|
||||
- On V(absent), all roles mapping in O(role_names) are removed if they exist.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- The Keycloak realm under which clients resides.
|
||||
default: 'master'
|
||||
|
||||
client_scope_id:
|
||||
required: true
|
||||
type: str
|
||||
description:
|
||||
- Roles provided in O(role_names) will be added to this client scope.
|
||||
|
||||
client_id:
|
||||
type: str
|
||||
description:
|
||||
- If the O(role_names) are client roles, the client ID under which it resides.
|
||||
- If this parameter is absent, the roles are considered realm roles.
|
||||
|
||||
role_names:
|
||||
required: true
|
||||
type: list
|
||||
elements: str
|
||||
description:
|
||||
- Names of roles to add.
|
||||
- If O(client_id) is present, all roles must be under this client.
|
||||
- If O(client_id) is absent, all roles must be under the realm.
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Felix Grzelka (@felix-grzelka)
|
||||
# This module was adapted from keycloak_client_rolescope, which was written by Andre Desrosiers (@desand01).
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Add roles to client scope
|
||||
middleware_automation.keycloak.keycloak_client_scope_rolemappings:
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: MyCustomRealm
|
||||
client_id: frontend-client-public
|
||||
client_scope_id: frontend-client-scope
|
||||
role_names:
|
||||
- backend-role-admin
|
||||
- backend-role-user
|
||||
|
||||
- name: Remove roles from client scope
|
||||
middleware_automation.keycloak.keycloak_client_scope_rolemappings:
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: MyCustomRealm
|
||||
client_id: frontend-client-public
|
||||
client_scope_id: frontend-client-scope
|
||||
role_names:
|
||||
- backend-role-admin
|
||||
state: absent
|
||||
|
||||
- name: Add realm roles to client scope
|
||||
middleware_automation.keycloak.keycloak_client_scope_rolemappings:
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: MyCustomRealm
|
||||
client_scope_id: frontend-client-scope
|
||||
role_names:
|
||||
- realm-role-admin
|
||||
- realm-role-user
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
end_state:
|
||||
description: Representation of client scope scope mappings after module execution.
|
||||
returned: on success
|
||||
type: list
|
||||
elements: dict
|
||||
sample:
|
||||
[
|
||||
{
|
||||
"clientRole": false,
|
||||
"composite": false,
|
||||
"containerId": "77f9bd4e-13a6-451e-9c72-ee6997299c1f",
|
||||
"description": "User role",
|
||||
"id": "9e155ef7-86f5-4def-b507-581ce7b87013",
|
||||
"name": "realm-role-user"
|
||||
},
|
||||
{
|
||||
"clientRole": false,
|
||||
"composite": false,
|
||||
"containerId": "77f9bd4e-13a6-451e-9c72-ee6997299c1f",
|
||||
"description": "Admin role",
|
||||
"id": "9e155ef7-86f5-4def-b507-581ce7b87013",
|
||||
"name": "realm-role-admin"
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
client_id=dict(type="str"),
|
||||
client_scope_id=dict(type="str", required=True),
|
||||
realm=dict(type="str", default="master"),
|
||||
role_names=dict(type="list", elements="str", required=True),
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params["realm"]
|
||||
client_id = module.params["client_id"]
|
||||
client_scope_id = module.params["client_scope_id"]
|
||||
role_names = module.params["role_names"]
|
||||
state = module.params["state"]
|
||||
|
||||
realm_object = kc.get_realm_by_id(realm)
|
||||
if not realm_object:
|
||||
module.fail_json(msg=f"Failed to retrieve realm '{realm}'")
|
||||
|
||||
client_scope_object = kc.get_client_scope_by_name(client_scope_id, realm)
|
||||
if not client_scope_object:
|
||||
module.fail_json(msg=f"Failed to retrieve client scope '{client_scope_id}'")
|
||||
|
||||
if client_id:
|
||||
# add client role
|
||||
client_object = kc.get_client_by_client_id(client_id, realm)
|
||||
if not client_object:
|
||||
module.fail_json(msg=f"Failed to retrieve client '{realm}.{client_id}'")
|
||||
if client_object["fullScopeAllowed"] and state == "present":
|
||||
module.fail_json(msg=f"FullScopeAllowed is active for Client '{realm}.{client_id}'")
|
||||
|
||||
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)
|
||||
else:
|
||||
# add realm role
|
||||
before_roles = kc.get_client_scope_scope_mappings_realm(client_scope_object["id"], realm)
|
||||
available_roles_by_name = kc.get_realm_roles(realm)
|
||||
|
||||
# convert to indexed Dict by name
|
||||
available_roles_by_name = {role["name"]: role for role in available_roles_by_name}
|
||||
before_roles_by_name = {role["name"]: role for role in before_roles}
|
||||
desired_roles = copy.deepcopy(before_roles)
|
||||
changed_roles = []
|
||||
|
||||
if state == "present":
|
||||
# update desired
|
||||
for role_name in role_names:
|
||||
if role_name not in available_roles_by_name:
|
||||
if client_id:
|
||||
module.fail_json(msg=f"Failed to retrieve role '{realm}.{client_id}.{role_name}'")
|
||||
else:
|
||||
module.fail_json(msg=f"Failed to retrieve role '{realm}.{role_name}'")
|
||||
if role_name not in before_roles_by_name:
|
||||
changed_roles.append(available_roles_by_name[role_name])
|
||||
desired_roles.append(available_roles_by_name[role_name])
|
||||
else:
|
||||
# remove role if present
|
||||
for role_name in role_names:
|
||||
if role_name in before_roles_by_name:
|
||||
changed_roles.append(before_roles_by_name[role_name])
|
||||
desired_roles.remove(available_roles_by_name[role_name])
|
||||
|
||||
before_roles = sorted(before_roles, key=lambda d: d["name"])
|
||||
desired_role_mapping = sorted(desired_roles, key=lambda d: d["name"])
|
||||
|
||||
result["changed"] = bool(changed_roles)
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before={"roles": before_roles}, after={"roles": desired_role_mapping})
|
||||
|
||||
if not result["changed"]:
|
||||
# no changes
|
||||
result["end_state"] = before_roles
|
||||
result["msg"] = f"No changes required for client scope {client_scope_id}."
|
||||
elif state == "present":
|
||||
# doing update
|
||||
if module.check_mode:
|
||||
result["end_state"] = desired_role_mapping
|
||||
elif client_id:
|
||||
result["end_state"] = kc.update_client_scope_scope_mappings_client(
|
||||
changed_roles, client_scope_object["id"], client_object["id"], realm
|
||||
)
|
||||
else:
|
||||
result["end_state"] = kc.update_client_scope_scope_mappings_realm(
|
||||
changed_roles, client_scope_object["id"], realm
|
||||
)
|
||||
result["msg"] = f"Clientscope scope mappings for {client_scope_id} have been updated"
|
||||
else:
|
||||
# doing delete
|
||||
if module.check_mode:
|
||||
result["end_state"] = desired_role_mapping
|
||||
elif client_id:
|
||||
result["end_state"] = kc.delete_client_scope_scope_mappings_client(
|
||||
changed_roles, client_scope_object["id"], client_object["id"], realm
|
||||
)
|
||||
else:
|
||||
result["end_state"] = kc.delete_client_scope_scope_mappings_realm(
|
||||
changed_roles, client_scope_object["id"], realm
|
||||
)
|
||||
result["msg"] = f"Clientscope scope mappings for {client_scope_id} have been deleted"
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_client_scope_type
|
||||
|
||||
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
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- 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.
|
||||
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.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- The Keycloak realm.
|
||||
default: 'master'
|
||||
|
||||
client_id:
|
||||
description:
|
||||
- The O(client_id) of the client. If not set the client scope types are set as a default for the realm.
|
||||
aliases:
|
||||
- clientId
|
||||
type: str
|
||||
|
||||
default_client_scopes:
|
||||
description:
|
||||
- Client scopes that should be of type default.
|
||||
type: list
|
||||
elements: str
|
||||
|
||||
optional_client_scopes:
|
||||
description:
|
||||
- Client scopes that should be of type optional.
|
||||
type: list
|
||||
elements: str
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Simon Pahl (@simonpahl)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Set default client scopes on realm level
|
||||
middleware_automation.keycloak.keycloak_client_scope_type:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: "MyCustomRealm"
|
||||
default_client_scopes: ['profile', 'roles']
|
||||
delegate_to: localhost
|
||||
|
||||
|
||||
- name: Set default and optional client scopes on client level with token auth
|
||||
middleware_automation.keycloak.keycloak_client_scope_type:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
realm: "MyCustomRealm"
|
||||
client_id: "MyCustomClient"
|
||||
default_client_scopes: ['profile', 'roles']
|
||||
optional_client_scopes: ['phone']
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
sample: ""
|
||||
proposed:
|
||||
description: Representation of proposed client scope types mapping.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"default_client_scopes": [
|
||||
"profile",
|
||||
"role"
|
||||
],
|
||||
"optional_client_scopes": []
|
||||
}
|
||||
existing:
|
||||
description:
|
||||
- Representation of client scopes before module execution.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"default_client_scopes": [
|
||||
"profile",
|
||||
"role"
|
||||
],
|
||||
"optional_client_scopes": [
|
||||
"phone"
|
||||
]
|
||||
}
|
||||
end_state:
|
||||
description:
|
||||
- Representation of client scopes after module execution.
|
||||
- The sample is truncated.
|
||||
returned: on success
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"default_client_scopes": [
|
||||
"profile",
|
||||
"role"
|
||||
],
|
||||
"optional_client_scopes": []
|
||||
}
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def keycloak_client_scope_type_module():
|
||||
"""
|
||||
Returns an AnsibleModule definition.
|
||||
|
||||
:return: argument_spec dict
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
realm=dict(default="master"),
|
||||
client_id=dict(type="str", aliases=["clientId"]),
|
||||
default_client_scopes=dict(type="list", elements="str"),
|
||||
optional_client_scopes=dict(type="list", elements="str"),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[
|
||||
["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"],
|
||||
["default_client_scopes", "optional_client_scopes"],
|
||||
]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
mutually_exclusive=[["token", "auth_realm"], ["token", "auth_username"], ["token", "auth_password"]],
|
||||
)
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def client_scopes_to_add(existing, proposed):
|
||||
to_add = []
|
||||
existing_client_scope_ids = extract_field(existing, "id")
|
||||
for client_scope in proposed:
|
||||
if client_scope["id"] not in existing_client_scope_ids:
|
||||
to_add.append(client_scope)
|
||||
return to_add
|
||||
|
||||
|
||||
def client_scopes_to_delete(existing, proposed):
|
||||
to_delete = []
|
||||
proposed_client_scope_ids = extract_field(proposed, "id")
|
||||
for client_scope in existing:
|
||||
if client_scope["id"] not in proposed_client_scope_ids:
|
||||
to_delete.append(client_scope)
|
||||
return to_delete
|
||||
|
||||
|
||||
def extract_field(dictionary, field="name"):
|
||||
return [cs[field] for cs in dictionary]
|
||||
|
||||
|
||||
def normalize_scopes(scopes):
|
||||
scopes_copy = scopes.copy()
|
||||
if isinstance(scopes_copy.get("default_client_scopes"), list):
|
||||
scopes_copy["default_client_scopes"] = sorted(scopes_copy["default_client_scopes"])
|
||||
if isinstance(scopes_copy.get("optional_client_scopes"), list):
|
||||
scopes_copy["optional_client_scopes"] = sorted(scopes_copy["optional_client_scopes"])
|
||||
return scopes_copy
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module keycloak_client_scope_type
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
module = keycloak_client_scope_type_module()
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
client_id = module.params.get("client_id")
|
||||
default_client_scopes = module.params.get("default_client_scopes")
|
||||
optional_client_scopes = module.params.get("optional_client_scopes")
|
||||
|
||||
result = dict(changed=False, msg="", proposed={}, existing={}, end_state={})
|
||||
|
||||
all_client_scopes = kc.get_client_scopes(realm)
|
||||
default_client_scopes_real = []
|
||||
optional_client_scopes_real = []
|
||||
|
||||
for client_scope in all_client_scopes:
|
||||
if default_client_scopes is not None and client_scope["name"] in default_client_scopes:
|
||||
default_client_scopes_real.append(client_scope)
|
||||
if optional_client_scopes is not None and client_scope["name"] in optional_client_scopes:
|
||||
optional_client_scopes_real.append(client_scope)
|
||||
|
||||
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_client_scopes does not exist!")
|
||||
|
||||
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_client_scopes does not exist!")
|
||||
|
||||
result["proposed"].update(
|
||||
{
|
||||
"default_client_scopes": "no-change" if default_client_scopes is None else default_client_scopes,
|
||||
"optional_client_scopes": "no-change" if optional_client_scopes is None else optional_client_scopes,
|
||||
}
|
||||
)
|
||||
|
||||
default_client_scopes_existing = kc.get_default_client_scopes(realm, client_id)
|
||||
optional_client_scopes_existing = kc.get_optional_client_scopes(realm, client_id)
|
||||
|
||||
result["existing"].update(
|
||||
{
|
||||
"default_client_scopes": extract_field(default_client_scopes_existing),
|
||||
"optional_client_scopes": extract_field(optional_client_scopes_existing),
|
||||
}
|
||||
)
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=normalize_scopes(result["existing"]), after=normalize_scopes(result["proposed"]))
|
||||
|
||||
default_client_scopes_add = client_scopes_to_add(default_client_scopes_existing, default_client_scopes_real)
|
||||
optional_client_scopes_add = client_scopes_to_add(optional_client_scopes_existing, optional_client_scopes_real)
|
||||
|
||||
default_client_scopes_delete = client_scopes_to_delete(default_client_scopes_existing, default_client_scopes_real)
|
||||
optional_client_scopes_delete = client_scopes_to_delete(optional_client_scopes_existing, optional_client_scopes_real)
|
||||
|
||||
result["changed"] = any(
|
||||
len(x) > 0
|
||||
for x in [
|
||||
default_client_scopes_add,
|
||||
optional_client_scopes_add,
|
||||
default_client_scopes_delete,
|
||||
optional_client_scopes_delete,
|
||||
]
|
||||
)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# first delete so client_scopes can change type
|
||||
for client_scope in default_client_scopes_delete:
|
||||
kc.delete_default_client_scope(client_scope["id"], realm, client_id)
|
||||
for client_scope in optional_client_scopes_delete:
|
||||
kc.delete_optional_client_scope(client_scope["id"], realm, client_id)
|
||||
|
||||
for client_scope in default_client_scopes_add:
|
||||
kc.add_default_client_scope(client_scope["id"], realm, client_id)
|
||||
for client_scope in optional_client_scopes_add:
|
||||
kc.add_optional_client_scope(client_scope["id"], realm, client_id)
|
||||
|
||||
result["end_state"].update(
|
||||
{
|
||||
"default_client_scopes": extract_field(kc.get_default_client_scopes(realm, client_id)),
|
||||
"optional_client_scopes": extract_field(kc.get_optional_client_scopes(realm, client_id)),
|
||||
}
|
||||
)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
|
||||
# Copyright (c) 2022, Fynn Chen <ethan.cfchen@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_clientsecret_info
|
||||
|
||||
short_description: Retrieve client secret using Keycloak API
|
||||
|
||||
# Originally added in community.general 6.1.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to get a Keycloak client secret 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. 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.
|
||||
- When retrieving a new client secret, where possible provide the client's O(id) (not O(client_id)) to the module. This
|
||||
removes a lookup to the API to translate the O(client_id) into the client ID.
|
||||
- 'Note that this module returns the client secret. To avoid this showing up in the logs, please add C(no_log: true) to
|
||||
the task.'
|
||||
attributes:
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- They Keycloak realm under which this client resides.
|
||||
default: 'master'
|
||||
|
||||
id:
|
||||
description:
|
||||
- The unique identifier for this client.
|
||||
- This parameter is not required for getting or generating a client secret but providing it reduces the number of API
|
||||
calls required.
|
||||
type: str
|
||||
|
||||
client_id:
|
||||
description:
|
||||
- The O(client_id) of the client. Passing this instead of O(id) results in an extra API call.
|
||||
aliases:
|
||||
- clientId
|
||||
type: str
|
||||
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
- middleware_automation.keycloak.attributes.info_module
|
||||
|
||||
author:
|
||||
- Fynn Chen (@fynncfchen)
|
||||
- John Cant (@johncant)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Get a Keycloak client secret, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_clientsecret_info:
|
||||
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
no_log: true
|
||||
|
||||
- name: Get a new Keycloak client secret, authentication with token
|
||||
middleware_automation.keycloak.keycloak_clientsecret_info:
|
||||
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
delegate_to: localhost
|
||||
no_log: true
|
||||
|
||||
- name: Get a new Keycloak client secret, passing client_id instead of id
|
||||
middleware_automation.keycloak.keycloak_clientsecret_info:
|
||||
client_id: 'myClientId'
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
delegate_to: localhost
|
||||
no_log: true
|
||||
|
||||
- name: Get a new Keycloak client secret, authentication with auth_client_id and auth_client_secret
|
||||
middleware_automation.keycloak.keycloak_clientsecret_info:
|
||||
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_client_secret: SECRET
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
delegate_to: localhost
|
||||
no_log: true
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Textual description of whether we succeeded or failed.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
clientsecret_info:
|
||||
description: Representation of the client secret.
|
||||
returned: on success
|
||||
type: complex
|
||||
contains:
|
||||
type:
|
||||
description: Credential type.
|
||||
type: str
|
||||
returned: always
|
||||
sample: secret
|
||||
value:
|
||||
description: Client secret.
|
||||
type: str
|
||||
returned: always
|
||||
sample: cUGnX1EIeTtPPAkcyGMv0ncyqDPu68P1
|
||||
"""
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
)
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak_clientsecret import (
|
||||
keycloak_clientsecret_module,
|
||||
keycloak_clientsecret_module_resolve_params,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module keycloak_clientsecret_info
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
module = keycloak_clientsecret_module()
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
id, realm = keycloak_clientsecret_module_resolve_params(module, kc)
|
||||
|
||||
clientsecret = kc.get_clientsecret(id=id, realm=realm)
|
||||
|
||||
result = {"clientsecret_info": clientsecret, "msg": f"Get client secret successful for ID {id}"}
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
|
||||
# Copyright (c) 2022, Fynn Chen <ethan.cfchen@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_clientsecret_regenerate
|
||||
|
||||
short_description: Regenerate Keycloak client secret using Keycloak API
|
||||
|
||||
# Originally added in community.general 6.1.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to regenerate a Keycloak client secret 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. 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.
|
||||
- When regenerating a client secret, where possible provide the client's ID (not client_id) to the module. This removes
|
||||
a lookup to the API to translate the client_id into the client ID.
|
||||
- 'Note that this module returns the client secret. To avoid this showing up in the logs, please add C(no_log: true) to
|
||||
the task.'
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- They Keycloak realm under which this client resides.
|
||||
default: 'master'
|
||||
|
||||
id:
|
||||
description:
|
||||
- The unique identifier for this client.
|
||||
- This parameter is not required for getting or generating a client secret but providing it reduces the number of API
|
||||
calls required.
|
||||
type: str
|
||||
|
||||
client_id:
|
||||
description:
|
||||
- The client_id of the client. Passing this instead of ID results in an extra API call.
|
||||
aliases:
|
||||
- clientId
|
||||
type: str
|
||||
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Fynn Chen (@fynncfchen)
|
||||
- John Cant (@johncant)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Regenerate a Keycloak client secret, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_clientsecret_regenerate:
|
||||
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
no_log: true
|
||||
|
||||
- name: Regenerate a Keycloak client secret, authentication with token
|
||||
middleware_automation.keycloak.keycloak_clientsecret_regenerate:
|
||||
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
delegate_to: localhost
|
||||
no_log: true
|
||||
|
||||
- name: Regenerate a Keycloak client secret, passing client_id instead of id
|
||||
middleware_automation.keycloak.keycloak_clientsecret_info:
|
||||
client_id: 'myClientId'
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
delegate_to: localhost
|
||||
no_log: true
|
||||
|
||||
- name: Regenerate a new Keycloak client secret, authentication with auth_client_id and auth_client_secret
|
||||
middleware_automation.keycloak.keycloak_clientsecret_regenerate:
|
||||
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_client_secret: SECRET
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
delegate_to: localhost
|
||||
no_log: true
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
end_state:
|
||||
description: Representation of the client credential after module execution.
|
||||
returned: on success
|
||||
type: complex
|
||||
contains:
|
||||
type:
|
||||
description: Credential type.
|
||||
type: str
|
||||
returned: always
|
||||
sample: secret
|
||||
value:
|
||||
description: Client secret.
|
||||
type: str
|
||||
returned: always
|
||||
sample: cUGnX1EIeTtPPAkcyGMv0ncyqDPu68P1
|
||||
"""
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
)
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak_clientsecret import (
|
||||
keycloak_clientsecret_module,
|
||||
keycloak_clientsecret_module_resolve_params,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module keycloak_clientsecret_regenerate
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
module = keycloak_clientsecret_module()
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
id, realm = keycloak_clientsecret_module_resolve_params(module, kc)
|
||||
|
||||
if module.check_mode:
|
||||
dummy_result = {
|
||||
"msg": "No action taken while in check mode",
|
||||
"end_state": {"type": "secret", "value": "X" * 32},
|
||||
}
|
||||
module.exit_json(**dummy_result)
|
||||
|
||||
# Create new secret
|
||||
clientsecret = kc.create_clientsecret(id=id, realm=realm)
|
||||
|
||||
result = {"msg": f"New client secret has been generated for ID {id}", "end_state": clientsecret}
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
|
||||
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_clienttemplate
|
||||
|
||||
short_description: Allows administration of Keycloak client templates using Keycloak API
|
||||
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows the administration of Keycloak client templates 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.
|
||||
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 names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/latest/rest-api/index.html).
|
||||
- The Keycloak API does not always enforce for only sensible settings to be used -- you can set SAML-specific settings on
|
||||
an OpenID Connect client for instance and the other way around. Be careful. If you do not specify a setting, usually a
|
||||
sensible default is chosen.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the client template.
|
||||
- On V(present), the client template is created (or updated if it exists already).
|
||||
- On V(absent), the client template is removed if it exists.
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
type: str
|
||||
|
||||
id:
|
||||
description:
|
||||
- ID of client template to be worked on. This is usually a UUID.
|
||||
type: str
|
||||
|
||||
realm:
|
||||
description:
|
||||
- Realm this client template is found in.
|
||||
type: str
|
||||
default: master
|
||||
|
||||
name:
|
||||
description:
|
||||
- Name of the client template.
|
||||
type: str
|
||||
|
||||
description:
|
||||
description:
|
||||
- Description of the client template in Keycloak.
|
||||
type: str
|
||||
|
||||
protocol:
|
||||
description:
|
||||
- Type of client template.
|
||||
- The V(docker-v2) value was added in middleware_automation.keycloak 8.6.0.
|
||||
choices: ['openid-connect', 'saml', 'docker-v2']
|
||||
type: str
|
||||
|
||||
full_scope_allowed:
|
||||
description:
|
||||
- Is the "Full Scope Allowed" feature set for this client template or not. This is C(fullScopeAllowed) in the Keycloak
|
||||
REST API.
|
||||
type: bool
|
||||
|
||||
protocol_mappers:
|
||||
description:
|
||||
- A list of dicts defining protocol mappers for this client template. This is C(protocolMappers) in the Keycloak REST
|
||||
API.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
consentRequired:
|
||||
description:
|
||||
- Specifies whether a user needs to provide consent to a client for this mapper to be active.
|
||||
type: bool
|
||||
|
||||
consentText:
|
||||
description:
|
||||
- The human-readable name of the consent the user is presented to accept.
|
||||
type: str
|
||||
|
||||
id:
|
||||
description:
|
||||
- Usually a UUID specifying the internal ID of this protocol mapper instance.
|
||||
type: str
|
||||
|
||||
name:
|
||||
description:
|
||||
- The name of this protocol mapper.
|
||||
type: str
|
||||
|
||||
protocol:
|
||||
description:
|
||||
- This specifies for which protocol this protocol mapper is active.
|
||||
choices: ['openid-connect', 'saml', 'docker-v2']
|
||||
type: str
|
||||
|
||||
protocolMapper:
|
||||
description:
|
||||
- 'The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is impossible to provide
|
||||
since this may be extended through SPIs by the user of Keycloak, by default Keycloak as of 3.4 ships with at least:'
|
||||
- V(docker-v2-allow-all-mapper).
|
||||
- V(oidc-address-mapper).
|
||||
- V(oidc-full-name-mapper).
|
||||
- V(oidc-group-membership-mapper).
|
||||
- V(oidc-hardcoded-claim-mapper).
|
||||
- V(oidc-hardcoded-role-mapper).
|
||||
- V(oidc-role-name-mapper).
|
||||
- V(oidc-script-based-protocol-mapper).
|
||||
- V(oidc-sha256-pairwise-sub-mapper).
|
||||
- V(oidc-usermodel-attribute-mapper).
|
||||
- V(oidc-usermodel-client-role-mapper).
|
||||
- V(oidc-usermodel-property-mapper).
|
||||
- V(oidc-usermodel-realm-role-mapper).
|
||||
- V(oidc-usersessionmodel-note-mapper).
|
||||
- V(saml-group-membership-mapper).
|
||||
- V(saml-hardcode-attribute-mapper).
|
||||
- V(saml-hardcode-role-mapper).
|
||||
- V(saml-role-list-mapper).
|
||||
- V(saml-role-name-mapper).
|
||||
- V(saml-user-attribute-mapper).
|
||||
- V(saml-user-property-mapper).
|
||||
- V(saml-user-session-note-mapper).
|
||||
- An exhaustive list of available mappers on your installation can be obtained on the admin console by going to
|
||||
Server Info -> Providers and looking under 'protocol-mapper'.
|
||||
type: str
|
||||
|
||||
config:
|
||||
description:
|
||||
- Dict specifying the configuration options for the protocol mapper; the contents differ depending on the value
|
||||
of O(protocol_mappers[].protocolMapper) and are not documented other than by the source of the mappers and its
|
||||
parent class(es). An example is given below. It is easiest to obtain valid config values by dumping an already-existing
|
||||
protocol mapper configuration through check-mode in the RV(existing) field.
|
||||
type: dict
|
||||
|
||||
attributes:
|
||||
description:
|
||||
- A dict of further attributes for this client template. This can contain various configuration settings, though in
|
||||
the default installation of Keycloak as of 3.4, none are documented or known, so this is usually empty.
|
||||
type: dict
|
||||
|
||||
notes:
|
||||
- The Keycloak REST API defines further fields (namely C(bearerOnly), C(consentRequired), C(standardFlowEnabled), C(implicitFlowEnabled),
|
||||
C(directAccessGrantsEnabled), C(serviceAccountsEnabled), C(publicClient), and C(frontchannelLogout)) which, while available
|
||||
with keycloak_client, do not have any effect on Keycloak client-templates and are discarded if supplied with an API request
|
||||
changing client-templates. As such, they are not available through this module.
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Eike Frost (@eikef)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create or update Keycloak client template (minimal), authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_client:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: master
|
||||
name: this_is_a_test
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create or update Keycloak client template (minimal), authentication with token
|
||||
middleware_automation.keycloak.keycloak_clienttemplate:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
token: TOKEN
|
||||
realm: master
|
||||
name: this_is_a_test
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete Keycloak client template
|
||||
middleware_automation.keycloak.keycloak_client:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: master
|
||||
state: absent
|
||||
name: test01
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create or update Keycloak client template (with a protocol mapper)
|
||||
middleware_automation.keycloak.keycloak_client:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: master
|
||||
name: this_is_a_test
|
||||
protocol_mappers:
|
||||
- config:
|
||||
access.token.claim: true
|
||||
claim.name: "family_name"
|
||||
id.token.claim: true
|
||||
jsonType.label: String
|
||||
user.attribute: lastName
|
||||
userinfo.token.claim: true
|
||||
consentRequired: true
|
||||
consentText: "${familyName}"
|
||||
name: family name
|
||||
protocol: openid-connect
|
||||
protocolMapper: oidc-usermodel-property-mapper
|
||||
full_scope_allowed: false
|
||||
id: bce6f5e9-d7d3-4955-817e-c5b7f8d65b3f
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Client template testclient has been updated"
|
||||
|
||||
proposed:
|
||||
description: Representation of proposed client template.
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {"name": "test01"}
|
||||
|
||||
existing:
|
||||
description: Representation of existing client template (sample is truncated).
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"description": "test01",
|
||||
"fullScopeAllowed": false,
|
||||
"id": "9c3712ab-decd-481e-954f-76da7b006e5f",
|
||||
"name": "test01",
|
||||
"protocol": "saml"
|
||||
}
|
||||
|
||||
end_state:
|
||||
description: Representation of client template after module execution (sample is truncated).
|
||||
returned: on success
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"description": "test01",
|
||||
"fullScopeAllowed": false,
|
||||
"id": "9c3712ab-decd-481e-954f-76da7b006e5f",
|
||||
"name": "test01",
|
||||
"protocol": "saml"
|
||||
}
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
camel,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
protmapper_spec = dict(
|
||||
consentRequired=dict(type="bool"),
|
||||
consentText=dict(type="str"),
|
||||
id=dict(type="str"),
|
||||
name=dict(type="str"),
|
||||
protocol=dict(type="str", choices=["openid-connect", "saml", "docker-v2"]),
|
||||
protocolMapper=dict(type="str"),
|
||||
config=dict(type="dict"),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
realm=dict(type="str", default="master"),
|
||||
state=dict(default="present", choices=["present", "absent"]),
|
||||
id=dict(type="str"),
|
||||
name=dict(type="str"),
|
||||
description=dict(type="str"),
|
||||
protocol=dict(type="str", choices=["openid-connect", "saml", "docker-v2"]),
|
||||
attributes=dict(type="dict"),
|
||||
full_scope_allowed=dict(type="bool"),
|
||||
protocol_mappers=dict(type="list", elements="dict", options=protmapper_spec),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[
|
||||
["id", "name"],
|
||||
["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"],
|
||||
]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, proposed={}, existing={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
state = module.params.get("state")
|
||||
cid = module.params.get("id")
|
||||
|
||||
# Filter and map the parameters names that apply to the client template
|
||||
clientt_params = [
|
||||
x
|
||||
for x in module.params
|
||||
if x
|
||||
not in [
|
||||
"state",
|
||||
"auth_keycloak_url",
|
||||
"auth_client_id",
|
||||
"auth_realm",
|
||||
"auth_client_secret",
|
||||
"auth_username",
|
||||
"auth_password",
|
||||
"validate_certs",
|
||||
"realm",
|
||||
]
|
||||
and module.params.get(x) is not None
|
||||
]
|
||||
|
||||
# See if it already exists in Keycloak
|
||||
if cid is None:
|
||||
before_clientt = kc.get_client_template_by_name(module.params.get("name"), realm=realm)
|
||||
if before_clientt is not None:
|
||||
cid = before_clientt["id"]
|
||||
else:
|
||||
before_clientt = kc.get_client_template_by_id(cid, realm=realm)
|
||||
|
||||
if before_clientt is None:
|
||||
before_clientt = {}
|
||||
|
||||
result["existing"] = before_clientt
|
||||
|
||||
# Build a proposed changeset from parameters given to this module
|
||||
changeset = {}
|
||||
|
||||
for clientt_param in clientt_params:
|
||||
# lists in the Keycloak API are sorted
|
||||
new_param_value = module.params.get(clientt_param)
|
||||
if isinstance(new_param_value, list):
|
||||
try:
|
||||
new_param_value = sorted(new_param_value)
|
||||
except TypeError:
|
||||
pass
|
||||
changeset[camel(clientt_param)] = new_param_value
|
||||
|
||||
# Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
|
||||
desired_clientt = before_clientt.copy()
|
||||
desired_clientt.update(changeset)
|
||||
|
||||
result["proposed"] = changeset
|
||||
|
||||
# Cater for when it doesn't exist (an empty dict)
|
||||
if not before_clientt:
|
||||
if state == "absent":
|
||||
# Do nothing and exit
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after="")
|
||||
result["changed"] = False
|
||||
result["end_state"] = {}
|
||||
result["msg"] = "Client template does not exist, doing nothing."
|
||||
module.exit_json(**result)
|
||||
|
||||
# Process a creation
|
||||
result["changed"] = True
|
||||
|
||||
if "name" not in desired_clientt:
|
||||
module.fail_json(msg="name needs to be specified when creating a new client")
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after=desired_clientt)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# create it
|
||||
kc.create_client_template(desired_clientt, realm=realm)
|
||||
after_clientt = kc.get_client_template_by_name(desired_clientt["name"], realm=realm)
|
||||
|
||||
result["end_state"] = after_clientt
|
||||
|
||||
result["msg"] = f"Client template {desired_clientt['name']} has been created."
|
||||
module.exit_json(**result)
|
||||
|
||||
else:
|
||||
if state == "present":
|
||||
# Process an update
|
||||
|
||||
result["changed"] = True
|
||||
if module.check_mode:
|
||||
# We can only compare the current client template with the proposed updates we have
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_clientt, after=desired_clientt)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
# do the update
|
||||
kc.update_client_template(cid, desired_clientt, realm=realm)
|
||||
|
||||
after_clientt = kc.get_client_template_by_id(cid, realm=realm)
|
||||
if before_clientt == after_clientt:
|
||||
result["changed"] = False
|
||||
|
||||
result["end_state"] = after_clientt
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_clientt, after=after_clientt)
|
||||
|
||||
result["msg"] = f"Client template {desired_clientt['name']} has been updated."
|
||||
module.exit_json(**result)
|
||||
|
||||
else:
|
||||
# Process a deletion (because state was not 'present')
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_clientt, after="")
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# delete it
|
||||
kc.delete_client_template(cid, realm=realm)
|
||||
result["proposed"] = {}
|
||||
|
||||
result["end_state"] = {}
|
||||
|
||||
result["msg"] = f"Client template {before_clientt['name']} has been deleted."
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
|
||||
# Copyright (c) 2024, Björn Bösel <bjoernboesel@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
|
||||
# https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_component
|
||||
|
||||
short_description: Allows administration of Keycloak components using Keycloak API
|
||||
|
||||
# Originally added in community.general 10.0.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows the administration of Keycloak components using the Keycloak REST API. It requires access to the REST
|
||||
API using OpenID Connect; the user connecting and the realm being used must have the requisite access rights. In a default
|
||||
Keycloak installation, C(admin-cli) and an C(admin) user would work, as would a separate realm definition with the scope
|
||||
tailored to your needs and a user having the expected roles.
|
||||
- The names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/latest/rest-api/index.html). Aliases are provided so camelCased versions can be
|
||||
used as well.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the Keycloak component.
|
||||
- On V(present), the component is created (or updated if it exists already).
|
||||
- On V(absent), the component is removed if it exists.
|
||||
choices: ['present', 'absent']
|
||||
default: 'present'
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Name of the component to create.
|
||||
type: str
|
||||
required: true
|
||||
parent_id:
|
||||
description:
|
||||
- The parent_id of the component. In practice the ID (name) of the realm.
|
||||
type: str
|
||||
required: true
|
||||
provider_id:
|
||||
description:
|
||||
- The name of the "provider ID" for the key.
|
||||
type: str
|
||||
required: true
|
||||
provider_type:
|
||||
description:
|
||||
- The name of the "provider type" for the key. That is, V(org.keycloak.storage.UserStorageProvider), V(org.keycloak.userprofile.UserProfileProvider),
|
||||
...
|
||||
- See U(https://www.keycloak.org/docs/latest/server_development/index.html#_providers).
|
||||
type: str
|
||||
required: true
|
||||
config:
|
||||
description:
|
||||
- Configuration properties for the provider.
|
||||
- Contents vary depending on the provider type.
|
||||
type: dict
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Björn Bösel (@fivetide)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Manage Keycloak User Storage Provider
|
||||
middleware_automation.keycloak.keycloak_component:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: keycloak
|
||||
auth_password: keycloak
|
||||
auth_realm: master
|
||||
name: my storage provider
|
||||
state: present
|
||||
parent_id: some_realm
|
||||
provider_id: my storage
|
||||
provider_type: "org.keycloak.storage.UserStorageProvider"
|
||||
config:
|
||||
myCustomKey: "my_custom_key"
|
||||
cachePolicy: "NO_CACHE"
|
||||
enabled: true
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
end_state:
|
||||
description: Representation of the keycloak_component after module execution.
|
||||
returned: on success
|
||||
type: dict
|
||||
contains:
|
||||
id:
|
||||
description: ID of the component.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: 5b7ec13f-99da-46ad-8326-ab4c73cf4ce4
|
||||
name:
|
||||
description: Name of the component.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: mykey
|
||||
parentId:
|
||||
description: ID of the realm this key belongs to.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: myrealm
|
||||
providerId:
|
||||
description: The ID of the key provider.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
sample: rsa
|
||||
providerType:
|
||||
description: The type of provider.
|
||||
type: str
|
||||
returned: when O(state=present)
|
||||
config:
|
||||
description: Component configuration.
|
||||
type: dict
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
camel,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
name=dict(type="str", required=True),
|
||||
parent_id=dict(type="str", required=True),
|
||||
provider_id=dict(type="str", required=True),
|
||||
provider_type=dict(type="str", required=True),
|
||||
config=dict(
|
||||
type="dict",
|
||||
),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", end_state={}, diff=dict(before={}, after={}))
|
||||
|
||||
# This will include the current state of the component if it is already
|
||||
# present. This is only used for diff-mode.
|
||||
before_component = {}
|
||||
before_component["config"] = {}
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
params_to_ignore = list(keycloak_argument_spec().keys()) + ["state", "parent_id"]
|
||||
|
||||
# Filter and map the parameters names that apply to the role
|
||||
component_params = [x for x in module.params if x not in params_to_ignore and module.params.get(x) is not None]
|
||||
|
||||
provider_type = module.params.get("provider_type")
|
||||
|
||||
# Build a proposed changeset from parameters given to this module
|
||||
changeset = {}
|
||||
changeset["config"] = {}
|
||||
|
||||
# Generate a JSON payload for Keycloak Admin API from the module
|
||||
# parameters. Parameters that do not belong to the JSON payload (e.g.
|
||||
# "state" or "auth_keycloal_url") have been filtered away earlier (see
|
||||
# above).
|
||||
#
|
||||
# This loop converts Ansible module parameters (snake-case) into
|
||||
# Keycloak-compatible format (camel-case). For example private_key
|
||||
# becomes privateKey.
|
||||
#
|
||||
# It also converts bool, str and int parameters into lists with a single
|
||||
# entry of 'str' type. Bool values are also lowercased. This is required
|
||||
# by Keycloak.
|
||||
#
|
||||
for component_param in component_params:
|
||||
if component_param == "config":
|
||||
for config_param in module.params.get("config"):
|
||||
changeset["config"][camel(config_param)] = []
|
||||
raw_value = module.params.get("config")[config_param]
|
||||
if isinstance(raw_value, bool):
|
||||
value = str(raw_value).lower()
|
||||
else:
|
||||
value = str(raw_value)
|
||||
|
||||
changeset["config"][camel(config_param)].append(value)
|
||||
else:
|
||||
# No need for camelcase in here as these are one word parameters
|
||||
new_param_value = module.params.get(component_param)
|
||||
changeset[camel(component_param)] = new_param_value
|
||||
|
||||
# Make a deep copy of the changeset. This is use when determining
|
||||
# changes to the current state.
|
||||
changeset_copy = deepcopy(changeset)
|
||||
|
||||
# Make it easier to refer to current module parameters
|
||||
name = module.params.get("name")
|
||||
state = module.params.get("state")
|
||||
provider_type = module.params.get("provider_type")
|
||||
parent_id = module.params.get("parent_id")
|
||||
|
||||
# Get a list of all Keycloak components that are of keyprovider type.
|
||||
current_components = kc.get_components(urlencode(dict(type=provider_type)), parent_id)
|
||||
|
||||
# If this component is present get its key ID. Confusingly the key ID is
|
||||
# also known as the Provider ID.
|
||||
component_id = None
|
||||
|
||||
# Track individual parameter changes
|
||||
changes = ""
|
||||
|
||||
# This tells Ansible whether the key was changed (added, removed, modified)
|
||||
result["changed"] = False
|
||||
|
||||
# Loop through the list of components. If we encounter a component whose
|
||||
# name matches the value of the name parameter then assume the key is
|
||||
# already present.
|
||||
for component in current_components:
|
||||
if component["name"] == name:
|
||||
component_id = component["id"]
|
||||
changeset["id"] = component_id
|
||||
changeset_copy["id"] = component_id
|
||||
|
||||
# Compare top-level parameters
|
||||
for param in changeset:
|
||||
before_component[param] = component[param]
|
||||
|
||||
if changeset_copy[param] != component[param] and param != "config":
|
||||
changes += f"{param}: {component[param]} -> {changeset_copy[param]}, "
|
||||
result["changed"] = True
|
||||
# Compare parameters under the "config" key
|
||||
for p, v in changeset_copy["config"].items():
|
||||
try:
|
||||
before_component["config"][p] = component["config"][p] or []
|
||||
except KeyError:
|
||||
before_component["config"][p] = []
|
||||
if v != component["config"][p]:
|
||||
changes += f"config.{p}: {component['config'][p]} -> {v}, "
|
||||
result["changed"] = True
|
||||
|
||||
# Check all the possible states of the resource and do what is needed to
|
||||
# converge current state with desired state (create, update or delete
|
||||
# the key).
|
||||
if component_id and state == "present":
|
||||
if result["changed"]:
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_component, after=changeset_copy)
|
||||
|
||||
if module.check_mode:
|
||||
result["msg"] = f"Component {name} would be changed: {changes.strip(', ')}"
|
||||
else:
|
||||
kc.update_component(changeset, parent_id)
|
||||
result["msg"] = f"Component {name} changed: {changes.strip(', ')}"
|
||||
else:
|
||||
result["msg"] = f"Component {name} was in sync"
|
||||
|
||||
result["end_state"] = changeset_copy
|
||||
elif component_id and state == "absent":
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_component, after={})
|
||||
|
||||
if module.check_mode:
|
||||
result["changed"] = True
|
||||
result["msg"] = f"Component {name} would be deleted"
|
||||
else:
|
||||
kc.delete_component(component_id, parent_id)
|
||||
result["changed"] = True
|
||||
result["msg"] = f"Component {name} deleted"
|
||||
|
||||
result["end_state"] = {}
|
||||
elif not component_id and state == "present":
|
||||
if module._diff:
|
||||
result["diff"] = dict(before={}, after=changeset_copy)
|
||||
|
||||
if module.check_mode:
|
||||
result["changed"] = True
|
||||
result["msg"] = f"Component {name} would be created"
|
||||
else:
|
||||
kc.create_component(changeset, parent_id)
|
||||
result["changed"] = True
|
||||
result["msg"] = f"Component {name} created"
|
||||
|
||||
result["end_state"] = changeset_copy
|
||||
elif not component_id and state == "absent":
|
||||
result["changed"] = False
|
||||
result["msg"] = f"Component {name} not present"
|
||||
result["end_state"] = {}
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_component_info
|
||||
|
||||
short_description: Retrieve component info in Keycloak
|
||||
|
||||
# Originally added in community.general 8.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module retrieve information on component from Keycloak.
|
||||
attributes:
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
realm:
|
||||
description:
|
||||
- The name of the realm.
|
||||
required: true
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Name of the Component.
|
||||
type: str
|
||||
provider_type:
|
||||
description:
|
||||
- Provider type of components.
|
||||
- 'Examples: V(org.keycloak.storage.UserStorageProvider), V(org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy),
|
||||
V(org.keycloak.keys.KeyProvider), V(org.keycloak.userprofile.UserProfileProvider), V(org.keycloak.storage.ldap.mappers.LDAPStorageMapper).'
|
||||
type: str
|
||||
parent_id:
|
||||
description:
|
||||
- Container ID of the components.
|
||||
type: str
|
||||
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
- middleware_automation.keycloak.attributes.info_module
|
||||
|
||||
author:
|
||||
- Andre Desrosiers (@desand01)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Retrive info of a UserStorageProvider named myldap
|
||||
middleware_automation.keycloak.keycloak_component_info:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
auth_realm: master
|
||||
realm: myrealm
|
||||
name: myldap
|
||||
provider_type: org.keycloak.storage.UserStorageProvider
|
||||
|
||||
- name: Retrive key info component
|
||||
middleware_automation.keycloak.keycloak_component_info:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
auth_realm: master
|
||||
realm: myrealm
|
||||
name: rsa-enc-generated
|
||||
provider_type: org.keycloak.keys.KeyProvider
|
||||
|
||||
- name: Retrive all component from realm master
|
||||
middleware_automation.keycloak.keycloak_component_info:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
auth_realm: master
|
||||
realm: myrealm
|
||||
|
||||
- name: Retrive all sub components of parent component filter by type
|
||||
middleware_automation.keycloak.keycloak_component_info:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
auth_realm: master
|
||||
realm: myrealm
|
||||
parent_id: "075ef2fa-19fc-4a6d-bf4c-249f57365fd2"
|
||||
provider_type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
components:
|
||||
description: JSON representation of components.
|
||||
returned: always
|
||||
type: list
|
||||
elements: dict
|
||||
"""
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
name=dict(type="str"),
|
||||
realm=dict(type="str", required=True),
|
||||
parent_id=dict(type="str"),
|
||||
provider_type=dict(type="str"),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
result = dict(changed=False, components=[])
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
parentId = module.params.get("parent_id")
|
||||
name = module.params.get("name")
|
||||
providerType = module.params.get("provider_type")
|
||||
|
||||
objRealm = kc.get_realm_by_id(realm)
|
||||
if not objRealm:
|
||||
module.fail_json(msg=f"Failed to retrive realm '{realm}'")
|
||||
|
||||
filters = []
|
||||
|
||||
if parentId:
|
||||
filters.append(f"parent={quote(parentId, safe='')}")
|
||||
else:
|
||||
filters.append(f"parent={quote(objRealm['id'], safe='')}")
|
||||
|
||||
if name:
|
||||
filters.append(f"name={quote(name, safe='')}")
|
||||
if providerType:
|
||||
filters.append(f"type={quote(providerType, safe='')}")
|
||||
|
||||
result["components"] = kc.get_components(filter="&".join(filters), realm=realm)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,495 @@
|
|||
|
||||
# Copyright (c) 2019, Adam Goossens <adam.goossens@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_group
|
||||
|
||||
short_description: Allows administration of Keycloak groups using Keycloak API
|
||||
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to add, remove or modify Keycloak groups 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. 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 names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/20.0.2/rest-api/index.html).
|
||||
- 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
|
||||
suitable for the API.
|
||||
- When updating a group, where possible provide the group ID to the module. This removes a lookup to the API to translate
|
||||
the name into the group ID.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the group.
|
||||
- On V(present), the group is created if it does not yet exist, or updated with the parameters you provide.
|
||||
- On V(absent), the group is removed if it exists. Be aware that absenting a group with subgroups automatically deletes
|
||||
all its subgroups too.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
name:
|
||||
type: str
|
||||
description:
|
||||
- Name of the group.
|
||||
- This parameter is required only when creating or updating the group.
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- They Keycloak realm under which this group resides.
|
||||
default: 'master'
|
||||
|
||||
id:
|
||||
type: str
|
||||
description:
|
||||
- The unique identifier for this group.
|
||||
- This parameter is not required for updating or deleting a group but providing it reduces the number of API calls required.
|
||||
attributes:
|
||||
type: dict
|
||||
description:
|
||||
- A dict of key/value pairs to set as custom attributes for the group.
|
||||
- Values may be single values (for example a string) or a list of strings.
|
||||
parents:
|
||||
type: list
|
||||
description:
|
||||
- List of parent groups for the group to handle sorted top to bottom.
|
||||
- Set this to create a group as a subgroup of another group or groups (parents) or when accessing an existing subgroup
|
||||
by name.
|
||||
- Not necessary to set when accessing an existing subgroup by its C(ID) because in that case the group can be directly
|
||||
queried without necessarily knowing its parent(s).
|
||||
elements: dict
|
||||
suboptions:
|
||||
id:
|
||||
type: str
|
||||
description:
|
||||
- Identify parent by ID.
|
||||
- Needs less API calls than using O(parents[].name).
|
||||
- A deep parent chain can be started at any point when first given parent is given as ID.
|
||||
- Note that in principle both ID and name can be specified at the same time but current implementation only always
|
||||
use just one of them, with ID being preferred.
|
||||
name:
|
||||
type: str
|
||||
description:
|
||||
- Identify parent by name.
|
||||
- Needs more internal API calls than using O(parents[].id) to map names to ID's under the hood.
|
||||
- When giving a parent chain with only names it must be complete up to the top.
|
||||
- Note that in principle both ID and name can be specified at the same time but current implementation only always
|
||||
use just one of them, with ID being preferred.
|
||||
notes:
|
||||
- Presently, the RV(end_state.realmRoles), RV(end_state.clientRoles), and RV(end_state.access) attributes returned by the
|
||||
Keycloak API are read-only for groups. This limitation will be removed in a later version of this module.
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Adam Goossens (@adamgoossens)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create a Keycloak group, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_group:
|
||||
name: my-new-kc-group
|
||||
realm: MyCustomRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
register: result_new_kcgrp
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a Keycloak group, authentication with token
|
||||
middleware_automation.keycloak.keycloak_group:
|
||||
name: my-new-kc-group
|
||||
realm: MyCustomRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete a keycloak group
|
||||
middleware_automation.keycloak.keycloak_group:
|
||||
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
|
||||
state: absent
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete a Keycloak group based on name
|
||||
middleware_automation.keycloak.keycloak_group:
|
||||
name: my-group-for-deletion
|
||||
state: absent
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Update the name of a Keycloak group
|
||||
middleware_automation.keycloak.keycloak_group:
|
||||
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
|
||||
name: an-updated-kc-group-name
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a keycloak group with some custom attributes
|
||||
middleware_automation.keycloak.keycloak_group:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
name: my-new_group
|
||||
attributes:
|
||||
attrib1: value1
|
||||
attrib2: value2
|
||||
attrib3:
|
||||
- with
|
||||
- numerous
|
||||
- individual
|
||||
- list
|
||||
- items
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a Keycloak subgroup of a base group (using parent name)
|
||||
middleware_automation.keycloak.keycloak_group:
|
||||
name: my-new-kc-group-sub
|
||||
realm: MyCustomRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
parents:
|
||||
- name: my-new-kc-group
|
||||
register: result_new_kcgrp_sub
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a Keycloak subgroup of a base group (using parent id)
|
||||
middleware_automation.keycloak.keycloak_group:
|
||||
name: my-new-kc-group-sub2
|
||||
realm: MyCustomRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
parents:
|
||||
- id: "{{ result_new_kcgrp.end_state.id }}"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a Keycloak subgroup of a subgroup (using parent names)
|
||||
middleware_automation.keycloak.keycloak_group:
|
||||
name: my-new-kc-group-sub-sub
|
||||
realm: MyCustomRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
parents:
|
||||
- name: my-new-kc-group
|
||||
- name: my-new-kc-group-sub
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a Keycloak subgroup of a subgroup (using direct parent id)
|
||||
middleware_automation.keycloak.keycloak_group:
|
||||
name: my-new-kc-group-sub-sub
|
||||
realm: MyCustomRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
parents:
|
||||
- id: "{{ result_new_kcgrp_sub.end_state.id }}"
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
end_state:
|
||||
description: Representation of the group after module execution (sample is truncated).
|
||||
returned: on success
|
||||
type: complex
|
||||
contains:
|
||||
id:
|
||||
description: GUID that identifies the group.
|
||||
type: str
|
||||
returned: always
|
||||
sample: 23f38145-3195-462c-97e7-97041ccea73e
|
||||
name:
|
||||
description: Name of the group.
|
||||
type: str
|
||||
returned: always
|
||||
sample: grp-test-123
|
||||
attributes:
|
||||
description: Attributes applied to this group.
|
||||
type: dict
|
||||
returned: always
|
||||
sample:
|
||||
attr1: ["val1", "val2", "val3"]
|
||||
path:
|
||||
description: URI path to the group.
|
||||
type: str
|
||||
returned: always
|
||||
sample: /grp-test-123
|
||||
realmRoles:
|
||||
description: An array of the realm-level roles granted to this group.
|
||||
type: list
|
||||
returned: always
|
||||
sample: []
|
||||
subGroups:
|
||||
description: A list of groups that are children of this group. These groups have the same parameters as documented here.
|
||||
type: list
|
||||
returned: always
|
||||
clientRoles:
|
||||
description: A list of client-level roles granted to this group.
|
||||
type: list
|
||||
returned: always
|
||||
sample: []
|
||||
access:
|
||||
description: A dict describing the accesses you have to this group based on the credentials used.
|
||||
type: dict
|
||||
returned: always
|
||||
sample:
|
||||
manage: true
|
||||
manageMembership: true
|
||||
view: true
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
camel,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(default="present", choices=["present", "absent"]),
|
||||
realm=dict(default="master"),
|
||||
id=dict(type="str"),
|
||||
name=dict(type="str"),
|
||||
attributes=dict(type="dict"),
|
||||
parents=dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=dict(id=dict(type="str"), name=dict(type="str")),
|
||||
),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[
|
||||
["id", "name"],
|
||||
["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"],
|
||||
]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, group="")
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
state = module.params.get("state")
|
||||
gid = module.params.get("id")
|
||||
name = module.params.get("name")
|
||||
attributes = module.params.get("attributes")
|
||||
|
||||
parents = module.params.get("parents")
|
||||
|
||||
# attributes in Keycloak have their values returned as lists
|
||||
# using the API. attributes is a dict, so we'll transparently convert
|
||||
# the values to lists.
|
||||
if attributes is not None:
|
||||
for key, val in module.params["attributes"].items():
|
||||
module.params["attributes"][key] = [val] if not isinstance(val, list) else val
|
||||
|
||||
# Filter and map the parameters names that apply to the group
|
||||
group_params = [
|
||||
x
|
||||
for x in module.params
|
||||
if x not in list(keycloak_argument_spec().keys()) + ["state", "realm", "parents"]
|
||||
and module.params.get(x) is not None
|
||||
]
|
||||
|
||||
# See if it already exists in Keycloak
|
||||
if gid is None:
|
||||
before_group = kc.get_group_by_name(name, realm=realm, parents=parents)
|
||||
else:
|
||||
before_group = kc.get_group_by_groupid(gid, realm=realm)
|
||||
|
||||
if before_group is None:
|
||||
before_group = {}
|
||||
|
||||
# Build a proposed changeset from parameters given to this module
|
||||
changeset = {}
|
||||
|
||||
for param in group_params:
|
||||
new_param_value = module.params.get(param)
|
||||
old_value = before_group[param] if param in before_group else None
|
||||
if new_param_value != old_value:
|
||||
changeset[camel(param)] = new_param_value
|
||||
|
||||
# Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
|
||||
desired_group = before_group.copy()
|
||||
desired_group.update(changeset)
|
||||
|
||||
# Cater for when it doesn't exist (an empty dict)
|
||||
if not before_group:
|
||||
if state == "absent":
|
||||
# Do nothing and exit
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after="")
|
||||
result["changed"] = False
|
||||
result["end_state"] = {}
|
||||
result["msg"] = "Group does not exist; doing nothing."
|
||||
module.exit_json(**result)
|
||||
|
||||
# Process a creation
|
||||
result["changed"] = True
|
||||
|
||||
if name is None:
|
||||
module.fail_json(msg="name must be specified when creating a new group")
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after=desired_group)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# create it ...
|
||||
if parents:
|
||||
# ... as subgroup of another parent group
|
||||
kc.create_subgroup(parents, desired_group, realm=realm)
|
||||
else:
|
||||
# ... as toplvl base group
|
||||
kc.create_group(desired_group, realm=realm)
|
||||
|
||||
after_group = kc.get_group_by_name(name, realm, parents=parents)
|
||||
|
||||
result["end_state"] = after_group
|
||||
|
||||
result["msg"] = f"Group {after_group['name']} has been created with ID {after_group['id']}"
|
||||
module.exit_json(**result)
|
||||
|
||||
else:
|
||||
if state == "present":
|
||||
# Process an update
|
||||
|
||||
# no changes
|
||||
if desired_group == before_group:
|
||||
result["changed"] = False
|
||||
result["end_state"] = desired_group
|
||||
result["msg"] = f"No changes required to group {before_group['name']}."
|
||||
module.exit_json(**result)
|
||||
|
||||
# doing an update
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_group, after=desired_group)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# do the update
|
||||
kc.update_group(desired_group, realm=realm)
|
||||
|
||||
after_group = kc.get_group_by_groupid(desired_group["id"], realm=realm)
|
||||
|
||||
result["end_state"] = after_group
|
||||
|
||||
result["msg"] = f"Group {after_group['id']} has been updated"
|
||||
module.exit_json(**result)
|
||||
|
||||
else:
|
||||
# Process a deletion (because state was not 'present')
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_group, after="")
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# delete it
|
||||
gid = before_group["id"]
|
||||
kc.delete_group(groupid=gid, realm=realm)
|
||||
|
||||
result["end_state"] = {}
|
||||
|
||||
result["msg"] = f"Group {before_group['name']} has been deleted"
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,795 @@
|
|||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_identity_provider
|
||||
|
||||
short_description: Allows administration of Keycloak identity providers using Keycloak API
|
||||
|
||||
# Originally added in community.general 3.6.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to add, remove or modify Keycloak identity providers 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.
|
||||
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 names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/15.0/rest-api/index.html).
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the identity provider.
|
||||
- On V(present), the identity provider is created if it does not yet exist, or updated with the parameters you provide.
|
||||
- On V(absent), the identity provider is removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
realm:
|
||||
description:
|
||||
- The Keycloak realm under which this identity provider resides.
|
||||
default: 'master'
|
||||
type: str
|
||||
|
||||
alias:
|
||||
description:
|
||||
- The alias uniquely identifies an identity provider and it is also used to build the redirect URI.
|
||||
required: true
|
||||
type: str
|
||||
|
||||
display_name:
|
||||
description:
|
||||
- Friendly name for identity provider.
|
||||
aliases:
|
||||
- displayName
|
||||
type: str
|
||||
|
||||
enabled:
|
||||
description:
|
||||
- Enable/disable this identity provider.
|
||||
type: bool
|
||||
|
||||
store_token:
|
||||
description:
|
||||
- Enable/disable whether tokens must be stored after authenticating users.
|
||||
aliases:
|
||||
- storeToken
|
||||
type: bool
|
||||
|
||||
add_read_token_role_on_create:
|
||||
description:
|
||||
- Enable/disable whether new users can read any stored tokens. This assigns the C(broker.read-token) role.
|
||||
aliases:
|
||||
- addReadTokenRoleOnCreate
|
||||
type: bool
|
||||
|
||||
trust_email:
|
||||
description:
|
||||
- If enabled, email provided by this provider is not verified even if verification is enabled for the realm.
|
||||
aliases:
|
||||
- trustEmail
|
||||
type: bool
|
||||
|
||||
link_only:
|
||||
description:
|
||||
- If true, users cannot log in through this provider. They can only link to this provider. This is useful if you do
|
||||
not want to allow login from the provider, but want to integrate with a provider.
|
||||
aliases:
|
||||
- linkOnly
|
||||
type: bool
|
||||
|
||||
first_broker_login_flow_alias:
|
||||
description:
|
||||
- Alias of authentication flow, which is triggered after first login with this identity provider.
|
||||
aliases:
|
||||
- firstBrokerLoginFlowAlias
|
||||
type: str
|
||||
|
||||
post_broker_login_flow_alias:
|
||||
description:
|
||||
- Alias of authentication flow, which is triggered after each login with this identity provider.
|
||||
aliases:
|
||||
- postBrokerLoginFlowAlias
|
||||
type: str
|
||||
|
||||
authenticate_by_default:
|
||||
description:
|
||||
- Specifies if this identity provider should be used by default for authentication even before displaying login screen.
|
||||
aliases:
|
||||
- authenticateByDefault
|
||||
type: bool
|
||||
|
||||
hide_on_login:
|
||||
description:
|
||||
- If hidden, login with this provider is possible only if requested explicitly, for example using the C(kc_idp_hint)
|
||||
parameter.
|
||||
aliases:
|
||||
- hideOnLogin
|
||||
- hide_on_login_page
|
||||
type: bool
|
||||
|
||||
provider_id:
|
||||
description:
|
||||
- Protocol used by this provider (supported values are V(oidc) or V(saml)).
|
||||
aliases:
|
||||
- providerId
|
||||
type: str
|
||||
|
||||
config:
|
||||
description:
|
||||
- Dict specifying the configuration options for the provider; the contents differ depending on the value of O(provider_id).
|
||||
Examples are given below for V(oidc) and V(saml). It is easiest to obtain valid config values by dumping an already-existing
|
||||
identity provider configuration through check-mode in the RV(existing) field.
|
||||
type: dict
|
||||
suboptions:
|
||||
gui_order:
|
||||
description:
|
||||
- Number defining order of the provider in GUI (for example, on Login page).
|
||||
aliases:
|
||||
- guiOrder
|
||||
type: int
|
||||
|
||||
sync_mode:
|
||||
description:
|
||||
- Default sync mode for all mappers. The sync mode determines when user data is synced using the mappers.
|
||||
aliases:
|
||||
- syncMode
|
||||
type: str
|
||||
|
||||
issuer:
|
||||
description:
|
||||
- The issuer identifier for the issuer of the response. If not provided, no validation is performed.
|
||||
type: str
|
||||
|
||||
authorizationUrl:
|
||||
description:
|
||||
- The Authorization URL.
|
||||
type: str
|
||||
|
||||
tokenUrl:
|
||||
description:
|
||||
- The Token URL.
|
||||
type: str
|
||||
|
||||
logoutUrl:
|
||||
description:
|
||||
- End session endpoint to use to logout user from external IDP.
|
||||
type: str
|
||||
|
||||
userInfoUrl:
|
||||
description:
|
||||
- The User Info URL.
|
||||
type: str
|
||||
|
||||
clientAuthMethod:
|
||||
description:
|
||||
- The client authentication method.
|
||||
type: str
|
||||
|
||||
clientId:
|
||||
description:
|
||||
- The client or client identifier registered within the identity provider.
|
||||
type: str
|
||||
|
||||
clientSecret:
|
||||
description:
|
||||
- The client or client secret registered within the identity provider.
|
||||
type: str
|
||||
|
||||
defaultScope:
|
||||
description:
|
||||
- The scopes to be sent when asking for authorization.
|
||||
type: str
|
||||
|
||||
validateSignature:
|
||||
description:
|
||||
- Enable/disable signature validation of external IDP signatures.
|
||||
type: bool
|
||||
|
||||
useJwksUrl:
|
||||
description:
|
||||
- If V(true), identity provider public keys are downloaded from given JWKS URL.
|
||||
type: bool
|
||||
|
||||
jwksUrl:
|
||||
description:
|
||||
- URL where identity provider keys in JWK format are stored. See JWK specification for more details.
|
||||
type: str
|
||||
|
||||
entityId:
|
||||
description:
|
||||
- The Entity ID that is used to uniquely identify this SAML Service Provider.
|
||||
type: str
|
||||
|
||||
singleSignOnServiceUrl:
|
||||
description:
|
||||
- The URL that must be used to send authentication requests (SAML AuthnRequest).
|
||||
type: str
|
||||
|
||||
singleLogoutServiceUrl:
|
||||
description:
|
||||
- The URL that must be used to send logout requests.
|
||||
type: str
|
||||
|
||||
backchannelSupported:
|
||||
description:
|
||||
- Does the external IDP support backchannel logout?
|
||||
type: str
|
||||
|
||||
nameIDPolicyFormat:
|
||||
description:
|
||||
- Specifies the URI reference corresponding to a name identifier format.
|
||||
type: str
|
||||
|
||||
principalType:
|
||||
description:
|
||||
- Way to identify and track external users from the assertion.
|
||||
type: str
|
||||
|
||||
fromUrl:
|
||||
description:
|
||||
- IDP well-known OpenID Connect configuration URL.
|
||||
- Support only O(provider_id=oidc).
|
||||
- O(config.fromUrl) is mutually exclusive with O(config.userInfoUrl), O(config.authorizationUrl),
|
||||
O(config.tokenUrl), O(config.logoutUrl), O(config.issuer) and O(config.jwksUrl).
|
||||
type: str
|
||||
|
||||
mappers:
|
||||
description:
|
||||
- A list of dicts defining mappers associated with this Identity Provider.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
id:
|
||||
description:
|
||||
- Unique ID of this mapper.
|
||||
type: str
|
||||
|
||||
name:
|
||||
description:
|
||||
- Name of the mapper.
|
||||
type: str
|
||||
|
||||
identityProviderAlias:
|
||||
description:
|
||||
- Alias of the identity provider for this mapper.
|
||||
type: str
|
||||
|
||||
identityProviderMapper:
|
||||
description:
|
||||
- Type of mapper.
|
||||
type: str
|
||||
|
||||
config:
|
||||
description:
|
||||
- Dict specifying the configuration options for the mapper; the contents differ depending on the value of O(mappers[].identityProviderMapper).
|
||||
type: dict
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Laurent Paumier (@laurpaum)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create OIDC identity provider, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_identity_provider:
|
||||
state: present
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: admin
|
||||
realm: myrealm
|
||||
alias: oidc-idp
|
||||
display_name: OpenID Connect IdP
|
||||
enabled: true
|
||||
provider_id: oidc
|
||||
config:
|
||||
issuer: https://idp.example.com
|
||||
authorizationUrl: https://idp.example.com
|
||||
tokenUrl: https://idp.example.com/token
|
||||
userInfoUrl: https://idp.example.com/userinfo
|
||||
clientAuthMethod: client_secret_post
|
||||
clientId: my-client
|
||||
clientSecret: secret
|
||||
syncMode: FORCE
|
||||
mappers:
|
||||
- name: first_name
|
||||
identityProviderMapper: oidc-user-attribute-idp-mapper
|
||||
config:
|
||||
claim: first_name
|
||||
user.attribute: first_name
|
||||
syncMode: INHERIT
|
||||
- name: last_name
|
||||
identityProviderMapper: oidc-user-attribute-idp-mapper
|
||||
config:
|
||||
claim: last_name
|
||||
user.attribute: last_name
|
||||
syncMode: INHERIT
|
||||
|
||||
- name: Create OIDC identity provider, with well-known configuration URL
|
||||
middleware_automation.keycloak.keycloak_identity_provider:
|
||||
state: present
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: admin
|
||||
realm: myrealm
|
||||
alias: oidc-idp
|
||||
display_name: OpenID Connect IdP
|
||||
enabled: true
|
||||
provider_id: oidc
|
||||
config:
|
||||
fromUrl: https://the-idp.example.com/realms/idprealm/.well-known/openid-configuration
|
||||
clientAuthMethod: client_secret_post
|
||||
clientId: my-client
|
||||
clientSecret: secret
|
||||
|
||||
- name: Create SAML identity provider, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_identity_provider:
|
||||
state: present
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: admin
|
||||
realm: myrealm
|
||||
alias: saml-idp
|
||||
display_name: SAML IdP
|
||||
enabled: true
|
||||
provider_id: saml
|
||||
config:
|
||||
entityId: https://auth.example.com/realms/myrealm
|
||||
singleSignOnServiceUrl: https://idp.example.com/login
|
||||
wantAuthnRequestsSigned: true
|
||||
wantAssertionsSigned: true
|
||||
mappers:
|
||||
- name: roles
|
||||
identityProviderMapper: saml-user-attribute-idp-mapper
|
||||
config:
|
||||
user.attribute: roles
|
||||
attribute.friendly.name: User Roles
|
||||
attribute.name: roles
|
||||
syncMode: INHERIT
|
||||
|
||||
- name: Create OIDC identity provider, authentication with credentials and advanced claim to group
|
||||
middleware_automation.keycloak.keycloak_identity_provider:
|
||||
state: present
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: admin
|
||||
realm: myrealm
|
||||
alias: oidc-idp
|
||||
display_name: OpenID Connect IdP
|
||||
enabled: true
|
||||
provider_id: oidc
|
||||
config:
|
||||
issuer: https://idp.example.com
|
||||
authorizationUrl: https://idp.example.com
|
||||
tokenUrl: https://idp.example.com/token
|
||||
userInfoUrl: https://idp.example.com/userinfo
|
||||
clientAuthMethod: client_secret_post
|
||||
clientId: my-client
|
||||
clientSecret: secret
|
||||
syncMode: FORCE
|
||||
mappers:
|
||||
- name: group_name
|
||||
identityProviderMapper: oidc-advanced-group-idp-mapper
|
||||
config:
|
||||
claims: '[{"key":"my_key","value":"my_value"}]'
|
||||
group: group_name
|
||||
syncMode: INHERIT
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Identity provider my-idp has been created"
|
||||
|
||||
proposed:
|
||||
description: Representation of proposed identity provider.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"config": {
|
||||
"authorizationUrl": "https://idp.example.com",
|
||||
"clientAuthMethod": "client_secret_post",
|
||||
"clientId": "my-client",
|
||||
"clientSecret": "secret",
|
||||
"issuer": "https://idp.example.com",
|
||||
"tokenUrl": "https://idp.example.com/token",
|
||||
"userInfoUrl": "https://idp.example.com/userinfo"
|
||||
},
|
||||
"displayName": "OpenID Connect IdP",
|
||||
"providerId": "oidc"
|
||||
}
|
||||
|
||||
existing:
|
||||
description: Representation of existing identity provider.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"addReadTokenRoleOnCreate": false,
|
||||
"alias": "my-idp",
|
||||
"authenticateByDefault": false,
|
||||
"config": {
|
||||
"authorizationUrl": "https://old.example.com",
|
||||
"clientAuthMethod": "client_secret_post",
|
||||
"clientId": "my-client",
|
||||
"clientSecret": "**********",
|
||||
"issuer": "https://old.example.com",
|
||||
"syncMode": "FORCE",
|
||||
"tokenUrl": "https://old.example.com/token",
|
||||
"userInfoUrl": "https://old.example.com/userinfo"
|
||||
},
|
||||
"displayName": "OpenID Connect IdP",
|
||||
"enabled": true,
|
||||
"firstBrokerLoginFlowAlias": "first broker login",
|
||||
"internalId": "4d28d7e3-1b80-45bb-8a30-5822bf55aa1c",
|
||||
"linkOnly": false,
|
||||
"providerId": "oidc",
|
||||
"storeToken": false,
|
||||
"trustEmail": false
|
||||
}
|
||||
|
||||
end_state:
|
||||
description: Representation of identity provider after module execution.
|
||||
returned: on success
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"addReadTokenRoleOnCreate": false,
|
||||
"alias": "my-idp",
|
||||
"authenticateByDefault": false,
|
||||
"config": {
|
||||
"authorizationUrl": "https://idp.example.com",
|
||||
"clientAuthMethod": "client_secret_post",
|
||||
"clientId": "my-client",
|
||||
"clientSecret": "**********",
|
||||
"issuer": "https://idp.example.com",
|
||||
"tokenUrl": "https://idp.example.com/token",
|
||||
"userInfoUrl": "https://idp.example.com/userinfo"
|
||||
},
|
||||
"displayName": "OpenID Connect IdP",
|
||||
"enabled": true,
|
||||
"firstBrokerLoginFlowAlias": "first broker login",
|
||||
"internalId": "4d28d7e3-1b80-45bb-8a30-5822bf55aa1c",
|
||||
"linkOnly": false,
|
||||
"providerId": "oidc",
|
||||
"storeToken": false,
|
||||
"trustEmail": false
|
||||
}
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
camel,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def sanitize(idp):
|
||||
idpcopy = deepcopy(idp)
|
||||
if "config" in idpcopy:
|
||||
if "clientSecret" in idpcopy["config"]:
|
||||
idpcopy["config"]["clientSecret"] = "**********"
|
||||
return idpcopy
|
||||
|
||||
|
||||
def get_identity_provider_with_mappers(kc, alias, realm):
|
||||
idp = kc.get_identity_provider(alias, realm)
|
||||
if idp is not None:
|
||||
idp["mappers"] = sorted(kc.get_identity_provider_mappers(alias, realm), key=lambda x: x.get("name"))
|
||||
# clientSecret returned by API when using `get_identity_provider(alias, realm)` is always **********
|
||||
# to detect changes to the secret, we get the actual cleartext secret from the full realm info
|
||||
if "config" in idp:
|
||||
if "clientSecret" in idp["config"]:
|
||||
for idp_from_realm in kc.get_realm_by_id(realm).get("identityProviders", []):
|
||||
if idp_from_realm["internalId"] == idp["internalId"]:
|
||||
cleartext_secret = idp_from_realm.get("config", {}).get("clientSecret")
|
||||
if cleartext_secret:
|
||||
idp["config"]["clientSecret"] = cleartext_secret
|
||||
if idp is None:
|
||||
idp = {}
|
||||
return idp
|
||||
|
||||
|
||||
def fetch_identity_provider_wellknown_config(kc, config):
|
||||
"""
|
||||
Fetches OpenID Connect well-known configuration from a given URL and updates the config dict with discovered endpoints.
|
||||
Support for oidc providers only.
|
||||
:param kc: KeycloakAPI instance used to fetch endpoints and handle errors.
|
||||
:param config: Dictionary containing identity provider configuration, must include 'fromUrl' key to trigger fetch.
|
||||
:return: None. The config dict is updated in-place.
|
||||
"""
|
||||
if config and "fromUrl" in config:
|
||||
if "providerId" in config and config["providerId"] != "oidc":
|
||||
kc.module.fail_json(msg="Only 'oidc' provider_id is supported when using 'fromUrl'.")
|
||||
endpoints = ["userInfoUrl", "authorizationUrl", "tokenUrl", "logoutUrl", "issuer", "jwksUrl"]
|
||||
if any(k in config for k in endpoints):
|
||||
kc.module.fail_json(
|
||||
msg="Cannot specify both 'fromUrl' and 'userInfoUrl', 'authorizationUrl', 'tokenUrl', 'logoutUrl', 'issuer' or 'jwksUrl'."
|
||||
)
|
||||
openIdConfig = kc.fetch_idp_endpoints_import_config_url(
|
||||
fromUrl=config["fromUrl"], realm=kc.module.params.get("realm", "master")
|
||||
)
|
||||
for k in endpoints:
|
||||
if k in openIdConfig:
|
||||
config[k] = openIdConfig[k]
|
||||
del config["fromUrl"]
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
mapper_spec = dict(
|
||||
id=dict(type="str"),
|
||||
name=dict(type="str"),
|
||||
identityProviderAlias=dict(type="str"),
|
||||
identityProviderMapper=dict(type="str"),
|
||||
config=dict(type="dict"),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
realm=dict(type="str", default="master"),
|
||||
alias=dict(type="str", required=True),
|
||||
add_read_token_role_on_create=dict(type="bool", aliases=["addReadTokenRoleOnCreate"]),
|
||||
authenticate_by_default=dict(type="bool", aliases=["authenticateByDefault"]),
|
||||
hide_on_login=dict(type="bool", aliases=["hideOnLogin", "hide_on_login_page"]),
|
||||
config=dict(type="dict"),
|
||||
display_name=dict(type="str", aliases=["displayName"]),
|
||||
enabled=dict(type="bool"),
|
||||
first_broker_login_flow_alias=dict(type="str", aliases=["firstBrokerLoginFlowAlias"]),
|
||||
link_only=dict(type="bool", aliases=["linkOnly"]),
|
||||
post_broker_login_flow_alias=dict(type="str", aliases=["postBrokerLoginFlowAlias"]),
|
||||
provider_id=dict(type="str", aliases=["providerId"]),
|
||||
store_token=dict(type="bool", aliases=["storeToken"]),
|
||||
trust_email=dict(type="bool", aliases=["trustEmail"]),
|
||||
mappers=dict(type="list", elements="dict", options=mapper_spec),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, proposed={}, existing={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
alias = module.params.get("alias")
|
||||
state = module.params.get("state")
|
||||
config = module.params.get("config")
|
||||
|
||||
if config is not None:
|
||||
for legacy_key in ("hide_on_login_page", "hideOnLoginPage"):
|
||||
if legacy_key in config:
|
||||
module.deprecate(
|
||||
f"Passing '{legacy_key}' inside 'config' is deprecated. "
|
||||
"Use the top-level 'hide_on_login' parameter instead.",
|
||||
version="4.0.0",
|
||||
collection_name="middleware_automation.keycloak",
|
||||
)
|
||||
if module.params.get("hide_on_login") is None:
|
||||
module.params["hide_on_login"] = config.pop(legacy_key)
|
||||
else:
|
||||
config.pop(legacy_key)
|
||||
|
||||
fetch_identity_provider_wellknown_config(kc, config)
|
||||
|
||||
# Filter and map the parameters names that apply to the identity provider.
|
||||
idp_params = [
|
||||
x
|
||||
for x in module.params
|
||||
if x not in list(keycloak_argument_spec().keys()) + ["state", "realm", "mappers"]
|
||||
and module.params.get(x) is not None
|
||||
]
|
||||
|
||||
# See if it already exists in Keycloak
|
||||
before_idp = get_identity_provider_with_mappers(kc, alias, realm)
|
||||
|
||||
# Build a proposed changeset from parameters given to this module
|
||||
changeset = {}
|
||||
|
||||
for param in idp_params:
|
||||
new_param_value = module.params.get(param)
|
||||
old_value = before_idp[camel(param)] if camel(param) in before_idp else None
|
||||
if new_param_value != old_value:
|
||||
changeset[camel(param)] = new_param_value
|
||||
|
||||
# special handling of mappers list to allow change detection
|
||||
if module.params.get("mappers") is not None:
|
||||
for change in module.params["mappers"]:
|
||||
change = {k: v for k, v in change.items() if v is not None}
|
||||
if change.get("id") is None and change.get("name") is None:
|
||||
module.fail_json(msg="Either `name` or `id` has to be specified on each mapper.")
|
||||
if before_idp == dict():
|
||||
old_mapper = dict()
|
||||
elif change.get("id") is not None:
|
||||
old_mapper = kc.get_identity_provider_mapper(change["id"], alias, realm)
|
||||
if old_mapper is None:
|
||||
old_mapper = dict()
|
||||
else:
|
||||
found = [x for x in kc.get_identity_provider_mappers(alias, realm) if x["name"] == change["name"]]
|
||||
if len(found) == 1:
|
||||
old_mapper = found[0]
|
||||
else:
|
||||
old_mapper = dict()
|
||||
new_mapper = old_mapper.copy()
|
||||
new_mapper.update(change)
|
||||
|
||||
if changeset.get("mappers") is None:
|
||||
changeset["mappers"] = list()
|
||||
# eventually this holds all desired mappers, unchanged, modified and newly added
|
||||
changeset["mappers"].append(new_mapper)
|
||||
|
||||
# ensure idempotency in case module.params.mappers is not sorted by name
|
||||
changeset["mappers"] = sorted(
|
||||
changeset["mappers"], key=lambda x: x.get("id") if x.get("name") is None else x["name"]
|
||||
)
|
||||
|
||||
# Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
|
||||
desired_idp = before_idp.copy()
|
||||
desired_idp.update(changeset)
|
||||
|
||||
result["proposed"] = sanitize(changeset)
|
||||
result["existing"] = sanitize(before_idp)
|
||||
|
||||
# Cater for when it doesn't exist (an empty dict)
|
||||
if not before_idp:
|
||||
if state == "absent":
|
||||
# Do nothing and exit
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after="")
|
||||
result["changed"] = False
|
||||
result["end_state"] = {}
|
||||
result["msg"] = "Identity provider does not exist; doing nothing."
|
||||
module.exit_json(**result)
|
||||
|
||||
# Process a creation
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after=sanitize(desired_idp))
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# create it
|
||||
desired_idp = desired_idp.copy()
|
||||
mappers = desired_idp.pop("mappers", [])
|
||||
kc.create_identity_provider(desired_idp, realm)
|
||||
for mapper in mappers:
|
||||
if mapper.get("identityProviderAlias") is None:
|
||||
mapper["identityProviderAlias"] = alias
|
||||
kc.create_identity_provider_mapper(mapper, alias, realm)
|
||||
after_idp = get_identity_provider_with_mappers(kc, alias, realm)
|
||||
|
||||
result["end_state"] = sanitize(after_idp)
|
||||
|
||||
result["msg"] = f"Identity provider {alias} has been created"
|
||||
module.exit_json(**result)
|
||||
|
||||
else:
|
||||
if state == "present":
|
||||
# Process an update
|
||||
|
||||
# no changes
|
||||
if desired_idp == before_idp:
|
||||
result["changed"] = False
|
||||
result["end_state"] = sanitize(desired_idp)
|
||||
result["msg"] = f"No changes required to identity provider {alias}."
|
||||
module.exit_json(**result)
|
||||
|
||||
# doing an update
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=sanitize(before_idp), after=sanitize(desired_idp))
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# do the update
|
||||
desired_idp = desired_idp.copy()
|
||||
updated_mappers = desired_idp.pop("mappers", [])
|
||||
original_mappers = list(before_idp.get("mappers", []))
|
||||
|
||||
kc.update_identity_provider(desired_idp, realm)
|
||||
for mapper in updated_mappers:
|
||||
if mapper.get("id") is not None:
|
||||
# only update existing if there is a change
|
||||
for i, orig in enumerate(original_mappers):
|
||||
if mapper["id"] == orig["id"]:
|
||||
del original_mappers[i]
|
||||
if mapper != orig:
|
||||
kc.update_identity_provider_mapper(mapper, alias, realm)
|
||||
else:
|
||||
if mapper.get("identityProviderAlias") is None:
|
||||
mapper["identityProviderAlias"] = alias
|
||||
kc.create_identity_provider_mapper(mapper, alias, realm)
|
||||
for mapper in [
|
||||
x for x in before_idp["mappers"] if [y for y in updated_mappers if y["name"] == x["name"]] == []
|
||||
]:
|
||||
kc.delete_identity_provider_mapper(mapper["id"], alias, realm)
|
||||
|
||||
after_idp = get_identity_provider_with_mappers(kc, alias, realm)
|
||||
|
||||
result["end_state"] = sanitize(after_idp)
|
||||
|
||||
result["msg"] = f"Identity provider {alias} has been updated"
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == "absent":
|
||||
# Process a deletion
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=sanitize(before_idp), after="")
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# delete it
|
||||
kc.delete_identity_provider(alias, realm)
|
||||
|
||||
result["end_state"] = {}
|
||||
|
||||
result["msg"] = f"Identity provider {alias} has been deleted"
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,416 @@
|
|||
|
||||
# Copyright (c) 2025, Chris Brown
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_organization
|
||||
|
||||
short_description: Allows administration of Keycloak organizations using Keycloak API
|
||||
|
||||
version_added: "3.1.0"
|
||||
|
||||
description:
|
||||
- This module allows you to add, remove or modify Keycloak organizations 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. 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 names of module options are snake_cased versions of the camelCase ones found in the Keycloak
|
||||
API and its documentation at U(https://www.keycloak.org/docs-api/latest/rest-api/index.html).
|
||||
- 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 suitable for the API.
|
||||
- Organizations are available in Keycloak 26 and later. The realm must have organizations enabled
|
||||
(C(organizations_enabled=true) in the M(middleware_automation.keycloak.keycloak_realm) module)
|
||||
before this module can be used.
|
||||
- When updating an organization, where possible provide the organization ID to the module. This
|
||||
removes a lookup to the API to translate the name into the organization ID.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
version_added: "3.1.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the organization.
|
||||
- On V(present), the organization is created if it does not yet exist, or updated with the
|
||||
parameters you provide.
|
||||
- On V(absent), the organization is removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- The Keycloak realm under which this organization resides.
|
||||
default: 'master'
|
||||
|
||||
id:
|
||||
type: str
|
||||
description:
|
||||
- The unique identifier for this organization.
|
||||
- This parameter is not required for updating or deleting an organization but providing it
|
||||
reduces the number of API calls required.
|
||||
|
||||
name:
|
||||
type: str
|
||||
description:
|
||||
- Name of the organization.
|
||||
- This parameter is required when creating a new organization.
|
||||
|
||||
alias:
|
||||
type: str
|
||||
description:
|
||||
- URL-friendly alias for the organization.
|
||||
- If not provided during creation, defaults to the organization name if URL-safe.
|
||||
- The alias is immutable once set and cannot be changed on update.
|
||||
|
||||
enabled:
|
||||
type: bool
|
||||
description:
|
||||
- Whether the organization is enabled.
|
||||
- Disabled organizations block member authentication.
|
||||
|
||||
description:
|
||||
type: str
|
||||
description:
|
||||
- Description of the organization.
|
||||
|
||||
domains:
|
||||
type: list
|
||||
elements: dict
|
||||
description:
|
||||
- List of internet domains associated with the organization.
|
||||
- A domain name can only belong to one organization within a realm.
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
- The domain name (e.g. V(example.com)).
|
||||
verified:
|
||||
type: bool
|
||||
default: false
|
||||
description:
|
||||
- Whether the domain has been verified.
|
||||
|
||||
attributes:
|
||||
type: dict
|
||||
description:
|
||||
- A dict of key/value pairs to set as custom attributes for the organization.
|
||||
- Values may be single values (for example a string) or a list of strings.
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Chris Brown (@chribro)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create a Keycloak organization
|
||||
middleware_automation.keycloak.keycloak_organization:
|
||||
name: my-org
|
||||
description: My Organization
|
||||
enabled: true
|
||||
realm: MyRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a Keycloak organization with domains
|
||||
middleware_automation.keycloak.keycloak_organization:
|
||||
name: acme-corp
|
||||
description: Acme Corporation
|
||||
enabled: true
|
||||
domains:
|
||||
- name: acme.com
|
||||
- name: acme.org
|
||||
verified: true
|
||||
realm: MyRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Update a Keycloak organization
|
||||
middleware_automation.keycloak.keycloak_organization:
|
||||
name: my-org
|
||||
description: Updated description
|
||||
realm: MyRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete a Keycloak organization
|
||||
middleware_automation.keycloak.keycloak_organization:
|
||||
name: my-org
|
||||
state: absent
|
||||
realm: MyRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
end_state:
|
||||
description: Representation of the organization after module execution.
|
||||
returned: on success
|
||||
type: complex
|
||||
contains:
|
||||
id:
|
||||
description: GUID that identifies the organization.
|
||||
type: str
|
||||
returned: always
|
||||
sample: 23f38145-3195-462c-97e7-97041ccea73e
|
||||
name:
|
||||
description: Name of the organization.
|
||||
type: str
|
||||
returned: always
|
||||
sample: my-org
|
||||
alias:
|
||||
description: URL-friendly alias of the organization.
|
||||
type: str
|
||||
returned: always
|
||||
sample: my-org
|
||||
enabled:
|
||||
description: Whether the organization is enabled.
|
||||
type: bool
|
||||
returned: always
|
||||
sample: true
|
||||
description:
|
||||
description: Description of the organization.
|
||||
type: str
|
||||
returned: always
|
||||
sample: My Organization
|
||||
domains:
|
||||
description: Domains associated with the organization.
|
||||
type: list
|
||||
returned: always
|
||||
sample: [{"name": "example.com", "verified": false}]
|
||||
attributes:
|
||||
description: Attributes applied to this organization.
|
||||
type: dict
|
||||
returned: always
|
||||
sample:
|
||||
attr1: ["val1", "val2"]
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
camel,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def normalise_org(org):
|
||||
"""Normalise an organization representation for comparison.
|
||||
Sorts domains by name to ensure order-independent comparison.
|
||||
"""
|
||||
if org and "domains" in org and org["domains"]:
|
||||
org["domains"] = sorted(org["domains"], key=lambda d: d.get("name", ""))
|
||||
return org
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(default="present", choices=["present", "absent"]),
|
||||
realm=dict(default="master"),
|
||||
id=dict(type="str"),
|
||||
name=dict(type="str"),
|
||||
alias=dict(type="str"),
|
||||
enabled=dict(type="bool"),
|
||||
description=dict(type="str"),
|
||||
domains=dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=dict(
|
||||
name=dict(type="str", required=True),
|
||||
verified=dict(type="bool", default=False),
|
||||
),
|
||||
),
|
||||
attributes=dict(type="dict"),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[
|
||||
["id", "name"],
|
||||
["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"],
|
||||
]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, end_state={})
|
||||
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
state = module.params.get("state")
|
||||
oid = module.params.get("id")
|
||||
name = module.params.get("name")
|
||||
attributes = module.params.get("attributes")
|
||||
|
||||
if attributes is not None:
|
||||
for key, val in module.params["attributes"].items():
|
||||
module.params["attributes"][key] = [val] if not isinstance(val, list) else val
|
||||
|
||||
# Parameters that map to the organization representation (excluding module-level params)
|
||||
org_params = [
|
||||
x
|
||||
for x in module.params
|
||||
if x not in list(keycloak_argument_spec().keys()) + ["state", "realm"]
|
||||
and module.params.get(x) is not None
|
||||
]
|
||||
|
||||
if oid is None:
|
||||
before_org = kc.get_organization_by_name(name, realm=realm)
|
||||
else:
|
||||
before_org = kc.get_organization_by_id(oid, realm=realm)
|
||||
|
||||
if before_org is None:
|
||||
before_org = {}
|
||||
|
||||
normalise_org(before_org)
|
||||
|
||||
changeset = {}
|
||||
for param in org_params:
|
||||
new_param_value = module.params.get(param)
|
||||
if param == "alias" and before_org:
|
||||
if new_param_value != before_org.get("alias"):
|
||||
module.warn(
|
||||
f"Organization alias is immutable and cannot be changed from '{before_org.get('alias')}' "
|
||||
f"to '{new_param_value}'. The alias parameter will be ignored for this update."
|
||||
)
|
||||
continue
|
||||
old_value = before_org.get(camel(param))
|
||||
if new_param_value != old_value:
|
||||
changeset[camel(param)] = new_param_value
|
||||
|
||||
desired_org = before_org.copy()
|
||||
desired_org.update(changeset)
|
||||
|
||||
normalise_org(desired_org)
|
||||
|
||||
if not before_org:
|
||||
if state == "absent":
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after="")
|
||||
result["changed"] = False
|
||||
result["end_state"] = {}
|
||||
result["msg"] = "Organization does not exist; doing nothing."
|
||||
module.exit_json(**result)
|
||||
|
||||
result["changed"] = True
|
||||
|
||||
if name is None:
|
||||
module.fail_json(msg="name must be specified when creating a new organization")
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after=desired_org)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
kc.create_organization(desired_org, realm=realm)
|
||||
|
||||
after_org = kc.get_organization_by_name(name, realm=realm)
|
||||
|
||||
result["end_state"] = normalise_org(after_org) if after_org else {}
|
||||
result["msg"] = f"Organization {name} has been created with ID {after_org['id']}"
|
||||
module.exit_json(**result)
|
||||
|
||||
else:
|
||||
if state == "present":
|
||||
if desired_org == before_org:
|
||||
result["changed"] = False
|
||||
result["end_state"] = desired_org
|
||||
result["msg"] = f"No changes required to organization {before_org['name']}."
|
||||
module.exit_json(**result)
|
||||
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_org, after=desired_org)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
kc.update_organization(desired_org, realm=realm)
|
||||
|
||||
after_org = kc.get_organization_by_id(desired_org["id"], realm=realm)
|
||||
|
||||
result["end_state"] = normalise_org(after_org) if after_org else {}
|
||||
result["msg"] = f"Organization {desired_org['id']} has been updated"
|
||||
module.exit_json(**result)
|
||||
|
||||
else:
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_org, after="")
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
oid = before_org["id"]
|
||||
kc.delete_organization(org_id=oid, realm=realm)
|
||||
|
||||
result["end_state"] = {}
|
||||
result["msg"] = f"Organization {before_org['name']} has been deleted"
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
|
||||
# Copyright (c) 2025, Chris Brown
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_organization_info
|
||||
|
||||
short_description: Retrieve organization info in Keycloak
|
||||
|
||||
version_added: "3.1.0"
|
||||
|
||||
description:
|
||||
- This module retrieves information on organizations from Keycloak.
|
||||
- Organizations are available in Keycloak 26 and later. The realm must have organizations enabled
|
||||
before this module can be used.
|
||||
attributes:
|
||||
action_group:
|
||||
version_added: "3.1.0"
|
||||
|
||||
options:
|
||||
realm:
|
||||
description:
|
||||
- The name of the realm.
|
||||
required: true
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Name of the organization to search for.
|
||||
- If not provided, all organizations in the realm are returned.
|
||||
type: str
|
||||
exact:
|
||||
description:
|
||||
- Whether the search should be an exact match.
|
||||
- Only relevant when O(name) is provided.
|
||||
type: bool
|
||||
default: true
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
- middleware_automation.keycloak.attributes.info_module
|
||||
|
||||
author:
|
||||
- Chris Brown (@chribro)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Retrieve a specific organization by name
|
||||
middleware_automation.keycloak.keycloak_organization_info:
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
auth_realm: master
|
||||
realm: MyRealm
|
||||
name: my-org
|
||||
|
||||
- name: List all organizations in a realm
|
||||
middleware_automation.keycloak.keycloak_organization_info:
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
auth_realm: master
|
||||
realm: MyRealm
|
||||
|
||||
- name: Search organizations by partial name
|
||||
middleware_automation.keycloak.keycloak_organization_info:
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
auth_realm: master
|
||||
realm: MyRealm
|
||||
name: acme
|
||||
exact: false
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
organizations:
|
||||
description: JSON representation of organizations.
|
||||
returned: always
|
||||
type: list
|
||||
elements: dict
|
||||
"""
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
name=dict(type="str"),
|
||||
realm=dict(type="str", required=True),
|
||||
exact=dict(type="bool", default=True),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
result = dict(changed=False, organizations=[])
|
||||
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
name = module.params.get("name")
|
||||
exact = module.params.get("exact")
|
||||
|
||||
filters = []
|
||||
if name:
|
||||
filters.append(f"search={quote(name, safe='')}")
|
||||
if exact:
|
||||
filters.append("exact=true")
|
||||
|
||||
filter_str = "&".join(filters) if filters else None
|
||||
|
||||
result["organizations"] = kc.get_organizations(search_filter=filter_str, realm=realm)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,133 @@
|
|||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_realm_info
|
||||
|
||||
short_description: Allows obtaining Keycloak realm public information using Keycloak API
|
||||
|
||||
# Originally added in community.general 4.3.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to get Keycloak realm public information using the Keycloak REST API.
|
||||
- The names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/latest/rest-api/index.html).
|
||||
- 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
|
||||
suitable for the API.
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.attributes
|
||||
- middleware_automation.keycloak.attributes.info_module
|
||||
|
||||
options:
|
||||
auth_keycloak_url:
|
||||
description:
|
||||
- URL to the Keycloak instance.
|
||||
type: str
|
||||
required: true
|
||||
aliases:
|
||||
- url
|
||||
validate_certs:
|
||||
description:
|
||||
- Verify TLS certificates (do not disable this in production).
|
||||
type: bool
|
||||
default: true
|
||||
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- They Keycloak realm ID.
|
||||
default: 'master'
|
||||
|
||||
author:
|
||||
- Fynn Chen (@fynncfchen)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Get a Keycloak public key
|
||||
middleware_automation.keycloak.keycloak_realm_info:
|
||||
realm: MyCustomRealm
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
realm_info:
|
||||
description:
|
||||
- Representation of the realm public information.
|
||||
returned: always
|
||||
type: dict
|
||||
contains:
|
||||
realm:
|
||||
description: Realm ID.
|
||||
type: str
|
||||
returned: always
|
||||
sample: MyRealm
|
||||
public_key:
|
||||
description: Public key of the realm.
|
||||
type: str
|
||||
returned: always
|
||||
sample: MIIBIjANBgkqhkiG9w0BAQEFAAO...
|
||||
token-service:
|
||||
description: Token endpoint URL.
|
||||
type: str
|
||||
returned: always
|
||||
sample: https://auth.example.com/realms/MyRealm/protocol/openid-connect
|
||||
account-service:
|
||||
description: Account console URL.
|
||||
type: str
|
||||
returned: always
|
||||
sample: https://auth.example.com/realms/MyRealm/account
|
||||
tokens-not-before:
|
||||
description: The token not before.
|
||||
type: int
|
||||
returned: always
|
||||
sample: 0
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = dict(
|
||||
auth_keycloak_url=dict(type="str", aliases=["url"], required=True, no_log=False),
|
||||
validate_certs=dict(type="bool", default=True),
|
||||
realm=dict(default="master"),
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||
|
||||
result = dict(changed=False, msg="", realm_info="")
|
||||
|
||||
kc = KeycloakAPI(module, {})
|
||||
|
||||
realm = module.params.get("realm")
|
||||
|
||||
realm_info = kc.get_realm_info_by_id(realm=realm)
|
||||
|
||||
result["realm_info"] = realm_info
|
||||
result["msg"] = f"Get realm public info successful for ID {realm}"
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,139 @@
|
|||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_realm_keys_metadata_info
|
||||
|
||||
short_description: Allows obtaining Keycloak realm keys metadata using Keycloak API
|
||||
|
||||
# Originally added in community.general 9.3.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to get Keycloak realm keys metadata using the Keycloak REST API.
|
||||
- The names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/latest/rest-api/index.html).
|
||||
attributes:
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- They Keycloak realm to fetch keys metadata.
|
||||
default: 'master'
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
- middleware_automation.keycloak.attributes.info_module
|
||||
|
||||
author:
|
||||
- Thomas Bach (@thomasbach-dev)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Fetch Keys metadata
|
||||
middleware_automation.keycloak.keycloak_realm_keys_metadata_info:
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
realm: MyCustomRealm
|
||||
delegate_to: localhost
|
||||
register: keycloak_keys_metadata
|
||||
|
||||
- name: Write the Keycloak keys certificate into a file
|
||||
ansible.builtin.copy:
|
||||
dest: /tmp/keycloak.cert
|
||||
content: |
|
||||
{{ keys_metadata['keycloak_keys_metadata']['keys']
|
||||
| selectattr('algorithm', 'equalto', 'RS256')
|
||||
| map(attribute='certificate')
|
||||
| first
|
||||
}}
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
|
||||
keys_metadata:
|
||||
description:
|
||||
|
||||
- Representation of the realm keys metadata (see U(https://www.keycloak.org/docs-api/latest/rest-api/index.html#KeysMetadataRepresentation)).
|
||||
returned: always
|
||||
type: dict
|
||||
contains:
|
||||
active:
|
||||
description: A mapping (that is, a dict) from key algorithms to UUIDs.
|
||||
type: dict
|
||||
returned: always
|
||||
keys:
|
||||
description: A list of dicts providing detailed information on the keys.
|
||||
type: list
|
||||
elements: dict
|
||||
returned: always
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
realm=dict(default="master"),
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", keys_metadata="")
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
|
||||
keys_metadata = kc.get_realm_keys_metadata_by_id(realm=realm)
|
||||
|
||||
result["keys_metadata"] = keys_metadata
|
||||
result["msg"] = f"Get realm keys metadata successful for ID {realm}"
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
# !/usr/bin/python
|
||||
# Copyright Jakub Danek <danek.ja@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
|
||||
# https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_realm_localization
|
||||
|
||||
short_description: Allows management of Keycloak realm localization overrides via the Keycloak API
|
||||
|
||||
# Originally added in community.general 12.4.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to manage per-locale message overrides for a Keycloak realm using the Keycloak Admin REST API.
|
||||
- Requires access via OpenID Connect; the connecting user/client must have sufficient privileges.
|
||||
- The names of module options are snake_cased versions of the names found in the Keycloak API.
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
|
||||
options:
|
||||
force:
|
||||
description:
|
||||
- If V(false), only the keys listed in the O(overrides) are modified by this module. Any other pre-existing
|
||||
keys are ignored.
|
||||
- If V(true), all locale overrides are made to match configuration of this module. For example any keys
|
||||
missing from the O(overrides) are removed regardless of O(state) value.
|
||||
type: bool
|
||||
default: false
|
||||
locale:
|
||||
description:
|
||||
- Locale code for which the overrides apply (for example, V(en), V(fi), V(de)).
|
||||
type: str
|
||||
required: true
|
||||
parent_id:
|
||||
description:
|
||||
- Name of the realm that owns the locale overrides.
|
||||
type: str
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Desired state of localization overrides for the given locale.
|
||||
- On V(present), the set of overrides for the locale are made to match O(overrides).
|
||||
If O(force) is V(true) keys not listed in O(overrides) are removed,
|
||||
and the listed keys are created or updated.
|
||||
If O(force) is V(false) keys not listed in O(overrides) are ignored,
|
||||
and the listed keys are created or updated.
|
||||
- On V(absent), overrides for the locale is removed. If O(force) is V(true), all keys are removed.
|
||||
If O(force) is V(false), only the keys listed in O(overrides) are removed.
|
||||
type: str
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
overrides:
|
||||
description:
|
||||
- List of overrides to ensure for the locale when O(state=present). Each item is a mapping with
|
||||
the record's O(overrides[].key) and its O(overrides[].value).
|
||||
- Ignored when O(state=absent).
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
suboptions:
|
||||
key:
|
||||
description:
|
||||
- The message key to override.
|
||||
type: str
|
||||
required: true
|
||||
value:
|
||||
description:
|
||||
- The override value for the message key. If omitted, value defaults to an empty string.
|
||||
type: str
|
||||
default: ""
|
||||
required: false
|
||||
|
||||
seealso:
|
||||
- module: middleware_automation.keycloak.keycloak_realm
|
||||
description: You can specify list of supported locales using O(middleware_automation.keycloak.keycloak_realm#module:supported_locales).
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author: Jakub Danek (@danekja)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Replace all overrides for locale "en" (credentials auth)
|
||||
middleware_automation.keycloak.keycloak_realm_localization:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
parent_id: my-realm
|
||||
locale: en
|
||||
state: present
|
||||
force: true
|
||||
overrides:
|
||||
- key: greeting
|
||||
value: "Hello"
|
||||
- key: farewell
|
||||
value: "Bye"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Replace listed overrides for locale "en" (credentials auth)
|
||||
middleware_automation.keycloak.keycloak_realm_localization:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
parent_id: my-realm
|
||||
locale: en
|
||||
state: present
|
||||
force: false
|
||||
overrides:
|
||||
- key: greeting
|
||||
value: "Hello"
|
||||
- key: farewell
|
||||
value: "Bye"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Ensure only one override exists for locale "fi" (token auth)
|
||||
middleware_automation.keycloak.keycloak_realm_localization:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
parent_id: my-realm
|
||||
locale: fi
|
||||
state: present
|
||||
force: true
|
||||
overrides:
|
||||
- key: app.title
|
||||
value: "Sovellukseni"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Remove all overrides for locale "de"
|
||||
middleware_automation.keycloak.keycloak_realm_localization:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
parent_id: my-realm
|
||||
locale: de
|
||||
state: absent
|
||||
force: true
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Remove only the listed overrides for locale "de"
|
||||
middleware_automation.keycloak.keycloak_realm_localization:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
parent_id: my-realm
|
||||
locale: de
|
||||
state: absent
|
||||
force: false
|
||||
overrides:
|
||||
- key: app.title
|
||||
- key: foo
|
||||
- key: bar
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
end_state:
|
||||
description:
|
||||
- Final state of localization overrides for the locale after module execution.
|
||||
- Contains the O(locale) and the list of O(overrides) as key/value items.
|
||||
returned: on success
|
||||
type: dict
|
||||
contains:
|
||||
locale:
|
||||
description: The locale code affected.
|
||||
type: str
|
||||
sample: en
|
||||
overrides:
|
||||
description: The list of overrides that exist after execution.
|
||||
type: list
|
||||
elements: dict
|
||||
sample:
|
||||
- key: greeting
|
||||
value: Hello
|
||||
- key: farewell
|
||||
value: Bye
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def _normalize_overrides(current: dict | None) -> list[dict]:
|
||||
"""
|
||||
Accepts:
|
||||
- dict: {'k1': 'v1', ...}
|
||||
Return a sorted list of {'key', 'value'}.
|
||||
|
||||
This helper provides a consistent shape for downstream comparison/diff logic.
|
||||
"""
|
||||
if not current:
|
||||
return []
|
||||
|
||||
return [{"key": k, "value": v} for k, v in sorted(current.items())]
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
# Single override record structure
|
||||
overrides_spec = dict(
|
||||
key=dict(type="str", no_log=False, required=True),
|
||||
value=dict(type="str", default=""),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
locale=dict(type="str", required=True),
|
||||
parent_id=dict(type="str", required=True),
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
overrides=dict(type="list", elements="dict", options=overrides_spec, default=[]),
|
||||
force=dict(type="bool", default=False),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=([["token", "auth_realm", "auth_username", "auth_password"]]),
|
||||
required_together=([["auth_realm", "auth_username", "auth_password"]]),
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", end_state={}, diff=dict(before={}, after={}))
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
# Convenience locals for frequently used parameters
|
||||
locale = module.params["locale"]
|
||||
state = module.params["state"]
|
||||
parent_id = module.params["parent_id"]
|
||||
force = module.params["force"]
|
||||
|
||||
desired_raw = module.params["overrides"]
|
||||
desired_overrides = _normalize_overrides({r["key"]: r.get("value") for r in desired_raw})
|
||||
|
||||
old_overrides = _normalize_overrides(kc.get_localization_values(locale, parent_id) or {})
|
||||
before = {
|
||||
"locale": locale,
|
||||
"overrides": deepcopy(old_overrides),
|
||||
}
|
||||
|
||||
# Proposed state used for diff reporting
|
||||
changeset = {
|
||||
"locale": locale,
|
||||
"overrides": [],
|
||||
}
|
||||
|
||||
result["changed"] = False
|
||||
|
||||
if state == "present":
|
||||
changeset["overrides"] = deepcopy(desired_overrides)
|
||||
|
||||
# Compute two sets:
|
||||
# - to_update: keys missing or with different values
|
||||
# - to_remove: keys existing in current state but not in desired
|
||||
to_update = []
|
||||
to_remove = deepcopy(old_overrides)
|
||||
|
||||
# Mark updates and remove matched ones from to_remove
|
||||
for record in desired_overrides:
|
||||
override_found = False
|
||||
|
||||
for override in to_remove:
|
||||
if override["key"] == record["key"]:
|
||||
override_found = True
|
||||
|
||||
# Value differs -> update needed
|
||||
if override["value"] != record["value"]:
|
||||
result["changed"] = True
|
||||
to_update.append(record)
|
||||
|
||||
# Remove processed item so what's left in to_remove are deletions
|
||||
to_remove.remove(override)
|
||||
break
|
||||
|
||||
if not override_found:
|
||||
# New key, must be created
|
||||
to_update.append(record)
|
||||
result["changed"] = True
|
||||
|
||||
# ignore any left-overs in to_remove, force is false
|
||||
if not force:
|
||||
changeset["overrides"].extend(to_remove)
|
||||
to_remove = []
|
||||
|
||||
if to_remove:
|
||||
result["changed"] = True
|
||||
|
||||
if result["changed"]:
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before, after=changeset)
|
||||
|
||||
if module.check_mode:
|
||||
result["msg"] = f"Locale {locale} overrides would be updated."
|
||||
|
||||
else:
|
||||
for override in to_remove:
|
||||
kc.delete_localization_value(locale, override["key"], parent_id)
|
||||
|
||||
for override in to_update:
|
||||
kc.set_localization_value(locale, override["key"], override["value"], parent_id)
|
||||
|
||||
result["msg"] = f"Locale {locale} overrides have been updated."
|
||||
|
||||
else:
|
||||
result["msg"] = f"Locale {locale} overrides are in sync."
|
||||
|
||||
# For accurate end_state, read back from API unless we are in check_mode
|
||||
if not module.check_mode:
|
||||
final_overrides = _normalize_overrides(kc.get_localization_values(locale, parent_id) or {})
|
||||
|
||||
else:
|
||||
final_overrides = ["overrides"]
|
||||
|
||||
result["end_state"] = {"locale": locale, "overrides": final_overrides}
|
||||
|
||||
elif state == "absent":
|
||||
if force:
|
||||
to_remove = old_overrides
|
||||
|
||||
else:
|
||||
# touch only overrides listed in parameters, leave the rest be
|
||||
to_remove = deepcopy(desired_overrides)
|
||||
to_keep = deepcopy(old_overrides)
|
||||
|
||||
for override in to_remove:
|
||||
found = False
|
||||
for keep in to_keep:
|
||||
if override["key"] == keep["key"]:
|
||||
to_keep.remove(keep)
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
to_remove.remove(override)
|
||||
|
||||
changeset["overrides"] = to_keep
|
||||
|
||||
if to_remove:
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before, after=changeset)
|
||||
|
||||
if module.check_mode:
|
||||
if result["changed"]:
|
||||
result["msg"] = f"{len(to_remove)} overrides for locale {locale} would be deleted."
|
||||
else:
|
||||
result["msg"] = f"No overrides for locale {locale} to be deleted."
|
||||
|
||||
else:
|
||||
for override in to_remove:
|
||||
kc.delete_localization_value(locale, override["key"], parent_id)
|
||||
|
||||
if result["changed"]:
|
||||
result["msg"] = f"{len(to_remove)} overrides for locale {locale} deleted."
|
||||
else:
|
||||
result["msg"] = f"No overrides for locale {locale} to be deleted."
|
||||
|
||||
result["end_state"] = changeset
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_realm_rolemapping
|
||||
|
||||
short_description: Allows administration of Keycloak realm role mappings into groups with the Keycloak API
|
||||
|
||||
# Originally added in community.general 8.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to add, remove or modify Keycloak realm role mappings into groups with 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. 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 names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/18.0/rest-api/index.html).
|
||||
- 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
|
||||
suitable for the API.
|
||||
- When updating a group_rolemapping, where possible provide the role ID to the module. This removes a lookup to the API
|
||||
to translate the name into the role ID.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the realm_rolemapping.
|
||||
- On C(present), the realm_rolemapping is created if it does not yet exist, or updated with the parameters you provide.
|
||||
- On C(absent), the realm_rolemapping is removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- They Keycloak realm under which this role_representation resides.
|
||||
default: 'master'
|
||||
|
||||
group_name:
|
||||
type: str
|
||||
description:
|
||||
- Name of the group to be mapped.
|
||||
- This parameter is required (can be replaced by gid for less API call).
|
||||
parents:
|
||||
type: list
|
||||
description:
|
||||
- List of parent groups for the group to handle sorted top to bottom.
|
||||
- Set this if your group is a subgroup and you do not provide the GID in O(gid).
|
||||
elements: dict
|
||||
suboptions:
|
||||
id:
|
||||
type: str
|
||||
description:
|
||||
- Identify parent by ID.
|
||||
- Needs less API calls than using O(parents[].name).
|
||||
- A deep parent chain can be started at any point when first given parent is given as ID.
|
||||
- Note that in principle both ID and name can be specified at the same time but current implementation only always
|
||||
use just one of them, with ID being preferred.
|
||||
name:
|
||||
type: str
|
||||
description:
|
||||
- Identify parent by name.
|
||||
- Needs more internal API calls than using O(parents[].id) to map names to ID's under the hood.
|
||||
- When giving a parent chain with only names it must be complete up to the top.
|
||||
- Note that in principle both ID and name can be specified at the same time but current implementation only always
|
||||
use just one of them, with ID being preferred.
|
||||
gid:
|
||||
type: str
|
||||
description:
|
||||
- ID of the group to be mapped.
|
||||
- This parameter is not required for updating or deleting the rolemapping but providing it reduces the number of API
|
||||
calls required.
|
||||
roles:
|
||||
description:
|
||||
- Roles to be mapped to the group.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
description:
|
||||
- Name of the role_representation.
|
||||
- This parameter is required only when creating or updating the role_representation.
|
||||
id:
|
||||
type: str
|
||||
description:
|
||||
- The unique identifier for this role_representation.
|
||||
- This parameter is not required for updating or deleting a role_representation but providing it reduces the number
|
||||
of API calls required.
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Gaëtan Daubresse (@Gaetan2907)
|
||||
- Marius Huysamen (@mhuysamen)
|
||||
- Alexander Groß (@agross)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Map a client role to a group, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_realm_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
state: present
|
||||
group_name: group1
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Map a client role to a group, authentication with token
|
||||
middleware_automation.keycloak.keycloak_realm_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
state: present
|
||||
group_name: group1
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Map a client role to a subgroup, authentication with token
|
||||
middleware_automation.keycloak.keycloak_realm_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
state: present
|
||||
group_name: subgroup1
|
||||
parents:
|
||||
- name: parent-group
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Unmap realm role from a group
|
||||
middleware_automation.keycloak.keycloak_realm_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
state: absent
|
||||
group_name: group1
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Role role1 assigned to group group1."
|
||||
|
||||
proposed:
|
||||
description: Representation of proposed client role mapping.
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {"clientId": "test"}
|
||||
|
||||
existing:
|
||||
description:
|
||||
- Representation of existing client role mapping.
|
||||
- The sample is truncated.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"adminUrl": "http://www.example.com/admin_url",
|
||||
"attributes": {
|
||||
"request.object.signature.alg": "RS256"
|
||||
}
|
||||
}
|
||||
|
||||
end_state:
|
||||
description:
|
||||
- Representation of client role mapping after module execution.
|
||||
- The sample is truncated.
|
||||
returned: on success
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"adminUrl": "http://www.example.com/admin_url",
|
||||
"attributes": {
|
||||
"request.object.signature.alg": "RS256"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
roles_spec = dict(
|
||||
name=dict(type="str"),
|
||||
id=dict(type="str"),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(default="present", choices=["present", "absent"]),
|
||||
realm=dict(default="master"),
|
||||
gid=dict(type="str"),
|
||||
group_name=dict(type="str"),
|
||||
parents=dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=dict(id=dict(type="str"), name=dict(type="str")),
|
||||
),
|
||||
roles=dict(type="list", elements="dict", options=roles_spec),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, proposed={}, existing={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
state = module.params.get("state")
|
||||
gid = module.params.get("gid")
|
||||
group_name = module.params.get("group_name")
|
||||
roles = module.params.get("roles")
|
||||
parents = module.params.get("parents")
|
||||
|
||||
# Check the parameters
|
||||
if gid is None and group_name is None:
|
||||
module.fail_json(msg="Either the `group_name` or `gid` has to be specified.")
|
||||
|
||||
# Get the potential missing parameters
|
||||
if gid is None:
|
||||
group_rep = kc.get_group_by_name(group_name, realm=realm, parents=parents)
|
||||
if group_rep is not None:
|
||||
gid = group_rep["id"]
|
||||
else:
|
||||
module.fail_json(msg=f"Could not fetch group {group_name}:")
|
||||
else:
|
||||
group_rep = kc.get_group_by_groupid(gid, realm=realm)
|
||||
|
||||
if roles is None:
|
||||
module.exit_json(msg="Nothing to do (no roles specified).")
|
||||
else:
|
||||
for role in roles:
|
||||
if role["name"] is None and role["id"] is None:
|
||||
module.fail_json(msg="Either the `name` or `id` has to be specified on each role.")
|
||||
# Fetch missing role_id
|
||||
if role["id"] is None:
|
||||
role_rep = kc.get_realm_role(role["name"], realm=realm)
|
||||
if role_rep is not None:
|
||||
role["id"] = role_rep["id"]
|
||||
else:
|
||||
module.fail_json(msg=f"Could not fetch realm role {role['name']} by name:")
|
||||
# Fetch missing role_name
|
||||
else:
|
||||
for realm_role in kc.get_realm_roles(realm=realm):
|
||||
if realm_role["id"] == role["id"]:
|
||||
role["name"] = realm_role["name"]
|
||||
break
|
||||
|
||||
if role["name"] is None:
|
||||
module.fail_json(msg=f"Could not fetch realm role {role['id']} by ID")
|
||||
|
||||
assigned_roles_before = group_rep.get("realmRoles", [])
|
||||
|
||||
result["existing"] = assigned_roles_before
|
||||
result["proposed"] = list(assigned_roles_before) if assigned_roles_before else []
|
||||
|
||||
update_roles = []
|
||||
for role in roles:
|
||||
# Fetch roles to assign if state present
|
||||
if state == "present":
|
||||
if any(assigned == role["name"] for assigned in assigned_roles_before):
|
||||
pass
|
||||
else:
|
||||
update_roles.append(
|
||||
{
|
||||
"id": role["id"],
|
||||
"name": role["name"],
|
||||
}
|
||||
)
|
||||
result["proposed"].append(role["name"])
|
||||
# Fetch roles to remove if state absent
|
||||
else:
|
||||
if any(assigned == role["name"] for assigned in assigned_roles_before):
|
||||
update_roles.append(
|
||||
{
|
||||
"id": role["id"],
|
||||
"name": role["name"],
|
||||
}
|
||||
)
|
||||
if role["name"] in result["proposed"]: # Handle double removal
|
||||
result["proposed"].remove(role["name"])
|
||||
|
||||
if len(update_roles):
|
||||
result["changed"] = True
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=assigned_roles_before, after=result["proposed"])
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
if state == "present":
|
||||
# Assign roles
|
||||
kc.add_group_realm_rolemapping(gid=gid, role_rep=update_roles, realm=realm)
|
||||
result["msg"] = f"Realm roles {update_roles} assigned to groupId {gid}."
|
||||
else:
|
||||
# Remove mapping of role
|
||||
kc.delete_group_realm_rolemapping(gid=gid, role_rep=update_roles, realm=realm)
|
||||
result["msg"] = f"Realm roles {update_roles} removed from groupId {gid}."
|
||||
|
||||
if gid is None:
|
||||
assigned_roles_after = kc.get_group_by_name(group_name, realm=realm, parents=parents).get("realmRoles", [])
|
||||
else:
|
||||
assigned_roles_after = kc.get_group_by_groupid(gid, realm=realm).get("realmRoles", [])
|
||||
result["end_state"] = assigned_roles_after
|
||||
module.exit_json(**result)
|
||||
# Do nothing
|
||||
else:
|
||||
result["changed"] = False
|
||||
result["msg"] = (
|
||||
f"Nothing to do, roles {roles} are {'mapped' if state == 'present' else 'not mapped'} with group {group_name}."
|
||||
)
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_realm_users_info
|
||||
|
||||
short_description: Retrieve users from a Keycloak realm using the Keycloak API
|
||||
|
||||
# Originally added in community.general 13.1.0
|
||||
version_added: "3.1.0"
|
||||
|
||||
description:
|
||||
- This module retrieves all users from a specified Keycloak realm using the Keycloak REST API.
|
||||
- Access to the REST API is performed via OpenID Connect. The user and client used must have the necessary permissions.
|
||||
- Authentication can be performed either with username/password or with a token.
|
||||
- The names of module options are snake_case versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/latest/rest-api/index.html).
|
||||
|
||||
options:
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- The Keycloak realm from which users should be retrieved.
|
||||
default: 'master'
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
- middleware_automation.keycloak.attributes.info_module
|
||||
|
||||
author:
|
||||
- Felix Grzelka (@felix-grzelka)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: List all users in the "MyCustomRealm" realm using username/password authentication
|
||||
middleware_automation.keycloak.keycloak_realm_users_info:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: List all users in the "MyCustomRealm" realm using a token
|
||||
middleware_automation.keycloak.keycloak_realm_users_info:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
users:
|
||||
description: List of users in the specified realm.
|
||||
returned: always
|
||||
type: list
|
||||
elements: dict
|
||||
sample:
|
||||
- id: "1234-5678-90"
|
||||
username: "user1"
|
||||
email: "user1@example.com"
|
||||
- id: "2345-6789-01"
|
||||
username: "user2"
|
||||
email: "user2@example.com"
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
realm=dict(default="master"),
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", users="")
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
|
||||
result["users"] = kc.get_realm_users(realm=realm)
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,452 @@
|
|||
|
||||
# Copyright (c) 2019, Adam Goossens <adam.goossens@gmail.com>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_role
|
||||
|
||||
short_description: Allows administration of Keycloak roles using Keycloak API
|
||||
|
||||
# Originally added in community.general 3.4.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to add, remove or modify Keycloak roles 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. 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 names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/latest/rest-api/index.html).
|
||||
- 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
|
||||
suitable for the API.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the role.
|
||||
- On V(present), the role is created if it does not yet exist, or updated with the parameters you provide.
|
||||
- On V(absent), the role is removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
- Name of the role.
|
||||
- This parameter is required.
|
||||
description:
|
||||
type: str
|
||||
description:
|
||||
- The role description.
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- The Keycloak realm under which this role resides.
|
||||
default: 'master'
|
||||
|
||||
client_id:
|
||||
type: str
|
||||
description:
|
||||
- If the role is a client role, the client ID under which it resides.
|
||||
- If this parameter is absent, the role is considered a realm role.
|
||||
attributes:
|
||||
type: dict
|
||||
description:
|
||||
- A dict of key/value pairs to set as custom attributes for the role.
|
||||
- Values may be single values (for example a string) or a list of strings.
|
||||
composite:
|
||||
description:
|
||||
- If V(true), the role is a composition of other realm and/or client role.
|
||||
default: false
|
||||
type: bool
|
||||
composites:
|
||||
description:
|
||||
- List of roles to include to the composite realm role.
|
||||
- If the composite role is a client role, the C(clientId) (not ID of the client) must be specified.
|
||||
default: []
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- Name of the role. This can be the name of a REALM role or a client role.
|
||||
type: str
|
||||
required: true
|
||||
client_id:
|
||||
description:
|
||||
- Client ID if the role is a client role. Do not include this option for a REALM role.
|
||||
- Use the client ID you can see in the Keycloak console, not the technical ID of the client.
|
||||
type: str
|
||||
aliases:
|
||||
- clientId
|
||||
state:
|
||||
description:
|
||||
- Create the composite if present, remove it if absent.
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Laurent Paumier (@laurpaum)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create a Keycloak realm role, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_role:
|
||||
name: my-new-kc-role
|
||||
realm: MyCustomRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a Keycloak realm role, authentication with token
|
||||
middleware_automation.keycloak.keycloak_role:
|
||||
name: my-new-kc-role
|
||||
realm: MyCustomRealm
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a Keycloak client role
|
||||
middleware_automation.keycloak.keycloak_role:
|
||||
name: my-new-kc-role
|
||||
realm: MyCustomRealm
|
||||
client_id: MyClient
|
||||
state: present
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Delete a Keycloak role
|
||||
middleware_automation.keycloak.keycloak_role:
|
||||
name: my-role-for-deletion
|
||||
state: absent
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Create a keycloak role with some custom attributes
|
||||
middleware_automation.keycloak.keycloak_role:
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
name: my-new-role
|
||||
attributes:
|
||||
attrib1: value1
|
||||
attrib2: value2
|
||||
attrib3:
|
||||
- with
|
||||
- numerous
|
||||
- individual
|
||||
- list
|
||||
- items
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Role myrole has been updated"
|
||||
|
||||
proposed:
|
||||
description: Representation of proposed role.
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {"description": "My updated test description"}
|
||||
|
||||
existing:
|
||||
description: Representation of existing role.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"attributes": {},
|
||||
"clientRole": true,
|
||||
"composite": false,
|
||||
"containerId": "9f03eb61-a826-4771-a9fd-930e06d2d36a",
|
||||
"description": "My client test role",
|
||||
"id": "561703dd-0f38-45ff-9a5a-0c978f794547",
|
||||
"name": "myrole"
|
||||
}
|
||||
|
||||
end_state:
|
||||
description: Representation of role after module execution (sample is truncated).
|
||||
returned: on success
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"attributes": {},
|
||||
"clientRole": true,
|
||||
"composite": false,
|
||||
"containerId": "9f03eb61-a826-4771-a9fd-930e06d2d36a",
|
||||
"description": "My updated client test role",
|
||||
"id": "561703dd-0f38-45ff-9a5a-0c978f794547",
|
||||
"name": "myrole"
|
||||
}
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
camel,
|
||||
get_token,
|
||||
is_struct_included,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
composites_spec = dict(
|
||||
name=dict(type="str", required=True),
|
||||
client_id=dict(type="str", aliases=["clientId"]),
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
name=dict(type="str", required=True),
|
||||
description=dict(type="str"),
|
||||
realm=dict(type="str", default="master"),
|
||||
client_id=dict(type="str"),
|
||||
attributes=dict(type="dict"),
|
||||
composites=dict(type="list", default=[], options=composites_spec, elements="dict"),
|
||||
composite=dict(type="bool", default=False),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, proposed={}, existing={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
clientid = module.params.get("client_id")
|
||||
name = module.params.get("name")
|
||||
state = module.params.get("state")
|
||||
|
||||
# attributes in Keycloak have their values returned as lists
|
||||
# using the API. attributes is a dict, so we'll transparently convert
|
||||
# the values to lists.
|
||||
if module.params.get("attributes") is not None:
|
||||
for key, val in module.params["attributes"].items():
|
||||
module.params["attributes"][key] = [val] if not isinstance(val, list) else val
|
||||
|
||||
# Filter and map the parameters names that apply to the role
|
||||
role_params = [
|
||||
x
|
||||
for x in module.params
|
||||
if x not in list(keycloak_argument_spec().keys()) + ["state", "realm", "client_id"]
|
||||
and module.params.get(x) is not None
|
||||
]
|
||||
|
||||
# See if it already exists in Keycloak
|
||||
if clientid is None:
|
||||
before_role = kc.get_realm_role(name, realm)
|
||||
else:
|
||||
before_role = kc.get_client_role(name, clientid, realm)
|
||||
|
||||
if before_role is None:
|
||||
before_role = {}
|
||||
|
||||
# Build a proposed changeset from parameters given to this module
|
||||
changeset = {}
|
||||
|
||||
for param in role_params:
|
||||
new_param_value = module.params.get(param)
|
||||
old_value = before_role[param] if param in before_role else None
|
||||
if new_param_value != old_value:
|
||||
changeset[camel(param)] = copy.deepcopy(new_param_value)
|
||||
|
||||
# Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
|
||||
desired_role = copy.deepcopy(before_role)
|
||||
desired_role.update(changeset)
|
||||
|
||||
result["proposed"] = changeset
|
||||
result["existing"] = before_role
|
||||
|
||||
# Cater for when it doesn't exist (an empty dict)
|
||||
if not before_role:
|
||||
if state == "absent":
|
||||
# Do nothing and exit
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after="")
|
||||
result["changed"] = False
|
||||
result["end_state"] = {}
|
||||
result["msg"] = "Role does not exist, doing nothing."
|
||||
module.exit_json(**result)
|
||||
|
||||
# Process a creation
|
||||
result["changed"] = True
|
||||
|
||||
if name is None:
|
||||
module.fail_json(msg="name must be specified when creating a new role")
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before="", after=desired_role)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# create it
|
||||
if clientid is None:
|
||||
kc.create_realm_role(desired_role, realm)
|
||||
after_role = kc.get_realm_role(name, realm)
|
||||
else:
|
||||
kc.create_client_role(desired_role, clientid, realm)
|
||||
after_role = kc.get_client_role(name, clientid, realm)
|
||||
|
||||
if after_role["composite"]:
|
||||
after_role["composites"] = kc.get_role_composites(rolerep=after_role, clientid=clientid, realm=realm)
|
||||
|
||||
result["end_state"] = after_role
|
||||
|
||||
result["msg"] = f"Role {name} has been created"
|
||||
module.exit_json(**result)
|
||||
|
||||
else:
|
||||
if state == "present":
|
||||
compare_exclude = ["clientId"]
|
||||
if (
|
||||
"composites" in desired_role
|
||||
and isinstance(desired_role["composites"], list)
|
||||
and len(desired_role["composites"]) > 0
|
||||
):
|
||||
composites = kc.get_role_composites(rolerep=before_role, clientid=clientid, realm=realm)
|
||||
before_role["composites"] = []
|
||||
for composite in composites:
|
||||
before_composite = {}
|
||||
if composite["clientRole"]:
|
||||
composite_client = kc.get_client_by_id(id=composite["containerId"], realm=realm)
|
||||
before_composite["client_id"] = composite_client["clientId"]
|
||||
else:
|
||||
before_composite["client_id"] = None
|
||||
before_composite["name"] = composite["name"]
|
||||
before_composite["state"] = "present"
|
||||
before_role["composites"].append(before_composite)
|
||||
else:
|
||||
compare_exclude.append("composites")
|
||||
# Process an update
|
||||
# no changes
|
||||
if is_struct_included(desired_role, before_role, exclude=compare_exclude):
|
||||
result["changed"] = False
|
||||
result["end_state"] = desired_role
|
||||
result["msg"] = f"No changes required to role {name}."
|
||||
module.exit_json(**result)
|
||||
|
||||
# doing an update
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_role, after=desired_role)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# do the update
|
||||
if clientid is None:
|
||||
kc.update_realm_role(desired_role, realm)
|
||||
after_role = kc.get_realm_role(name, realm)
|
||||
else:
|
||||
kc.update_client_role(desired_role, clientid, realm)
|
||||
after_role = kc.get_client_role(name, clientid, realm)
|
||||
if after_role["composite"]:
|
||||
after_role["composites"] = kc.get_role_composites(rolerep=after_role, clientid=clientid, realm=realm)
|
||||
|
||||
result["end_state"] = after_role
|
||||
|
||||
result["msg"] = f"Role {name} has been updated"
|
||||
module.exit_json(**result)
|
||||
|
||||
else:
|
||||
# Process a deletion (because state was not 'present')
|
||||
result["changed"] = True
|
||||
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_role, after="")
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
|
||||
# delete it
|
||||
if clientid is None:
|
||||
kc.delete_realm_role(name, realm)
|
||||
else:
|
||||
kc.delete_client_role(name, clientid, realm)
|
||||
|
||||
result["end_state"] = {}
|
||||
|
||||
result["msg"] = f"Role {name} has been deleted"
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,599 @@
|
|||
|
||||
# Copyright (c) 2019, INSPQ (@elfelip)
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_user
|
||||
short_description: Create and configure a user in Keycloak
|
||||
description:
|
||||
- This module creates, removes, or updates Keycloak users.
|
||||
# Originally added in community.general 7.1.0
|
||||
version_added: "3.0.0"
|
||||
options:
|
||||
auth_username:
|
||||
aliases: []
|
||||
realm:
|
||||
description:
|
||||
- The name of the realm in which is the client.
|
||||
default: master
|
||||
type: str
|
||||
username:
|
||||
description:
|
||||
- Username for the user.
|
||||
required: true
|
||||
type: str
|
||||
id:
|
||||
description:
|
||||
- ID of the user on the Keycloak server if known.
|
||||
type: str
|
||||
enabled:
|
||||
description:
|
||||
- Enabled user.
|
||||
type: bool
|
||||
email_verified:
|
||||
description:
|
||||
- Set or reset the C(emailVerified) flag of the user.
|
||||
- When O(email_verified_behavior=no_defaults), the default value of this option becomes C(null) and
|
||||
that causes the module not to change any existing value for that attribute.
|
||||
type: bool
|
||||
aliases:
|
||||
- emailVerified
|
||||
first_name:
|
||||
description:
|
||||
- The user's first name.
|
||||
type: str
|
||||
aliases:
|
||||
- firstName
|
||||
last_name:
|
||||
description:
|
||||
- The user's last name.
|
||||
type: str
|
||||
aliases:
|
||||
- lastName
|
||||
email:
|
||||
description:
|
||||
- User email.
|
||||
type: str
|
||||
federation_link:
|
||||
description:
|
||||
- Federation Link.
|
||||
type: str
|
||||
aliases:
|
||||
- federationLink
|
||||
service_account_client_id:
|
||||
description:
|
||||
- Description of the client Application.
|
||||
type: str
|
||||
aliases:
|
||||
- serviceAccountClientId
|
||||
client_consents:
|
||||
description:
|
||||
- Client Authenticator Type.
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
aliases:
|
||||
- clientConsents
|
||||
suboptions:
|
||||
client_id:
|
||||
description:
|
||||
- Client ID of the client role. Not the technical ID of the client.
|
||||
type: str
|
||||
required: true
|
||||
aliases:
|
||||
- clientId
|
||||
roles:
|
||||
description:
|
||||
- List of client roles to assign to the user.
|
||||
type: list
|
||||
required: true
|
||||
elements: str
|
||||
groups:
|
||||
description:
|
||||
- List of groups for the user.
|
||||
- Groups can be referenced by their name, like V(staff), or their path, like V(/staff/engineering). The path syntax
|
||||
allows you to reference subgroups, which is not possible otherwise.
|
||||
- Using the path is possible since middleware_automation.keycloak 3.0.0.
|
||||
type: list
|
||||
elements: dict
|
||||
default: []
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- Name of the group.
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- Control whether the user must be member of this group or not.
|
||||
choices: ["present", "absent"]
|
||||
default: present
|
||||
type: str
|
||||
credentials:
|
||||
description:
|
||||
- User credentials.
|
||||
default: []
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
type:
|
||||
description:
|
||||
- Credential type.
|
||||
type: str
|
||||
required: true
|
||||
value:
|
||||
description:
|
||||
- Value of the credential.
|
||||
type: str
|
||||
required: true
|
||||
temporary:
|
||||
description:
|
||||
- If V(true), the users are required to reset their credentials at next login.
|
||||
type: bool
|
||||
default: false
|
||||
required_actions:
|
||||
description:
|
||||
- Set or reset a user's required actions.
|
||||
type: list
|
||||
elements: str
|
||||
aliases:
|
||||
- requiredActions
|
||||
federated_identities:
|
||||
description:
|
||||
- List of IDPs of user.
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
aliases:
|
||||
- federatedIdentities
|
||||
attributes:
|
||||
description:
|
||||
- List of user attributes.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- Name of the attribute.
|
||||
type: str
|
||||
values:
|
||||
description:
|
||||
- Values for the attribute as list.
|
||||
type: list
|
||||
elements: str
|
||||
state:
|
||||
description:
|
||||
- Control whether the attribute must exists or not.
|
||||
choices: ["present", "absent"]
|
||||
default: present
|
||||
type: str
|
||||
access:
|
||||
description:
|
||||
- List user access.
|
||||
type: dict
|
||||
disableable_credential_types:
|
||||
description:
|
||||
- List user Credential Type.
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
aliases:
|
||||
- disableableCredentialTypes
|
||||
origin:
|
||||
description:
|
||||
- User origin.
|
||||
type: str
|
||||
self:
|
||||
description:
|
||||
- User self administration.
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- Control whether the user should exists or not.
|
||||
choices: ["present", "absent"]
|
||||
default: present
|
||||
type: str
|
||||
force:
|
||||
description:
|
||||
- If V(true), allows to remove user and recreate it.
|
||||
type: bool
|
||||
default: false
|
||||
email_verified_behavior:
|
||||
description:
|
||||
- The O(email_verified) option used to have a default value. This caused problems when the
|
||||
user expects different behavior from keycloak by default.
|
||||
- The default value of this option is V(compatibility), which will ensure that the old default value
|
||||
for O(email_verified) is used.
|
||||
- When set to V(no_defaults), the module will not change existing values of O(email_verified) if no value is specified.
|
||||
type: str
|
||||
choices:
|
||||
- compatibility
|
||||
- no_defaults
|
||||
default: compatibility
|
||||
# Originally added in community.general 13.1.0
|
||||
version_added: "3.0.0"
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
notes:
|
||||
- The module does not modify the user ID of an existing user.
|
||||
author:
|
||||
- Philippe Gauthier (@elfelip)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create a user user1
|
||||
middleware_automation.keycloak.keycloak_user:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: master
|
||||
username: user1
|
||||
firstName: user1
|
||||
lastName: user1
|
||||
email: user1
|
||||
enabled: true
|
||||
emailVerified: false
|
||||
credentials:
|
||||
- type: password
|
||||
value: password
|
||||
temporary: false
|
||||
attributes:
|
||||
- name: attr1
|
||||
values:
|
||||
- value1
|
||||
state: present
|
||||
- name: attr2
|
||||
values:
|
||||
- value2
|
||||
state: absent
|
||||
groups:
|
||||
- name: group1
|
||||
state: present
|
||||
state: present
|
||||
|
||||
- name: Re-create a User
|
||||
middleware_automation.keycloak.keycloak_user:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: master
|
||||
username: user1
|
||||
firstName: user1
|
||||
lastName: user1
|
||||
email: user1
|
||||
enabled: true
|
||||
emailVerified: false
|
||||
credentials:
|
||||
- type: password
|
||||
value: password
|
||||
temporary: false
|
||||
attributes:
|
||||
- name: attr1
|
||||
values:
|
||||
- value1
|
||||
state: present
|
||||
- name: attr2
|
||||
values:
|
||||
- value2
|
||||
state: absent
|
||||
groups:
|
||||
- name: group1
|
||||
state: present
|
||||
state: present
|
||||
|
||||
- name: Re-create a User
|
||||
middleware_automation.keycloak.keycloak_user:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: master
|
||||
username: user1
|
||||
firstName: user1
|
||||
lastName: user1
|
||||
email: user1
|
||||
enabled: true
|
||||
emailVerified: false
|
||||
credentials:
|
||||
- type: password
|
||||
value: password
|
||||
temporary: false
|
||||
attributes:
|
||||
- name: attr1
|
||||
values:
|
||||
- value1
|
||||
state: present
|
||||
- name: attr2
|
||||
values:
|
||||
- value2
|
||||
state: absent
|
||||
groups:
|
||||
- name: group1
|
||||
state: present
|
||||
state: present
|
||||
force: true
|
||||
|
||||
- name: Remove User
|
||||
middleware_automation.keycloak.keycloak_user:
|
||||
auth_keycloak_url: http://localhost:8080
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: master
|
||||
username: user1
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
proposed:
|
||||
description: Representation of the proposed user.
|
||||
returned: on success
|
||||
type: dict
|
||||
existing:
|
||||
description: Representation of the existing user.
|
||||
returned: on success
|
||||
type: dict
|
||||
end_state:
|
||||
description: Representation of the user after module execution.
|
||||
returned: on success
|
||||
type: dict
|
||||
user_created:
|
||||
description: Indicates whether a user was created.
|
||||
returned: in success
|
||||
type: bool
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
camel,
|
||||
get_token,
|
||||
is_struct_included,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
argument_spec["auth_username"]["aliases"] = []
|
||||
credential_spec = dict(
|
||||
type=dict(type="str", required=True),
|
||||
value=dict(type="str", required=True, no_log=True),
|
||||
temporary=dict(type="bool", default=False),
|
||||
)
|
||||
client_consents_spec = dict(
|
||||
client_id=dict(type="str", required=True, aliases=["clientId"]),
|
||||
roles=dict(type="list", elements="str", required=True),
|
||||
)
|
||||
attributes_spec = dict(
|
||||
name=dict(type="str"),
|
||||
values=dict(type="list", elements="str"),
|
||||
state=dict(type="str", choices=["present", "absent"], default="present"),
|
||||
)
|
||||
groups_spec = dict(name=dict(type="str"), state=dict(type="str", choices=["present", "absent"], default="present"))
|
||||
meta_args = dict(
|
||||
realm=dict(type="str", default="master"),
|
||||
self=dict(type="str"),
|
||||
id=dict(type="str"),
|
||||
username=dict(type="str", required=True),
|
||||
first_name=dict(type="str", aliases=["firstName"]),
|
||||
last_name=dict(type="str", aliases=["lastName"]),
|
||||
email=dict(type="str"),
|
||||
enabled=dict(type="bool"),
|
||||
email_verified=dict(type="bool", aliases=["emailVerified"]),
|
||||
federation_link=dict(type="str", aliases=["federationLink"]),
|
||||
service_account_client_id=dict(type="str", aliases=["serviceAccountClientId"]),
|
||||
attributes=dict(type="list", elements="dict", options=attributes_spec),
|
||||
access=dict(type="dict"),
|
||||
groups=dict(type="list", default=[], elements="dict", options=groups_spec),
|
||||
disableable_credential_types=dict(
|
||||
type="list", default=[], aliases=["disableableCredentialTypes"], elements="str"
|
||||
),
|
||||
required_actions=dict(type="list", aliases=["requiredActions"], elements="str"),
|
||||
credentials=dict(type="list", default=[], elements="dict", options=credential_spec),
|
||||
federated_identities=dict(type="list", default=[], aliases=["federatedIdentities"], elements="str"),
|
||||
client_consents=dict(
|
||||
type="list", default=[], aliases=["clientConsents"], elements="dict", options=client_consents_spec
|
||||
),
|
||||
origin=dict(type="str"),
|
||||
state=dict(choices=["absent", "present"], default="present"),
|
||||
force=dict(type="bool", default=False),
|
||||
email_verified_behavior=dict(type="str", choices=["compatibility", "no_defaults"], default="compatibility"),
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, proposed={}, existing={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
state = module.params.get("state")
|
||||
force = module.params.get("force")
|
||||
username = module.params.get("username")
|
||||
groups = module.params.get("groups")
|
||||
|
||||
# If there is no value for email_verified, check if we should to set the old default
|
||||
if module.params["email_verified"] is None and module.params["email_verified_behavior"] == "compatibility":
|
||||
module.params["email_verified"] = False
|
||||
|
||||
ignored_arguments = list(keycloak_argument_spec().keys()) + [
|
||||
"state",
|
||||
"realm",
|
||||
"force",
|
||||
"groups",
|
||||
"email_verified_behavior",
|
||||
]
|
||||
|
||||
# Filter and map the parameters names that apply to the user
|
||||
user_params = [x for x in module.params if x not in ignored_arguments and module.params[x] is not None]
|
||||
|
||||
before_user = kc.get_user_by_username(username=username, realm=realm)
|
||||
|
||||
if before_user is None:
|
||||
before_user = {}
|
||||
|
||||
changeset = {}
|
||||
|
||||
for param in user_params:
|
||||
new_param_value = module.params.get(param)
|
||||
if param == "attributes" and param in before_user:
|
||||
old_value = kc.convert_keycloak_user_attributes_dict_to_module_list(attributes=before_user["attributes"])
|
||||
else:
|
||||
old_value = before_user[param] if param in before_user else None
|
||||
if new_param_value != old_value:
|
||||
if old_value is not None and param == "attributes":
|
||||
for old_attribute in old_value:
|
||||
old_attribute_found = False
|
||||
for new_attribute in new_param_value:
|
||||
if new_attribute["name"] == old_attribute["name"]:
|
||||
old_attribute_found = True
|
||||
if not old_attribute_found:
|
||||
new_param_value.append(copy.deepcopy(old_attribute))
|
||||
if isinstance(new_param_value, dict):
|
||||
changeset[camel(param)] = copy.deepcopy(new_param_value)
|
||||
else:
|
||||
changeset[camel(param)] = new_param_value
|
||||
# Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
|
||||
desired_user = copy.deepcopy(before_user)
|
||||
desired_user.update(changeset)
|
||||
|
||||
if before_user:
|
||||
before_groups = kc.get_user_groups(user_id=before_user["id"], realm=realm)
|
||||
before_user["groups"] = before_groups
|
||||
else:
|
||||
before_groups = []
|
||||
|
||||
result["proposed"] = changeset
|
||||
result["existing"] = before_user
|
||||
# Default values for user_created
|
||||
result["user_created"] = False
|
||||
changed = False
|
||||
after_user = {}
|
||||
|
||||
user_compare_excludes = [
|
||||
"access",
|
||||
"notBefore",
|
||||
"createdTimestamp",
|
||||
"totp",
|
||||
"credentials",
|
||||
"disableableCredentialTypes",
|
||||
"groups",
|
||||
"clientConsents",
|
||||
"federatedIdentities",
|
||||
]
|
||||
|
||||
if state == "absent":
|
||||
if not before_user:
|
||||
# Do nothing and exit
|
||||
result["msg"] = "User does not exist, doing nothing."
|
||||
else:
|
||||
# Delete user
|
||||
if not module.check_mode:
|
||||
kc.delete_user(user_id=before_user["id"], realm=realm)
|
||||
result["msg"] = f"User {before_user['username']} deleted"
|
||||
changed = True
|
||||
else:
|
||||
if (not before_user or force) and username is None:
|
||||
module.fail_json(msg="username must be specified when creating a new user")
|
||||
|
||||
if force and before_user and not module.check_mode: # If the force option is set to true
|
||||
# Delete the existing user
|
||||
kc.delete_user(user_id=before_user["id"], realm=realm)
|
||||
|
||||
if not before_user or force:
|
||||
# Create a new user
|
||||
if not module.check_mode:
|
||||
# Create the user
|
||||
after_user = kc.create_user(userrep=desired_user, realm=realm)
|
||||
if after_user is None:
|
||||
module.fail_json(
|
||||
msg=f"User {desired_user['username']} was created in realm {realm} but could not be retrieved",
|
||||
)
|
||||
# Add user ID to desired_user for group updates
|
||||
desired_user["id"] = after_user["id"]
|
||||
else:
|
||||
after_user = desired_user
|
||||
|
||||
result["msg"] = f"User {desired_user['username']} created"
|
||||
result["user_created"] = True
|
||||
changed = True
|
||||
else:
|
||||
# Update an existing user
|
||||
# Add user ID to new representation
|
||||
desired_user["id"] = before_user["id"]
|
||||
|
||||
# Compare users
|
||||
if not (
|
||||
is_struct_included(desired_user, before_user, user_compare_excludes, empty_list_result=False)
|
||||
): # If the new user introduces a change to the existing user
|
||||
# Update the user
|
||||
if not module.check_mode:
|
||||
after_user = kc.update_user(userrep=desired_user, realm=realm)
|
||||
changed = True
|
||||
|
||||
if not after_user:
|
||||
# no change
|
||||
after_user = desired_user
|
||||
|
||||
# set user groups
|
||||
if not module.check_mode:
|
||||
changed |= kc.update_user_groups_membership(userrep=desired_user, groups=groups, realm=realm)
|
||||
|
||||
present_groups = [g["name"] for g in groups if g["state"] == "present"]
|
||||
absent_groups = [g["name"] for g in groups if g["state"] == "absent"]
|
||||
|
||||
desired_user["groups"] = (set(before_groups) | set(present_groups)) - set(absent_groups)
|
||||
|
||||
if module.check_mode:
|
||||
# check if group meberships would have changed
|
||||
changed |= not is_struct_included(
|
||||
desired_user["groups"], before_groups, user_compare_excludes, empty_list_result=False
|
||||
)
|
||||
else:
|
||||
after_user["groups"] = kc.get_user_groups(user_id=desired_user["id"], realm=realm)
|
||||
|
||||
if not result["msg"]:
|
||||
if changed:
|
||||
result["msg"] = f"User {desired_user['username']} updated"
|
||||
else:
|
||||
result["msg"] = f"No changes made for user {desired_user['username']}"
|
||||
result["end_state"] = after_user
|
||||
result["changed"] = changed
|
||||
result["diff"] = dict(before=before_user, after=after_user)
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
|
||||
# Copyright (c) 2025, mariusbertram <marius@brtrm.de>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_user_execute_actions_email
|
||||
|
||||
short_description: Send a Keycloak execute-actions email to a user
|
||||
|
||||
# Originally added in community.general 12.0.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- Triggers the Keycloak endpoint C(execute-actions-email) for a user.
|
||||
This sends an email with one or more required actions the user must complete (for example resetting the password).
|
||||
- If no O(actions) list is provided, the default action C(UPDATE_PASSWORD) is used.
|
||||
- You must supply either the user's O(id) or O(username). Supplying only C(username) causes an extra lookup call.
|
||||
- This module always reports RV(ignore:changed=true) because sending an email is a side effect and cannot be made idempotent.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
options:
|
||||
auth_username:
|
||||
aliases: []
|
||||
realm:
|
||||
description:
|
||||
- The Keycloak realm where the user resides.
|
||||
type: str
|
||||
default: master
|
||||
id:
|
||||
description:
|
||||
- The unique ID (UUID) of the user.
|
||||
- Mutually exclusive with O(username).
|
||||
type: str
|
||||
username:
|
||||
description:
|
||||
- Username of the user.
|
||||
- Mutually exclusive with O(id).
|
||||
type: str
|
||||
actions:
|
||||
description:
|
||||
- List of required actions to include in the email.
|
||||
type: list
|
||||
elements: str
|
||||
default:
|
||||
- UPDATE_PASSWORD
|
||||
client_id:
|
||||
description:
|
||||
- Optional client ID used for the redirect link.
|
||||
aliases: [clientId]
|
||||
type: str
|
||||
redirect_uri:
|
||||
description:
|
||||
- Optional redirect URI. Must be valid for the given client if O(client_id) is set.
|
||||
aliases: [redirectUri]
|
||||
type: str
|
||||
lifespan:
|
||||
description:
|
||||
- Optional lifespan (in seconds) for the action token (supported on newer Keycloak versions). Forwarded as query parameter if provided.
|
||||
type: int
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
author:
|
||||
- Marius Bertram (@mariusbertram)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Password reset email (default action) with 1h lifespan
|
||||
middleware_automation.keycloak.keycloak_user_execute_actions_email:
|
||||
username: johndoe
|
||||
realm: MyRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: ADMIN
|
||||
auth_password: SECRET
|
||||
lifespan: 3600
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Multiple required actions using token auth
|
||||
middleware_automation.keycloak.keycloak_user_execute_actions_email:
|
||||
username: johndoe
|
||||
actions:
|
||||
- UPDATE_PASSWORD
|
||||
- VERIFY_EMAIL
|
||||
realm: MyRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Email by user id with redirect
|
||||
middleware_automation.keycloak.keycloak_user_execute_actions_email:
|
||||
id: 9d59aa76-2755-48c6-b1af-beb70a82c3cd
|
||||
client_id: my-frontend
|
||||
redirect_uri: https://app.example.com/post-actions
|
||||
actions:
|
||||
- UPDATE_PASSWORD
|
||||
realm: MyRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: ADMIN
|
||||
auth_password: SECRET
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
user_id:
|
||||
description: The user ID the email was (or would be, in check mode) sent to.
|
||||
returned: success
|
||||
type: str
|
||||
actions:
|
||||
description: List of actions included in the email.
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
# Avoid alias collision as in keycloak_user: clear auth_username aliases locally
|
||||
argument_spec["auth_username"]["aliases"] = []
|
||||
|
||||
meta_args = dict(
|
||||
realm=dict(type="str", default="master"),
|
||||
id=dict(type="str"),
|
||||
username=dict(type="str"),
|
||||
actions=dict(type="list", elements="str", default=["UPDATE_PASSWORD"]),
|
||||
client_id=dict(type="str", aliases=["clientId"]),
|
||||
redirect_uri=dict(type="str", aliases=["redirectUri"]),
|
||||
lifespan=dict(type="int"),
|
||||
)
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=[["id", "username"]],
|
||||
mutually_exclusive=[["id", "username"]],
|
||||
)
|
||||
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
user_id = module.params.get("id")
|
||||
username = module.params.get("username")
|
||||
actions = module.params.get("actions")
|
||||
client_id = module.params.get("client_id")
|
||||
redirect_uri = module.params.get("redirect_uri")
|
||||
lifespan = module.params.get("lifespan")
|
||||
|
||||
# Resolve user ID if only username is provided
|
||||
if user_id is None:
|
||||
user_obj = kc.get_user_by_username(username=username, realm=realm)
|
||||
if user_obj is None:
|
||||
module.fail_json(msg=f"User '{username}' not found in realm {realm}")
|
||||
user_id = user_obj["id"]
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(
|
||||
changed=True, msg=f"Would send execute-actions email to user {user_id}", user_id=user_id, actions=actions
|
||||
)
|
||||
|
||||
try:
|
||||
kc.send_execute_actions_email(
|
||||
user_id=user_id,
|
||||
realm=realm,
|
||||
client_id=client_id,
|
||||
data=actions,
|
||||
redirect_uri=redirect_uri,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
module.exit_json(
|
||||
changed=True, msg=f"Execute-actions email sent to user {user_id}", user_id=user_id, actions=actions
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,444 @@
|
|||
|
||||
# Copyright (c) 2022, Dušan Marković (@bratwurzt)
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_user_rolemapping
|
||||
|
||||
short_description: Allows administration of Keycloak user_rolemapping with the Keycloak API
|
||||
|
||||
# Originally added in community.general 5.7.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
description:
|
||||
- This module allows you to add, remove or modify Keycloak user_rolemapping with 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.
|
||||
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 names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/latest/rest-api/index.html).
|
||||
- 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
|
||||
suitable for the API.
|
||||
- When updating a user_rolemapping, where possible provide the role ID to the module. This removes a lookup to the API to
|
||||
translate the name into the role ID.
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the user_rolemapping.
|
||||
- On V(present), the user_rolemapping is created if it does not yet exist, or updated with the parameters you provide.
|
||||
- On V(absent), the user_rolemapping is removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
realm:
|
||||
type: str
|
||||
description:
|
||||
- They Keycloak realm under which this role_representation resides.
|
||||
default: 'master'
|
||||
|
||||
target_username:
|
||||
type: str
|
||||
description:
|
||||
- Username of the user roles are mapped to.
|
||||
- This parameter is not required (can be replaced by uid for less API call).
|
||||
uid:
|
||||
type: str
|
||||
description:
|
||||
- ID of the user to be mapped.
|
||||
- This parameter is not required for updating or deleting the rolemapping but providing it reduces the number of API
|
||||
calls required.
|
||||
service_account_user_client_id:
|
||||
type: str
|
||||
description:
|
||||
- Client ID of the service-account-user to be mapped.
|
||||
- This parameter is not required for updating or deleting the rolemapping but providing it reduces the number of API
|
||||
calls required.
|
||||
client_id:
|
||||
type: str
|
||||
description:
|
||||
- Name of the client (different than O(cid)) whose role is to be mapped.
|
||||
- This parameter is required if O(cid) is not provided (can be replaced by O(cid) to reduce the number of API calls
|
||||
that must be made).
|
||||
- If neither O(cid) nor O(client_id) is specified, a B(realm) role is mapped instead.
|
||||
cid:
|
||||
type: str
|
||||
description:
|
||||
- ID of the client whose role is to be mapped.
|
||||
- This parameter is not required for updating or deleting the rolemapping but providing it reduces the number of API
|
||||
calls required.
|
||||
- If neither O(cid) nor O(client_id) is specified, a B(realm) role is mapped instead.
|
||||
roles:
|
||||
description:
|
||||
- Roles to be mapped to the user.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
name:
|
||||
type: str
|
||||
description:
|
||||
- Name of the role representation.
|
||||
- This parameter is required only when creating or updating the role_representation.
|
||||
id:
|
||||
type: str
|
||||
description:
|
||||
- The unique identifier for this role_representation.
|
||||
- This parameter is not required for updating or deleting a role_representation but providing it reduces the number
|
||||
of API calls required.
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Dušan Marković (@bratwurzt)
|
||||
- Ivan Kokalović (@koke1997)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Map a realm role to a user, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_user_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
state: present
|
||||
uid: user_uid
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Map a client role to a user, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_user_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
state: present
|
||||
client_id: client1
|
||||
uid: user_uid
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Map a client role to a service account user for a client, authentication with credentials
|
||||
middleware_automation.keycloak.keycloak_user_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
state: present
|
||||
client_id: client1
|
||||
service_account_user_client_id: clientIdOfServiceAccount
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Map a client role to a user, authentication with token
|
||||
middleware_automation.keycloak.keycloak_user_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
token: TOKEN
|
||||
state: present
|
||||
client_id: client1
|
||||
target_username: user1
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Unmap client role from a user
|
||||
middleware_automation.keycloak.keycloak_user_rolemapping:
|
||||
realm: MyCustomRealm
|
||||
auth_client_id: admin-cli
|
||||
auth_keycloak_url: https://auth.example.com
|
||||
auth_realm: master
|
||||
auth_username: USERNAME
|
||||
auth_password: PASSWORD
|
||||
state: absent
|
||||
client_id: client1
|
||||
uid: 70e3ae72-96b6-11e6-9056-9737fd4d0764
|
||||
roles:
|
||||
- name: role_name1
|
||||
id: role_id1
|
||||
- name: role_name2
|
||||
id: role_id2
|
||||
delegate_to: localhost
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: Message as to what action was taken.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Role role1 assigned to user user1."
|
||||
|
||||
proposed:
|
||||
description: Representation of proposed client role mapping.
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {"clientId": "test"}
|
||||
|
||||
existing:
|
||||
description:
|
||||
- Representation of existing client role mapping.
|
||||
- The sample is truncated.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"adminUrl": "http://www.example.com/admin_url",
|
||||
"attributes": {
|
||||
"request.object.signature.alg": "RS256"
|
||||
}
|
||||
}
|
||||
|
||||
end_state:
|
||||
description:
|
||||
- Representation of client role mapping after module execution.
|
||||
- The sample is truncated.
|
||||
returned: on success
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"adminUrl": "http://www.example.com/admin_url",
|
||||
"attributes": {
|
||||
"request.object.signature.alg": "RS256"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Module execution
|
||||
|
||||
:return:
|
||||
"""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
roles_spec = dict(
|
||||
name=dict(type="str"),
|
||||
id=dict(type="str"),
|
||||
)
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(default="present", choices=["present", "absent"]),
|
||||
realm=dict(default="master"),
|
||||
uid=dict(type="str"),
|
||||
target_username=dict(type="str"),
|
||||
service_account_user_client_id=dict(type="str"),
|
||||
cid=dict(type="str"),
|
||||
client_id=dict(type="str"),
|
||||
roles=dict(type="list", elements="dict", options=roles_spec),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[
|
||||
["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"],
|
||||
["uid", "target_username", "service_account_user_client_id"],
|
||||
]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", diff={}, proposed={}, existing={}, end_state={})
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
realm = module.params.get("realm")
|
||||
state = module.params.get("state")
|
||||
cid = module.params.get("cid")
|
||||
client_id = module.params.get("client_id")
|
||||
uid = module.params.get("uid")
|
||||
target_username = module.params.get("target_username")
|
||||
service_account_user_client_id = module.params.get("service_account_user_client_id")
|
||||
roles = module.params.get("roles")
|
||||
|
||||
# Check the parameters
|
||||
if uid is None and target_username is None and service_account_user_client_id is None:
|
||||
module.fail_json(
|
||||
msg="Either the `target_username`, `uid` or `service_account_user_client_id` has to be specified."
|
||||
)
|
||||
|
||||
# Get the potential missing parameters
|
||||
if uid is None and service_account_user_client_id is None:
|
||||
user_rep = kc.get_user_by_username(username=target_username, realm=realm)
|
||||
if user_rep is not None:
|
||||
uid = user_rep.get("id")
|
||||
else:
|
||||
module.fail_json(msg=f"Could not fetch user for username {target_username}:")
|
||||
else:
|
||||
if uid is None and target_username is None:
|
||||
user_rep = kc.get_service_account_user_by_client_id(client_id=service_account_user_client_id, realm=realm)
|
||||
if user_rep is not None:
|
||||
uid = user_rep["id"]
|
||||
else:
|
||||
module.fail_json(msg=f"Could not fetch service-account-user for client_id {target_username}:")
|
||||
|
||||
if cid is None and client_id is not None:
|
||||
cid = kc.get_client_id(client_id=client_id, realm=realm)
|
||||
if cid is None:
|
||||
module.fail_json(msg=f"Could not fetch client {client_id}:")
|
||||
if roles is None:
|
||||
module.exit_json(msg="Nothing to do (no roles specified).")
|
||||
else:
|
||||
for role in roles:
|
||||
if role.get("name") is None and role.get("id") is None:
|
||||
module.fail_json(msg="Either the `name` or `id` has to be specified on each role.")
|
||||
# Fetch missing role_id
|
||||
if role.get("id") is None:
|
||||
if cid is None:
|
||||
role_id = kc.get_realm_role(name=role.get("name"), realm=realm)["id"]
|
||||
else:
|
||||
role_id = kc.get_client_role_id_by_name(cid=cid, name=role.get("name"), realm=realm)
|
||||
if role_id is not None:
|
||||
role["id"] = role_id
|
||||
else:
|
||||
module.fail_json(
|
||||
msg=f"Could not fetch role {role.get('name')} for client_id {client_id} or realm {realm}"
|
||||
)
|
||||
# Fetch missing role_name
|
||||
else:
|
||||
if cid is None:
|
||||
role_rep = kc.get_realm_user_rolemapping_by_id(uid=uid, rid=role.get("id"), realm=realm)
|
||||
if role_rep is not None:
|
||||
role["name"] = role_rep["name"]
|
||||
else:
|
||||
role_rep = kc.get_client_user_rolemapping_by_id(uid=uid, cid=cid, rid=role.get("id"), realm=realm)
|
||||
if role_rep is not None:
|
||||
role["name"] = role_rep["name"]
|
||||
if role.get("name") is None:
|
||||
module.fail_json(
|
||||
msg=f"Could not fetch role {role.get('id')} for client_id {client_id} or realm {realm}"
|
||||
)
|
||||
|
||||
# Get effective role mappings
|
||||
if cid is None:
|
||||
available_roles_before = kc.get_realm_user_available_rolemappings(uid=uid, realm=realm)
|
||||
assigned_roles_before = kc.get_realm_user_composite_rolemappings(uid=uid, realm=realm)
|
||||
else:
|
||||
available_roles_before = kc.get_client_user_available_rolemappings(uid=uid, cid=cid, realm=realm)
|
||||
assigned_roles_before = kc.get_client_user_composite_rolemappings(uid=uid, cid=cid, realm=realm)
|
||||
|
||||
result["existing"] = assigned_roles_before
|
||||
result["proposed"] = roles
|
||||
|
||||
update_roles = []
|
||||
for role in roles:
|
||||
# Fetch roles to assign if state present
|
||||
if state == "present":
|
||||
for available_role in available_roles_before:
|
||||
if role.get("name") == available_role.get("name"):
|
||||
update_roles.append(
|
||||
{
|
||||
"id": role.get("id"),
|
||||
"name": role.get("name"),
|
||||
}
|
||||
)
|
||||
# Fetch roles to remove if state absent
|
||||
else:
|
||||
for assigned_role in assigned_roles_before:
|
||||
if role.get("name") == assigned_role.get("name"):
|
||||
update_roles.append(
|
||||
{
|
||||
"id": role.get("id"),
|
||||
"name": role.get("name"),
|
||||
}
|
||||
)
|
||||
|
||||
if len(update_roles):
|
||||
if state == "present":
|
||||
# Assign roles
|
||||
result["changed"] = True
|
||||
if module._diff:
|
||||
result["diff"] = dict(before={"roles": assigned_roles_before}, after={"roles": update_roles})
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
kc.add_user_rolemapping(uid=uid, cid=cid, role_rep=update_roles, realm=realm)
|
||||
result["msg"] = f"Roles {update_roles} assigned to userId {uid}."
|
||||
if cid is None:
|
||||
assigned_roles_after = kc.get_realm_user_composite_rolemappings(uid=uid, realm=realm)
|
||||
else:
|
||||
assigned_roles_after = kc.get_client_user_composite_rolemappings(uid=uid, cid=cid, realm=realm)
|
||||
result["end_state"] = assigned_roles_after
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
# Remove mapping of role
|
||||
result["changed"] = True
|
||||
if module._diff:
|
||||
result["diff"] = dict(before={"roles": assigned_roles_before}, after={"roles": update_roles})
|
||||
if module.check_mode:
|
||||
module.exit_json(**result)
|
||||
kc.delete_user_rolemapping(uid=uid, cid=cid, role_rep=update_roles, realm=realm)
|
||||
result["msg"] = f"Roles {update_roles} removed from userId {uid}."
|
||||
if cid is None:
|
||||
assigned_roles_after = kc.get_realm_user_composite_rolemappings(uid=uid, realm=realm)
|
||||
else:
|
||||
assigned_roles_after = kc.get_client_user_composite_rolemappings(uid=uid, cid=cid, realm=realm)
|
||||
result["end_state"] = assigned_roles_after
|
||||
module.exit_json(**result)
|
||||
# Do nothing
|
||||
else:
|
||||
result["changed"] = False
|
||||
result["msg"] = f"Nothing to do, roles {roles} are correctly mapped to user for username {target_username}."
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,826 @@
|
|||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import annotations
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_userprofile
|
||||
|
||||
short_description: Allows managing Keycloak User Profiles
|
||||
|
||||
description:
|
||||
- This module allows you to create, update, or delete Keycloak User Profiles using the Keycloak API. You can also customize
|
||||
the "Unmanaged Attributes" with it.
|
||||
- The names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation
|
||||
at U(https://www.keycloak.org/docs-api/24.0.5/rest-api/index.html). For compatibility reasons, the module also accepts
|
||||
the camelCase versions of the options.
|
||||
# Originally added in community.general 9.4.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
action_group:
|
||||
# Originally added in community.general 10.2.0
|
||||
version_added: "3.0.0"
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- State of the User Profile provider.
|
||||
- On V(present), the User Profile provider is created if it does not yet exist, or updated with the parameters you provide.
|
||||
- On V(absent), the User Profile provider is removed if it exists.
|
||||
default: 'present'
|
||||
type: str
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
|
||||
parent_id:
|
||||
description:
|
||||
- The parent ID of the realm key. In practice the ID (name) of the realm.
|
||||
aliases:
|
||||
- parentId
|
||||
- realm
|
||||
type: str
|
||||
required: true
|
||||
|
||||
provider_id:
|
||||
description:
|
||||
- The name of the provider ID for the key (supported value is V(declarative-user-profile)).
|
||||
aliases:
|
||||
- providerId
|
||||
choices: ['declarative-user-profile']
|
||||
default: 'declarative-user-profile'
|
||||
type: str
|
||||
|
||||
provider_type:
|
||||
description:
|
||||
- Component type for User Profile (only supported value is V(org.keycloak.userprofile.UserProfileProvider)).
|
||||
aliases:
|
||||
- providerType
|
||||
choices: ['org.keycloak.userprofile.UserProfileProvider']
|
||||
default: org.keycloak.userprofile.UserProfileProvider
|
||||
type: str
|
||||
|
||||
config:
|
||||
description:
|
||||
- The configuration of the User Profile Provider.
|
||||
type: dict
|
||||
suboptions:
|
||||
kc_user_profile_config:
|
||||
description:
|
||||
- Define a declarative User Profile. See EXAMPLES for more context.
|
||||
aliases:
|
||||
- kcUserProfileConfig
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
attributes:
|
||||
description:
|
||||
- A list of attributes to be included in the User Profile.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- The name of the attribute.
|
||||
type: str
|
||||
required: true
|
||||
|
||||
display_name:
|
||||
description:
|
||||
- The display name of the attribute.
|
||||
aliases:
|
||||
- displayName
|
||||
type: str
|
||||
required: true
|
||||
|
||||
validations:
|
||||
description:
|
||||
- The validations to be applied to the attribute.
|
||||
type: dict
|
||||
suboptions:
|
||||
length:
|
||||
description:
|
||||
- The length validation for the attribute.
|
||||
type: dict
|
||||
suboptions:
|
||||
min:
|
||||
description:
|
||||
- The minimum length of the attribute.
|
||||
type: int
|
||||
max:
|
||||
description:
|
||||
- The maximum length of the attribute.
|
||||
type: int
|
||||
required: true
|
||||
|
||||
email:
|
||||
description:
|
||||
- The email validation for the attribute.
|
||||
type: dict
|
||||
|
||||
username_prohibited_characters:
|
||||
description:
|
||||
- The prohibited characters validation for the username attribute.
|
||||
type: dict
|
||||
aliases:
|
||||
- usernameProhibitedCharacters
|
||||
|
||||
up_username_not_idn_homograph:
|
||||
description:
|
||||
- The validation to prevent IDN homograph attacks in usernames.
|
||||
type: dict
|
||||
aliases:
|
||||
- upUsernameNotIdnHomograph
|
||||
|
||||
person_name_prohibited_characters:
|
||||
description:
|
||||
- The prohibited characters validation for person name attributes.
|
||||
type: dict
|
||||
aliases:
|
||||
- personNameProhibitedCharacters
|
||||
|
||||
uri:
|
||||
description:
|
||||
- The URI validation for the attribute.
|
||||
type: dict
|
||||
|
||||
pattern:
|
||||
description:
|
||||
- The pattern validation for the attribute using regular expressions.
|
||||
type: dict
|
||||
|
||||
options:
|
||||
description:
|
||||
- Validation to ensure the attribute matches one of the provided options.
|
||||
type: dict
|
||||
|
||||
integer:
|
||||
description:
|
||||
- The integer validation for the attribute.
|
||||
type: dict
|
||||
|
||||
double:
|
||||
description:
|
||||
- The double validation for the attribute.
|
||||
type: dict
|
||||
|
||||
iso_date:
|
||||
description:
|
||||
- The iso-date validation for the attribute.
|
||||
type: dict
|
||||
aliases:
|
||||
- isoDate
|
||||
|
||||
local_date:
|
||||
description:
|
||||
- The local-date validation for the attribute.
|
||||
type: dict
|
||||
aliases:
|
||||
- localDate
|
||||
|
||||
multivalued:
|
||||
description:
|
||||
- The multivalued validation for the attribute.
|
||||
type: dict
|
||||
suboptions:
|
||||
min:
|
||||
description:
|
||||
- The minimum amount of values of the attribute.
|
||||
type: int
|
||||
max:
|
||||
description:
|
||||
- The maximum amount of values of the attribute.
|
||||
type: int
|
||||
required: true
|
||||
|
||||
annotations:
|
||||
description:
|
||||
- Annotations for the attribute.
|
||||
type: dict
|
||||
|
||||
group:
|
||||
description:
|
||||
- Specifies the User Profile group where this attribute is added.
|
||||
type: str
|
||||
|
||||
permissions:
|
||||
description:
|
||||
- The permissions for viewing and editing the attribute.
|
||||
type: dict
|
||||
suboptions:
|
||||
view:
|
||||
description:
|
||||
- The roles that can view the attribute.
|
||||
- Supported values are V(admin) and V(user).
|
||||
type: list
|
||||
elements: str
|
||||
default:
|
||||
- admin
|
||||
- user
|
||||
|
||||
edit:
|
||||
description:
|
||||
- The roles that can edit the attribute.
|
||||
- Supported values are V(admin) and V(user).
|
||||
type: list
|
||||
elements: str
|
||||
default:
|
||||
- admin
|
||||
- user
|
||||
|
||||
multivalued:
|
||||
description:
|
||||
- Whether the attribute can have multiple values.
|
||||
type: bool
|
||||
default: false
|
||||
|
||||
required:
|
||||
description:
|
||||
- The roles that require this attribute.
|
||||
type: dict
|
||||
suboptions:
|
||||
roles:
|
||||
description:
|
||||
- The roles for which this attribute is required.
|
||||
- Supported values are V(admin) and V(user).
|
||||
type: list
|
||||
elements: str
|
||||
default:
|
||||
- user
|
||||
|
||||
selector:
|
||||
description:
|
||||
- Selector when the attribute should be added.
|
||||
type: dict
|
||||
suboptions:
|
||||
scopes:
|
||||
description:
|
||||
- Scopes to which the attribute should be added.
|
||||
type: list
|
||||
elements: str
|
||||
|
||||
groups:
|
||||
description:
|
||||
- A list of attribute groups to be included in the User Profile.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- The name of the group.
|
||||
type: str
|
||||
required: true
|
||||
|
||||
display_header:
|
||||
description:
|
||||
- The display header for the group.
|
||||
aliases:
|
||||
- displayHeader
|
||||
type: str
|
||||
required: true
|
||||
|
||||
display_description:
|
||||
description:
|
||||
- The display description for the group.
|
||||
aliases:
|
||||
- displayDescription
|
||||
type: str
|
||||
|
||||
annotations:
|
||||
description:
|
||||
- The annotations included in the group.
|
||||
type: dict
|
||||
|
||||
unmanaged_attribute_policy:
|
||||
description:
|
||||
- Policy for unmanaged attributes.
|
||||
aliases:
|
||||
- unmanagedAttributePolicy
|
||||
type: str
|
||||
choices:
|
||||
- ENABLED
|
||||
- ADMIN_EDIT
|
||||
- ADMIN_VIEW
|
||||
|
||||
notes:
|
||||
- Currently, only a single V(declarative-user-profile) entry is supported for O(provider_id) (design of the Keyckoak API).
|
||||
However, there can be multiple O(config.kc_user_profile_config[].attributes[]) entries.
|
||||
extends_documentation_fragment:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
- middleware_automation.keycloak.actiongroup_keycloak
|
||||
- middleware_automation.keycloak.attributes
|
||||
|
||||
author:
|
||||
- Eike Waldt (@yeoldegrove)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create a Declarative User Profile with default settings
|
||||
middleware_automation.keycloak.keycloak_userprofile:
|
||||
state: present
|
||||
parent_id: master
|
||||
config:
|
||||
kc_user_profile_config:
|
||||
- attributes:
|
||||
- name: username
|
||||
displayName: ${username}
|
||||
validations:
|
||||
length:
|
||||
min: 3
|
||||
max: 255
|
||||
username_prohibited_characters: {}
|
||||
up_username_not_idn_homograph: {}
|
||||
annotations: {}
|
||||
permissions:
|
||||
view:
|
||||
- admin
|
||||
- user
|
||||
edit: []
|
||||
multivalued: false
|
||||
- name: email
|
||||
displayName: ${email}
|
||||
validations:
|
||||
email: {}
|
||||
length:
|
||||
max: 255
|
||||
annotations: {}
|
||||
required:
|
||||
roles:
|
||||
- user
|
||||
permissions:
|
||||
view:
|
||||
- admin
|
||||
- user
|
||||
edit: []
|
||||
multivalued: false
|
||||
- name: firstName
|
||||
displayName: ${firstName}
|
||||
validations:
|
||||
length:
|
||||
max: 255
|
||||
person_name_prohibited_characters: {}
|
||||
annotations: {}
|
||||
required:
|
||||
roles:
|
||||
- user
|
||||
permissions:
|
||||
view:
|
||||
- admin
|
||||
- user
|
||||
edit: []
|
||||
multivalued: false
|
||||
- name: lastName
|
||||
displayName: ${lastName}
|
||||
validations:
|
||||
length:
|
||||
max: 255
|
||||
person_name_prohibited_characters: {}
|
||||
annotations: {}
|
||||
required:
|
||||
roles:
|
||||
- user
|
||||
permissions:
|
||||
view:
|
||||
- admin
|
||||
- user
|
||||
edit: []
|
||||
multivalued: false
|
||||
- name: testAttribute
|
||||
displayName: ${testAttribute}
|
||||
validations:
|
||||
integer:
|
||||
min: 0
|
||||
max: 255
|
||||
annotations: {}
|
||||
required:
|
||||
roles:
|
||||
- user
|
||||
permissions:
|
||||
view:
|
||||
- admin
|
||||
- user
|
||||
edit: []
|
||||
multivalued: false
|
||||
groups:
|
||||
- name: user-metadata
|
||||
displayHeader: User metadata
|
||||
displayDescription: Attributes, which refer to user metadata
|
||||
annotations: {}
|
||||
|
||||
- name: Delete a Keycloak User Profile Provider
|
||||
keycloak_userprofile:
|
||||
state: absent
|
||||
parent_id: master
|
||||
|
||||
# Unmanaged attributes are user attributes not explicitly defined in the User Profile
|
||||
# configuration. By default, unmanaged attributes are "Disabled" and are not
|
||||
# available from any context such as registration, account, and the
|
||||
# administration console. By setting "Enabled", unmanaged attributes are fully
|
||||
# recognized by the server and accessible through all contexts, useful if you are
|
||||
# starting migrating an existing realm to the declarative User Profile
|
||||
# and you don't have yet all user attributes defined in the User Profile configuration.
|
||||
- name: Enable Unmanaged Attributes
|
||||
middleware_automation.keycloak.keycloak_userprofile:
|
||||
state: present
|
||||
parent_id: master
|
||||
config:
|
||||
kc_user_profile_config:
|
||||
- unmanagedAttributePolicy: ENABLED
|
||||
|
||||
# By setting "Only administrators can write", unmanaged attributes can be managed
|
||||
# only through the administration console and API, useful if you have already
|
||||
# defined any custom attribute that can be managed by users but you are unsure
|
||||
# about adding other attributes that should only be managed by administrators.
|
||||
- name: Enable ADMIN_EDIT on Unmanaged Attributes
|
||||
middleware_automation.keycloak.keycloak_userprofile:
|
||||
state: present
|
||||
parent_id: master
|
||||
config:
|
||||
kc_user_profile_config:
|
||||
- unmanagedAttributePolicy: ADMIN_EDIT
|
||||
|
||||
# By setting `Only administrators can view`, unmanaged attributes are read-only
|
||||
# and only available through the administration console and API.
|
||||
- name: Enable ADMIN_VIEW on Unmanaged Attributes
|
||||
middleware_automation.keycloak.keycloak_userprofile:
|
||||
state: present
|
||||
parent_id: master
|
||||
config:
|
||||
kc_user_profile_config:
|
||||
- unmanagedAttributePolicy: ADMIN_VIEW
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
msg:
|
||||
description: The output message generated by the module.
|
||||
returned: always
|
||||
type: str
|
||||
sample: UserProfileProvider created successfully
|
||||
data:
|
||||
description: The data returned by the Keycloak API.
|
||||
returned: when state is present
|
||||
type: dict
|
||||
"""
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
camel,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def remove_null_values(data):
|
||||
if isinstance(data, dict):
|
||||
# Recursively remove null values from dictionaries
|
||||
return {k: remove_null_values(v) for k, v in data.items() if v is not None}
|
||||
elif isinstance(data, list):
|
||||
# Recursively remove null values from lists
|
||||
return [remove_null_values(item) for item in data if item is not None]
|
||||
else:
|
||||
# Return the data if it is neither a dictionary nor a list
|
||||
return data
|
||||
|
||||
|
||||
def camel_recursive(data):
|
||||
if isinstance(data, dict):
|
||||
# Convert keys to camelCase and apply recursively
|
||||
return {camel(k): camel_recursive(v) for k, v in data.items()}
|
||||
elif isinstance(data, list):
|
||||
# Apply camelCase conversion to each item in the list
|
||||
return [camel_recursive(item) for item in data]
|
||||
else:
|
||||
# Return the data as-is if it is not a dict or list
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
state=dict(type="str", choices=["present", "absent"], default="present"),
|
||||
parent_id=dict(type="str", aliases=["parentId", "realm"], required=True),
|
||||
provider_id=dict(
|
||||
type="str", aliases=["providerId"], default="declarative-user-profile", choices=["declarative-user-profile"]
|
||||
),
|
||||
provider_type=dict(
|
||||
type="str",
|
||||
aliases=["providerType"],
|
||||
default="org.keycloak.userprofile.UserProfileProvider",
|
||||
choices=["org.keycloak.userprofile.UserProfileProvider"],
|
||||
),
|
||||
config=dict(
|
||||
type="dict",
|
||||
options={
|
||||
"kc_user_profile_config": dict(
|
||||
type="list",
|
||||
aliases=["kcUserProfileConfig"],
|
||||
elements="dict",
|
||||
options={
|
||||
"attributes": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options={
|
||||
"name": dict(type="str", required=True),
|
||||
"display_name": dict(type="str", aliases=["displayName"], required=True),
|
||||
"validations": dict(
|
||||
type="dict",
|
||||
options={
|
||||
"length": dict(
|
||||
type="dict",
|
||||
options={"min": dict(type="int"), "max": dict(type="int", required=True)},
|
||||
),
|
||||
"email": dict(type="dict"),
|
||||
"username_prohibited_characters": dict(
|
||||
type="dict", aliases=["usernameProhibitedCharacters"]
|
||||
),
|
||||
"up_username_not_idn_homograph": dict(
|
||||
type="dict", aliases=["upUsernameNotIdnHomograph"]
|
||||
),
|
||||
"person_name_prohibited_characters": dict(
|
||||
type="dict", aliases=["personNameProhibitedCharacters"]
|
||||
),
|
||||
"uri": dict(type="dict"),
|
||||
"pattern": dict(type="dict"),
|
||||
"options": dict(type="dict"),
|
||||
"integer": dict(type="dict"),
|
||||
"double": dict(type="dict"),
|
||||
"iso_date": dict(type="dict", aliases=["isoDate"]),
|
||||
"local_date": dict(type="dict", aliases=["localDate"]),
|
||||
"multivalued": dict(
|
||||
type="dict",
|
||||
options={
|
||||
"min": dict(type="int", required=False),
|
||||
"max": dict(type="int", required=True),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
"annotations": dict(type="dict"),
|
||||
"group": dict(type="str"),
|
||||
"permissions": dict(
|
||||
type="dict",
|
||||
options={
|
||||
"view": dict(type="list", elements="str", default=["admin", "user"]),
|
||||
"edit": dict(type="list", elements="str", default=["admin", "user"]),
|
||||
},
|
||||
),
|
||||
"multivalued": dict(type="bool", default=False),
|
||||
"required": dict(
|
||||
type="dict", options={"roles": dict(type="list", elements="str", default=["user"])}
|
||||
),
|
||||
"selector": dict(type="dict", options={"scopes": dict(type="list", elements="str")}),
|
||||
},
|
||||
),
|
||||
"groups": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options={
|
||||
"name": dict(type="str", required=True),
|
||||
"display_header": dict(type="str", aliases=["displayHeader"], required=True),
|
||||
"display_description": dict(type="str", aliases=["displayDescription"]),
|
||||
"annotations": dict(type="dict"),
|
||||
},
|
||||
),
|
||||
"unmanaged_attribute_policy": dict(
|
||||
type="str",
|
||||
aliases=["unmanagedAttributePolicy"],
|
||||
choices=["ENABLED", "ADMIN_EDIT", "ADMIN_VIEW"],
|
||||
),
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
argument_spec.update(meta_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_one_of=(
|
||||
[["token", "auth_realm", "auth_username", "auth_password", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
# Initialize the result object. Only "changed" seems to have special
|
||||
# meaning for Ansible.
|
||||
result = dict(changed=False, msg="", end_state={}, diff=dict(before={}, after={}))
|
||||
|
||||
# This will include the current state of the realm userprofile if it is already
|
||||
# present. This is only used for diff-mode.
|
||||
before_realm_userprofile = {}
|
||||
before_realm_userprofile["config"] = {}
|
||||
|
||||
# Obtain access token, initialize API
|
||||
try:
|
||||
connection_header = get_token(module.params)
|
||||
except KeycloakError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
kc = KeycloakAPI(module, connection_header)
|
||||
|
||||
params_to_ignore = list(keycloak_argument_spec().keys()) + ["state"]
|
||||
|
||||
# Filter and map the parameters names that apply to the role
|
||||
component_params = [x for x in module.params if x not in params_to_ignore and module.params.get(x) is not None]
|
||||
|
||||
# Build a proposed changeset from parameters given to this module
|
||||
changeset = {}
|
||||
|
||||
# Build the changeset with proper JSON serialization for kc_user_profile_config
|
||||
config = module.params.get("config")
|
||||
changeset["config"] = {}
|
||||
|
||||
# Generate a JSON payload for Keycloak Admin API from the module
|
||||
# parameters. Parameters that do not belong to the JSON payload (e.g.
|
||||
# "state" or "auth_keycloal_url") have been filtered away earlier (see
|
||||
# above).
|
||||
#
|
||||
# This loop converts Ansible module parameters (snake-case) into
|
||||
# Keycloak-compatible format (camel-case). For example proider_id
|
||||
# becomes providerId. It also handles some special cases, e.g. aliases.
|
||||
for component_param in component_params:
|
||||
# realm/parent_id parameter
|
||||
if component_param == "realm" or component_param == "parent_id":
|
||||
changeset["parent_id"] = module.params.get(component_param)
|
||||
changeset.pop(component_param, None)
|
||||
# complex parameters in config suboptions
|
||||
elif component_param == "config":
|
||||
for config_param in config:
|
||||
# special parameter kc_user_profile_config
|
||||
if config_param in ("kcUserProfileConfig", "kc_user_profile_config"):
|
||||
config_param_org = config_param
|
||||
# rename parameter to be accepted by Keycloak API
|
||||
config_param = "kc.user.profile.config"
|
||||
# make sure no null values are passed to Keycloak API
|
||||
kc_user_profile_config = remove_null_values(config[config_param_org])
|
||||
changeset[camel(component_param)][config_param] = []
|
||||
if len(kc_user_profile_config) > 0:
|
||||
# convert aliases to camelCase
|
||||
kc_user_profile_config = camel_recursive(kc_user_profile_config)
|
||||
# rename validations to be accepted by Keycloak API
|
||||
if "attributes" in kc_user_profile_config[0]:
|
||||
for attribute in kc_user_profile_config[0]["attributes"]:
|
||||
if "validations" in attribute:
|
||||
if "usernameProhibitedCharacters" in attribute["validations"]:
|
||||
attribute["validations"]["username-prohibited-characters"] = attribute[
|
||||
"validations"
|
||||
].pop("usernameProhibitedCharacters")
|
||||
if "upUsernameNotIdnHomograph" in attribute["validations"]:
|
||||
attribute["validations"]["up-username-not-idn-homograph"] = attribute[
|
||||
"validations"
|
||||
].pop("upUsernameNotIdnHomograph")
|
||||
if "personNameProhibitedCharacters" in attribute["validations"]:
|
||||
attribute["validations"]["person-name-prohibited-characters"] = attribute[
|
||||
"validations"
|
||||
].pop("personNameProhibitedCharacters")
|
||||
if "isoDate" in attribute["validations"]:
|
||||
attribute["validations"]["iso-date"] = attribute["validations"].pop("isoDate")
|
||||
if "localDate" in attribute["validations"]:
|
||||
attribute["validations"]["local-date"] = attribute["validations"].pop(
|
||||
"localDate"
|
||||
)
|
||||
changeset[camel(component_param)][config_param].append(kc_user_profile_config[0])
|
||||
# usual camelCase parameters
|
||||
else:
|
||||
changeset[camel(component_param)][camel(config_param)] = []
|
||||
raw_value = module.params.get(component_param)[config_param]
|
||||
if isinstance(raw_value, bool):
|
||||
value = str(raw_value).lower()
|
||||
else:
|
||||
value = raw_value # Directly use the raw value
|
||||
changeset[camel(component_param)][camel(config_param)].append(value)
|
||||
# usual parameters
|
||||
else:
|
||||
new_param_value = module.params.get(component_param)
|
||||
changeset[camel(component_param)] = new_param_value
|
||||
|
||||
# Make it easier to refer to current module parameters
|
||||
state = module.params.get("state")
|
||||
parent_id = module.params.get("parent_id")
|
||||
provider_type = module.params.get("provider_type")
|
||||
provider_id = module.params.get("provider_id")
|
||||
|
||||
# Make a deep copy of the changeset. This is use when determining
|
||||
# changes to the current state.
|
||||
changeset_copy = deepcopy(changeset)
|
||||
|
||||
# Get a list of all Keycloak components that are of userprofile provider type.
|
||||
realm_userprofiles = kc.get_components(urlencode(dict(type=provider_type)), parent_id)
|
||||
|
||||
# If this component is present get its userprofile ID. Confusingly the userprofile ID is
|
||||
# also known as the Provider ID.
|
||||
userprofile_id = None
|
||||
|
||||
# Track individual parameter changes
|
||||
changes = ""
|
||||
|
||||
# This tells Ansible whether the userprofile was changed (added, removed, modified)
|
||||
result["changed"] = False
|
||||
|
||||
# Loop through the list of components. If we encounter a component whose
|
||||
# name matches the value of the name parameter then assume the userprofile is
|
||||
# already present.
|
||||
for userprofile in realm_userprofiles:
|
||||
if provider_id == "declarative-user-profile":
|
||||
userprofile_id = userprofile["id"]
|
||||
changeset["id"] = userprofile_id
|
||||
changeset_copy["id"] = userprofile_id
|
||||
|
||||
# keycloak returns kc.user.profile.config as a single JSON formatted string, so we have to deserialize it
|
||||
if "config" in userprofile and "kc.user.profile.config" in userprofile["config"]:
|
||||
userprofile["config"]["kc.user.profile.config"][0] = json.loads(
|
||||
userprofile["config"]["kc.user.profile.config"][0]
|
||||
)
|
||||
|
||||
# Compare top-level parameters
|
||||
for param in changeset:
|
||||
before_realm_userprofile[param] = userprofile[param]
|
||||
|
||||
if changeset_copy[param] != userprofile[param] and param != "config":
|
||||
changes += f"{param}: {userprofile[param]} -> {changeset_copy[param]}, "
|
||||
result["changed"] = True
|
||||
|
||||
# Compare parameters under the "config" userprofile
|
||||
for p, v in changeset_copy["config"].items():
|
||||
before_realm_userprofile["config"][p] = userprofile["config"][p]
|
||||
if v != userprofile["config"][p]:
|
||||
changes += f"config.{p}: {userprofile['config'][p]} -> {v}, "
|
||||
result["changed"] = True
|
||||
|
||||
# Check all the possible states of the resource and do what is needed to
|
||||
# converge current state with desired state (create, update or delete
|
||||
# the userprofile).
|
||||
|
||||
# keycloak expects kc.user.profile.config as a single JSON formatted string, so we have to serialize it
|
||||
if "config" in changeset and "kc.user.profile.config" in changeset["config"]:
|
||||
changeset["config"]["kc.user.profile.config"][0] = json.dumps(changeset["config"]["kc.user.profile.config"][0])
|
||||
if userprofile_id and state == "present":
|
||||
if result["changed"]:
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_realm_userprofile, after=changeset_copy)
|
||||
|
||||
if module.check_mode:
|
||||
result["msg"] = f"Userprofile {provider_id} would be changed: {changes.strip(', ')}"
|
||||
else:
|
||||
kc.update_component(changeset, parent_id)
|
||||
result["msg"] = f"Userprofile {provider_id} changed: {changes.strip(', ')}"
|
||||
else:
|
||||
result["msg"] = f"Userprofile {provider_id} was in sync"
|
||||
|
||||
result["end_state"] = changeset_copy
|
||||
elif userprofile_id and state == "absent":
|
||||
if module._diff:
|
||||
result["diff"] = dict(before=before_realm_userprofile, after={})
|
||||
|
||||
if module.check_mode:
|
||||
result["changed"] = True
|
||||
result["msg"] = f"Userprofile {provider_id} would be deleted"
|
||||
else:
|
||||
kc.delete_component(userprofile_id, parent_id)
|
||||
result["changed"] = True
|
||||
result["msg"] = f"Userprofile {provider_id} deleted"
|
||||
|
||||
result["end_state"] = {}
|
||||
elif not userprofile_id and state == "present":
|
||||
if module._diff:
|
||||
result["diff"] = dict(before={}, after=changeset_copy)
|
||||
|
||||
if module.check_mode:
|
||||
result["changed"] = True
|
||||
result["msg"] = f"Userprofile {provider_id} would be created"
|
||||
else:
|
||||
kc.create_component(changeset, parent_id)
|
||||
result["changed"] = True
|
||||
result["msg"] = f"Userprofile {provider_id} created"
|
||||
|
||||
result["end_state"] = changeset_copy
|
||||
elif not userprofile_id and state == "absent":
|
||||
result["changed"] = False
|
||||
result["msg"] = f"Userprofile {provider_id} not present"
|
||||
result["end_state"] = {}
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#################################################
|
||||
# python dependencies required to be installed
|
||||
# on the controller host with:
|
||||
# pip install -r requirements.txt
|
||||
#
|
||||
netaddr
|
||||
lxml # for middleware_automation.common.maven_artifact
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
collections:
|
||||
- name: middleware_automation.common
|
||||
version: ">=1.2.4"
|
||||
- name: middleware_automation.infinispan
|
||||
- name: community.general
|
||||
- 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"
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
keycloak
|
||||
========
|
||||
|
||||
Install [keycloak](https://keycloak.org/) or [Red Hat Single Sign-On](https://access.redhat.com/products/red-hat-single-sign-on) server configurations.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
This role requires the `python3-netaddr` library installed on the controller node.
|
||||
|
||||
* to install via yum/dnf: `dnf install python3-netaddr`
|
||||
* to install via apt: `apt install python3-netaddr`
|
||||
* or via pip: `pip install netaddr==0.8.0`
|
||||
* or via the collection: `pip install -r requirements.txt`
|
||||
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
The roles depends on:
|
||||
|
||||
* [middleware_automation.common](https://github.com/ansible-middleware/common)
|
||||
* [ansible-posix](https://docs.ansible.com/ansible/latest/collections/ansible/posix/index.html)
|
||||
|
||||
To install all the dependencies via galaxy:
|
||||
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
|
||||
|
||||
Versions
|
||||
--------
|
||||
|
||||
| RH-SSO VERSION | Release Date | Keycloak Version | EAP Version | Notes |
|
||||
|:---------------|:------------------|:-----------------|:------------|:----------------|
|
||||
|`7.5.0 GA` |September 20, 2021 |`15.0.2` | `7.4.6` |[Release Notes](https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/7.5/html/release_notes/index)|
|
||||
|`7.6.0 GA` |June 30, 2022 |`18.0.3` | `7.4.6` |[Release Notes](https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/7.6/html-single/release_notes/index)|
|
||||
|
||||
|
||||
Patching
|
||||
--------
|
||||
|
||||
When variable `keycloak_rhsso_apply_patches` is `true` (default: `false`), the role will automatically apply the latest cumulative patch for the selected base version.
|
||||
|
||||
| RH-SSO VERSION | Release Date | RH-SSO LATEST CP | Notes |
|
||||
|:---------------|:------------------|:-----------------|:----------------|
|
||||
|`7.5.0 GA` |January 20, 2022 |`7.5.3 GA` |[Release Notes](https://access.redhat.com/articles/6646321)|
|
||||
|`7.6.0 GA` |November 11, 2022 |`7.6.1 GA` |[Release Notes](https://access.redhat.com/articles/6982711)|
|
||||
|
||||
|
||||
Role Defaults
|
||||
-------------
|
||||
|
||||
* Service configuration
|
||||
|
||||
| Variable | Description | Default |
|
||||
|:---------|:------------|:---------|
|
||||
|`keycloak_ha_enabled`| Enable auto configuration for database backend, clustering and remote caches on infinispan | `False` |
|
||||
|`keycloak_ha_discovery`| Discovery protocol for HA cluster members | `JDBC_PING` if `keycloak_db_enabled` else `TCPPING` |
|
||||
|`keycloak_db_enabled`| Enable auto configuration for database backend | `True` if `keycloak_ha_enabled` is True, else `False` |
|
||||
|`keycloak_remote_cache_enabled`| Enable remote cache store when in clustered ha configurations | `True` if `keycloak_ha_enabled` else `False` |
|
||||
|`keycloak_admin_user`| Administration console user account | `admin` |
|
||||
|`keycloak_bind_address`| Address for binding service ports | `0.0.0.0` |
|
||||
|`keycloak_management_port_bind_address`| Address for binding management ports | `127.0.0.1` |
|
||||
|`keycloak_host`| hostname | `localhost` |
|
||||
|`keycloak_http_port`| HTTP port | `8080` |
|
||||
|`keycloak_https_port`| TLS HTTP port | `8443` |
|
||||
|`keycloak_ajp_port`| AJP port | `8009` |
|
||||
|`keycloak_jgroups_port`| jgroups cluster tcp port | `7600` |
|
||||
|`keycloak_management_http_port`| Management port | `9990` |
|
||||
|`keycloak_management_https_port`| TLS management port | `9993` |
|
||||
|`keycloak_prefer_ipv4`| Prefer IPv4 stack and addresses for port binding | `true` |
|
||||
|`keycloak_config_standalone_xml`| filename for configuration | `keycloak.xml` |
|
||||
|`keycloak_service_user`| posix account username | `keycloak` |
|
||||
|`keycloak_service_group`| posix account group | `keycloak` |
|
||||
|`keycloak_service_restart_always`| systemd restart always behavior activation | `False` |
|
||||
|`keycloak_service_restart_on_failure`| systemd restart on-failure behavior activation | `False` |
|
||||
|`keycloak_service_startlimitintervalsec`| systemd StartLimitIntervalSec | `300` |
|
||||
|`keycloak_service_startlimitburst`| systemd StartLimitBurst | `5` |
|
||||
|`keycloak_service_restartsec`| systemd RestartSec | `10s` |
|
||||
|`keycloak_service_pidfile`| pid file path for service | `/run/keycloak/keycloak.pid` |
|
||||
|`keycloak_features` | List of `name`/`status` pairs of features (also known as profiles on RH-SSO) to `enable` or `disable`, example: `[ { name: 'docker', status: 'enabled' } ]` | `[]`
|
||||
|`keycloak_jvm_package`| RHEL java package runtime | `java-1.8.0-openjdk-headless` |
|
||||
|`keycloak_java_home`| `JAVA_HOME` of installed JRE, leave empty for using RPM path at `keycloak_jvm_package` | `None` |
|
||||
|`keycloak_java_opts`| Additional JVM options | `-Xms1024m -Xmx2048m` |
|
||||
|
||||
|
||||
* Install options
|
||||
|
||||
| Variable | Description | Default |
|
||||
|:---------|:------------|:---------|
|
||||
|`keycloak_offline_install` | perform an offline install | `false`|
|
||||
|`keycloak_download_url`| Download URL for keycloak | `https://github.com/keycloak/keycloak/releases/download/<version>/<archive>`|
|
||||
|`keycloak_version`| keycloak.org package version | `18.0.2` |
|
||||
|`keycloak_dest`| Installation root path | `/opt/keycloak` |
|
||||
|`keycloak_download_url` | Download URL for keycloak | `https://github.com/keycloak/keycloak/releases/download/{{ keycloak_version }}/{{ keycloak_archive }}` |
|
||||
|`keycloak_configure_firewalld` | Ensure firewalld is running and configure keycloak ports | `false` |
|
||||
|
||||
|
||||
* Miscellaneous configuration
|
||||
|
||||
| Variable | Description | Default |
|
||||
|:---------|:------------|:--------|
|
||||
|`keycloak_archive` | keycloak install archive filename | `keycloak-legacy-{{ keycloak_version }}.zip` |
|
||||
|`keycloak_download_url_9x` | Download URL for keycloak (deprecated) | `https://downloads.jboss.org/keycloak/{{ keycloak_version }}/{{ keycloak_archive }}` |
|
||||
|`keycloak_installdir` | Installation path | `{{ keycloak_dest }}/keycloak-{{ keycloak_version }}` |
|
||||
|`keycloak_jboss_home` | Installation work directory | `{{ keycloak_rhsso_installdir }}` |
|
||||
|`keycloak_jboss_port_offset` | Port offset for the JBoss socket binding | `0` |
|
||||
|`keycloak_config_dir` | Path for configuration | `{{ keycloak_jboss_home }}/standalone/configuration` |
|
||||
|`keycloak_config_path_to_standalone_xml` | Custom path for configuration | `{{ keycloak_jboss_home }}/standalone/configuration/{{ keycloak_config_standalone_xml }}` |
|
||||
|`keycloak_config_override_template` | Path to custom template for standalone.xml configuration | `''` |
|
||||
|`keycloak_auth_realm` | Name for rest authentication realm | `master` |
|
||||
|`keycloak_auth_client` | Authentication client for configuration REST calls | `admin-cli` |
|
||||
|`keycloak_force_install` | Remove pre-existing versions of service | `false` |
|
||||
|`keycloak_url` | URL for configuration rest calls | `http://{{ keycloak_host }}:{{ keycloak_http_port + keycloak_jboss_port_offset }}` |
|
||||
|`keycloak_management_url` | URL for management console rest calls | `http://{{ keycloak_host }}:{{ keycloak_management_http_port + keycloak_jboss_port_offset }}` |
|
||||
|`keycloak_frontend_url_force` | Force backend requests to use the frontend URL | `false` |
|
||||
|`keycloak_db_background_validation` | Enable background validation of database connection | `false` |
|
||||
|`keycloak_db_background_validation_millis`| How frequenly the connection pool is validated in the background | `10000` if background validation enabled |
|
||||
|`keycloak_db_background_validate_on_match` | Enable validate on match for database connections | `false` |
|
||||
|`keycloak_frontend_url` | frontend URL for keycloak endpoint | `http://localhost:8080/auth/` |
|
||||
|`keycloak_log_target`| Set the destination of the keycloak log folder link | `/var/log/keycloak` |
|
||||
|
||||
|
||||
Role Variables
|
||||
--------------
|
||||
|
||||
The following are a set of _required_ variables for the role:
|
||||
|
||||
| Variable | Description |
|
||||
|:---------|:------------|
|
||||
|`keycloak_admin_password`| Password for the administration console user account (minimum 12 characters) |
|
||||
|`keycloak_frontend_url` | frontend URL for keycloak endpoint | `http://localhost:8080/auth/` |
|
||||
|
||||
|
||||
The following parameters are _required_ only when `keycloak_ha_enabled` is true:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|:---------|:------------|:--------|
|
||||
|`keycloak_modcluster_enabled`| Enable configuration for modcluster subsystem | `True` if `keycloak_ha_enabled` is True, else `False` |
|
||||
|`keycloak_modcluster_url` | _deprecated_ Host for the modcluster reverse proxy | `localhost` |
|
||||
|`keycloak_modcluster_port` | _deprecated_ Port for the modcluster reverse proxy | `6666` |
|
||||
|`keycloak_modcluster_urls` | List of {host,port} dicts for the modcluster reverse proxies | `[ { localhost:6666 } ]` |
|
||||
|`keycloak_jdbc_engine` | backend database engine when db is enabled: [ postgres, mariadb, sqlserver ] | `postgres` |
|
||||
|`keycloak_infinispan_url` | URL for the infinispan remote-cache server | `localhost:11122` |
|
||||
|`keycloak_infinispan_user` | username for connecting to infinispan | `supervisor` |
|
||||
|`keycloak_infinispan_pass` | password for connecting to infinispan | `supervisor` |
|
||||
|`keycloak_infinispan_sasl_mechanism`| Authentication type | `SCRAM-SHA-512` |
|
||||
|`keycloak_infinispan_use_ssl`| Enable hotrod TLS communication | `False` |
|
||||
|`keycloak_infinispan_trust_store_path`| Path to truststore with infinispan server certificate | `/etc/pki/java/cacerts` |
|
||||
|`keycloak_infinispan_trust_store_password`| Password for opening truststore | `changeit` |
|
||||
|
||||
|
||||
The following parameters are _required_ only when `keycloak_db_enabled` is true:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|:---------|:------------|:---------|
|
||||
|`keycloak_jdbc_url` | URL for the postgres backend database | `jdbc:postgresql://localhost:5432/keycloak` |
|
||||
|`keycloak_jdbc_driver_version`| Version for the JDBC driver to download | `9.4.1212` |
|
||||
|`keycloak_db_user` | username for connecting to postgres | `keycloak-user` |
|
||||
|`keycloak_db_pass` | password for connecting to postgres | `keycloak-pass` |
|
||||
|
||||
|
||||
The following variables are _optional_:
|
||||
|
||||
| Variable | Description |
|
||||
|:---------|:------------|
|
||||
|`keycloak_db_valid_conn_sql` | Override the default database connection validation query sql |
|
||||
|`keycloak_admin_url` | Override the default administration endpoint URL |
|
||||
|`keycloak_jgroups_subnet`| Override the subnet match for jgroups cluster formation; if not defined, it will be inferred from local machine route configuration |
|
||||
|
||||
Example Playbook
|
||||
-----------------
|
||||
|
||||
* The following is an example playbook that makes use of the role to install keycloak from remote:
|
||||
|
||||
```yaml
|
||||
---
|
||||
- hosts: ...
|
||||
vars:
|
||||
keycloak_admin_password: "remembertochangeme"
|
||||
roles:
|
||||
- middleware_automation.keycloak.keycloak
|
||||
```
|
||||
|
||||
|
||||
* The following example playbook makes use of the role to install keycloak from the controller node:
|
||||
|
||||
```yaml
|
||||
---
|
||||
- hosts: ...
|
||||
collections:
|
||||
- middleware_automation.keycloak
|
||||
tasks:
|
||||
- name: Include keycloak role
|
||||
include_role:
|
||||
name: keycloak
|
||||
vars:
|
||||
keycloak_admin_password: "remembertochangeme"
|
||||
keycloak_offline_install: true
|
||||
# This should be the filename of keycloak archive on Ansible node: keycloak-16.1.0.zip
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Apache License 2.0
|
||||
|
||||
|
||||
Author Information
|
||||
------------------
|
||||
|
||||
* [Guido Grazioli](https://github.com/guidograzioli)
|
||||
* [Romain Pelisse](https://github.com/rpelisse)
|
||||
* [Pavan Kumar Motaparthi](https://github.com/motaparthipavankumar)
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
### Configuration specific to keycloak
|
||||
keycloak_version: 18.0.2
|
||||
keycloak_archive: "keycloak-legacy-{{ keycloak_version }}.zip"
|
||||
keycloak_download_url: "https://github.com/keycloak/keycloak/releases/download/{{ keycloak_version }}/{{ keycloak_archive }}"
|
||||
keycloak_download_url_9x: "https://downloads.jboss.org/keycloak/{{ keycloak_version }}/{{ keycloak_archive }}"
|
||||
keycloak_installdir: "{{ keycloak_dest }}/keycloak-{{ keycloak_version }}"
|
||||
keycloak_offline_install: false
|
||||
|
||||
# Authentication for Keycloak binary download (e.g. from internal artifact repository)
|
||||
keycloak_binary_download_user:
|
||||
keycloak_binary_download_pass:
|
||||
|
||||
### Install location and service settings
|
||||
keycloak_java_home:
|
||||
keycloak_dest: /opt/keycloak
|
||||
keycloak_jboss_home: "{{ keycloak_installdir }}"
|
||||
keycloak_jboss_port_offset: 0
|
||||
keycloak_config_dir: "{{ keycloak_jboss_home }}/standalone/configuration"
|
||||
keycloak_config_standalone_xml: "keycloak.xml"
|
||||
keycloak_config_path_to_standalone_xml: "{{ keycloak_jboss_home }}/standalone/configuration/{{ keycloak_config_standalone_xml }}"
|
||||
keycloak_config_override_template: ''
|
||||
keycloak_config_path_to_properties: "{{ keycloak_jboss_home }}/standalone/configuration/profile.properties"
|
||||
keycloak_service_runas: false
|
||||
keycloak_service_user: keycloak
|
||||
keycloak_service_group: keycloak
|
||||
keycloak_service_pidfile: "/run/keycloak/keycloak.pid"
|
||||
keycloak_service_name: keycloak
|
||||
keycloak_service_desc: Keycloak
|
||||
keycloak_service_start_delay: 10
|
||||
keycloak_service_start_retries: 25
|
||||
keycloak_service_restart_always: false
|
||||
keycloak_service_restart_on_failure: false
|
||||
keycloak_service_startlimitintervalsec: "300"
|
||||
keycloak_service_startlimitburst: "5"
|
||||
keycloak_service_restartsec: "10s"
|
||||
|
||||
keycloak_configure_firewalld: false
|
||||
keycloak_configure_iptables: false
|
||||
|
||||
### administrator console password
|
||||
keycloak_admin_password: ''
|
||||
|
||||
### Common configuration settings
|
||||
keycloak_bind_address: 0.0.0.0
|
||||
keycloak_host: localhost
|
||||
keycloak_http_port: 8080
|
||||
keycloak_https_port: 8443
|
||||
keycloak_ajp_port: 8009
|
||||
keycloak_jgroups_port: 7600
|
||||
keycloak_jgroups_subnet:
|
||||
keycloak_management_port_bind_address: 127.0.0.1
|
||||
keycloak_management_http_port: 9990
|
||||
keycloak_management_https_port: 9993
|
||||
keycloak_java_opts: "-Xms1024m -Xmx2048m"
|
||||
keycloak_prefer_ipv4: true
|
||||
keycloak_features: []
|
||||
|
||||
### Enable configuration for database backend, clustering and remote caches on infinispan
|
||||
keycloak_ha_enabled: false
|
||||
### Enable database configuration, must be enabled when HA is configured
|
||||
keycloak_db_enabled: "{{ True if keycloak_ha_enabled else False }}"
|
||||
### Discovery protocol for ha cluster members, valus [ 'JDBC_PING', 'TCPPING' ]
|
||||
keycloak_ha_discovery: "{{ 'JDBC_PING' if keycloak_db_enabled else 'TCPPING' }}"
|
||||
### Remote cache store on infinispan cluster
|
||||
keycloak_remote_cache_enabled: "{{ True if keycloak_ha_enabled else False }}"
|
||||
|
||||
### Keycloak administration console user
|
||||
keycloak_admin_user: admin
|
||||
keycloak_auth_realm: master
|
||||
keycloak_auth_client: admin-cli
|
||||
|
||||
keycloak_force_install: false
|
||||
|
||||
### mod_cluster reverse proxy list
|
||||
keycloak_modcluster_enabled: "{{ True if keycloak_ha_enabled else False }}"
|
||||
keycloak_modcluster_url: localhost
|
||||
keycloak_modcluster_port: 6666
|
||||
keycloak_modcluster_urls:
|
||||
- host: "{{ keycloak_modcluster_url }}"
|
||||
port: "{{ keycloak_modcluster_port }}"
|
||||
|
||||
### keycloak frontend url
|
||||
keycloak_frontend_url: http://localhost:8080/auth/
|
||||
keycloak_frontend_url_force: false
|
||||
keycloak_admin_url:
|
||||
|
||||
### infinispan remote caches access (hotrod)
|
||||
keycloak_infinispan_user: supervisor
|
||||
keycloak_infinispan_pass: supervisor
|
||||
keycloak_infinispan_url: localhost
|
||||
keycloak_infinispan_sasl_mechanism: SCRAM-SHA-512
|
||||
keycloak_infinispan_use_ssl: false
|
||||
# if ssl is enabled, import ispn server certificate here
|
||||
keycloak_infinispan_trust_store_path: /etc/pki/java/cacerts
|
||||
keycloak_infinispan_trust_store_password: changeit
|
||||
|
||||
### database backend engine: values [ 'postgres', 'mariadb', 'sqlserver' ]
|
||||
keycloak_jdbc_engine: postgres
|
||||
### database backend credentials
|
||||
keycloak_db_user: keycloak-user
|
||||
keycloak_db_pass: keycloak-pass
|
||||
## connection validation
|
||||
keycloak_db_background_validation: false
|
||||
keycloak_db_background_validation_millis: "{{ 10000 if keycloak_db_background_validation else 0 }}"
|
||||
keycloak_db_background_validate_on_match: false
|
||||
keycloak_jdbc_url: "{{ keycloak_default_jdbc[keycloak_jdbc_engine].url }}"
|
||||
keycloak_jdbc_driver_version: "{{ keycloak_default_jdbc[keycloak_jdbc_engine].version }}"
|
||||
# override the variables above, following defaults show minimum supported versions
|
||||
keycloak_default_jdbc:
|
||||
postgres:
|
||||
url: 'jdbc:postgresql://localhost:5432/keycloak'
|
||||
version: 9.4.1212
|
||||
mariadb:
|
||||
url: 'jdbc:mariadb://localhost:3306/keycloak'
|
||||
version: 2.7.4
|
||||
sqlserver:
|
||||
url: 'jdbc:sqlserver://localhost:1433;databaseName=keycloak;'
|
||||
version: 12.2.0
|
||||
# role specific vars
|
||||
keycloak_no_log: true
|
||||
|
||||
### logging configuration
|
||||
keycloak_log_target: /var/log/keycloak
|
||||
|
||||
# locations
|
||||
keycloak_url: "http://{{ keycloak_host }}:{{ keycloak_http_port + keycloak_jboss_port_offset }}"
|
||||
keycloak_management_url: "http://{{ keycloak_host }}:{{ keycloak_management_http_port + keycloak_jboss_port_offset }}"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
- name: "Restart handler"
|
||||
ansible.builtin.include_tasks: restart_keycloak.yml
|
||||
listen: "restart keycloak"
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
argument_specs:
|
||||
main:
|
||||
options:
|
||||
keycloak_version:
|
||||
default: "18.0.2"
|
||||
description: "keycloak.org package version"
|
||||
type: "str"
|
||||
keycloak_archive:
|
||||
default: "keycloak-legacy-{{ keycloak_version }}.zip"
|
||||
description: "keycloak install archive filename"
|
||||
type: "str"
|
||||
keycloak_configure_iptables:
|
||||
default: false
|
||||
description: "Ensure iptables is running and configure keycloak ports"
|
||||
type: "bool"
|
||||
keycloak_configure_firewalld:
|
||||
default: false
|
||||
description: "Ensure firewalld is running and configure keycloak ports"
|
||||
type: "bool"
|
||||
keycloak_download_url:
|
||||
default: "https://github.com/keycloak/keycloak/releases/download/{{ keycloak_version }}/{{ keycloak_archive }}"
|
||||
description: "Download URL for keycloak"
|
||||
type: "str"
|
||||
keycloak_download_url_9x:
|
||||
default: "https://downloads.jboss.org/keycloak/{{ keycloak_version }}/{{ keycloak_archive }}"
|
||||
description: "Download URL for keycloak (deprecated)"
|
||||
type: "str"
|
||||
keycloak_installdir:
|
||||
default: "{{ keycloak_dest }}/keycloak-{{ keycloak_version }}"
|
||||
description: "Installation path"
|
||||
type: "str"
|
||||
keycloak_offline_install:
|
||||
default: false
|
||||
description: "Perform an offline install"
|
||||
type: "bool"
|
||||
keycloak_jvm_package:
|
||||
default: "java-1.8.0-openjdk-headless"
|
||||
description: "RHEL java package runtime rpm"
|
||||
type: "str"
|
||||
keycloak_java_home:
|
||||
description: "JAVA_HOME of installed JRE, leave empty for using specified keycloak_jvm_package RPM path"
|
||||
type: "str"
|
||||
keycloak_dest:
|
||||
default: "/opt/keycloak"
|
||||
description: "Root installation directory"
|
||||
type: "str"
|
||||
keycloak_jboss_home:
|
||||
default: "{{ keycloak_installdir }}"
|
||||
description: "Installation work directory"
|
||||
type: "str"
|
||||
keycloak_jboss_port_offset:
|
||||
default: 0
|
||||
description: "Port offset for the JBoss socket binding"
|
||||
type: "int"
|
||||
keycloak_config_dir:
|
||||
default: "{{ keycloak_jboss_home }}/standalone/configuration"
|
||||
description: "Path for configuration"
|
||||
type: "str"
|
||||
keycloak_config_standalone_xml:
|
||||
default: "keycloak.xml"
|
||||
description: "Service configuration filename"
|
||||
type: "str"
|
||||
keycloak_config_path_to_standalone_xml:
|
||||
default: "{{ keycloak_jboss_home }}/standalone/configuration/{{ keycloak_config_standalone_xml }}"
|
||||
description: "Custom path for configuration"
|
||||
type: "str"
|
||||
keycloak_config_override_template:
|
||||
default: ""
|
||||
description: "Path to custom template for standalone.xml configuration"
|
||||
type: "str"
|
||||
keycloak_service_runas:
|
||||
default: false
|
||||
description: "Enable execution of service as `keycloak_service_user`"
|
||||
type: "bool"
|
||||
keycloak_service_user:
|
||||
default: "keycloak"
|
||||
description: "posix account username"
|
||||
type: "str"
|
||||
keycloak_service_group:
|
||||
default: "keycloak"
|
||||
description: "posix account group"
|
||||
type: "str"
|
||||
keycloak_service_pidfile:
|
||||
default: "/run/keycloak/keycloak.pid"
|
||||
description: "PID file path for service"
|
||||
type: "str"
|
||||
keycloak_features:
|
||||
default: "[]"
|
||||
description: >
|
||||
List of `name`/`status` pairs of features (also known as profiles on RH-SSO) to `enable` or `disable`,
|
||||
example: `[ { name: 'docker', status: 'enabled' } ]`
|
||||
type: "list"
|
||||
keycloak_bind_address:
|
||||
default: "0.0.0.0"
|
||||
description: "Address for binding service ports"
|
||||
type: "str"
|
||||
keycloak_management_port_bind_address:
|
||||
default: "127.0.0.1"
|
||||
description: "Address for binding the management ports"
|
||||
type: "str"
|
||||
keycloak_host:
|
||||
default: "localhost"
|
||||
description: "Hostname for service"
|
||||
type: "str"
|
||||
keycloak_http_port:
|
||||
default: 8080
|
||||
description: "Listening HTTP port"
|
||||
type: "int"
|
||||
keycloak_https_port:
|
||||
default: 8443
|
||||
description: "Listening HTTPS port"
|
||||
type: "int"
|
||||
keycloak_ajp_port:
|
||||
default: 8009
|
||||
description: "Listening AJP port"
|
||||
type: "int"
|
||||
keycloak_jgroups_port:
|
||||
default: 7600
|
||||
description: "jgroups cluster tcp port"
|
||||
type: "int"
|
||||
keycloak_management_http_port:
|
||||
default: 9990
|
||||
description: "Management port (http)"
|
||||
type: "int"
|
||||
keycloak_management_https_port:
|
||||
default: 9993
|
||||
description: "Management port (https)"
|
||||
type: "int"
|
||||
keycloak_java_opts:
|
||||
default: "-Xms1024m -Xmx2048m"
|
||||
description: "Additional JVM options"
|
||||
type: "str"
|
||||
keycloak_prefer_ipv4:
|
||||
default: true
|
||||
description: "Prefer IPv4 stack and addresses for port binding"
|
||||
type: "bool"
|
||||
keycloak_ha_enabled:
|
||||
default: false
|
||||
description: "Enable auto configuration for database backend, clustering and remote caches on infinispan"
|
||||
type: "bool"
|
||||
keycloak_ha_discovery:
|
||||
default: "{{ 'JDBC_PING' if keycloak_db_enabled else 'TCPPING' }}"
|
||||
description: "Discovery protocol for HA cluster members"
|
||||
type: "str"
|
||||
keycloak_db_enabled:
|
||||
default: "{{ True if keycloak_ha_enabled else False }}"
|
||||
description: "Enable auto configuration for database backend"
|
||||
type: "bool"
|
||||
keycloak_admin_user:
|
||||
default: "admin"
|
||||
description: "Administration console user account"
|
||||
type: "str"
|
||||
keycloak_auth_realm:
|
||||
default: "master"
|
||||
description: "Name for rest authentication realm"
|
||||
type: "str"
|
||||
keycloak_auth_client:
|
||||
default: "admin-cli"
|
||||
description: "Authentication client for configuration REST calls"
|
||||
type: "str"
|
||||
keycloak_force_install:
|
||||
default: false
|
||||
description: "Remove pre-existing versions of service"
|
||||
type: "bool"
|
||||
keycloak_modcluster_enabled:
|
||||
default: "{{ True if keycloak_ha_enabled else False }}"
|
||||
description: "Enable configuration for modcluster subsystem"
|
||||
type: "bool"
|
||||
keycloak_modcluster_url:
|
||||
default: "localhost"
|
||||
description: "URL for the modcluster reverse proxy"
|
||||
type: "str"
|
||||
keycloak_modcluster_port:
|
||||
default: 6666
|
||||
description: "Port for the modcluster reverse proxy"
|
||||
type: "int"
|
||||
keycloak_modcluster_urls:
|
||||
default: "[ { host: 'localhost', port: 6666 } ]"
|
||||
description: "List of modproxy node URLs in the format { host, port } for the modcluster reverse proxy"
|
||||
type: "list"
|
||||
keycloak_frontend_url:
|
||||
default: "http://localhost"
|
||||
description: "Frontend URL for keycloak endpoints when a reverse proxy is used"
|
||||
type: "str"
|
||||
keycloak_frontend_url_force:
|
||||
default: false
|
||||
description: "Force backend requests to use the frontend URL"
|
||||
type: "bool"
|
||||
keycloak_infinispan_user:
|
||||
default: "supervisor"
|
||||
description: "Username for connecting to infinispan"
|
||||
type: "str"
|
||||
keycloak_infinispan_pass:
|
||||
default: "supervisor"
|
||||
description: "Password for connecting to infinispan"
|
||||
type: "str"
|
||||
keycloak_infinispan_url:
|
||||
default: "localhost"
|
||||
description: "URL for the infinispan remote-cache server"
|
||||
type: "str"
|
||||
keycloak_infinispan_sasl_mechanism:
|
||||
default: "SCRAM-SHA-512"
|
||||
description: "Authentication type to infinispan server"
|
||||
type: "str"
|
||||
keycloak_infinispan_use_ssl:
|
||||
default: false
|
||||
description: "Enable hotrod client TLS communication"
|
||||
type: "bool"
|
||||
keycloak_infinispan_trust_store_path:
|
||||
default: "/etc/pki/java/cacerts"
|
||||
description: "TODO document argument"
|
||||
type: "str"
|
||||
keycloak_infinispan_trust_store_password:
|
||||
default: "changeit"
|
||||
description: "Path to truststore containing infinispan server certificate"
|
||||
type: "str"
|
||||
keycloak_jdbc_engine:
|
||||
default: "postgres"
|
||||
description: "Backend database flavour when db is enabled: [ postgres, mariadb, sqlserver ]"
|
||||
type: "str"
|
||||
keycloak_db_user:
|
||||
default: "keycloak-user"
|
||||
description: "Username for connecting to database"
|
||||
type: "str"
|
||||
keycloak_db_pass:
|
||||
default: "keycloak-pass"
|
||||
description: "Password for connecting to database"
|
||||
type: "str"
|
||||
keycloak_jdbc_url:
|
||||
default: "{{ keycloak_default_jdbc[keycloak_jdbc_engine].url }}"
|
||||
description: "URL for connecting to backend database"
|
||||
type: "str"
|
||||
keycloak_jdbc_driver_version:
|
||||
default: "{{ keycloak_default_jdbc[keycloak_jdbc_engine].version }}"
|
||||
description: "Version for the JDBC driver to download"
|
||||
type: "str"
|
||||
keycloak_admin_password:
|
||||
required: true
|
||||
description: "Password for the administration console user account"
|
||||
type: "str"
|
||||
keycloak_url:
|
||||
default: "http://{{ keycloak_host }}:{{ keycloak_http_port + keycloak_jboss_port_offset }}"
|
||||
description: "URL for configuration rest calls"
|
||||
type: "str"
|
||||
keycloak_management_url:
|
||||
default: "http://{{ keycloak_host }}:{{ keycloak_management_http_port + keycloak_jboss_port_offset }}"
|
||||
description: "URL for management console rest calls"
|
||||
type: "str"
|
||||
keycloak_service_name:
|
||||
default: "keycloak"
|
||||
description: "systemd service name for keycloak"
|
||||
type: "str"
|
||||
keycloak_service_desc:
|
||||
default: "Keycloak"
|
||||
description: "systemd description for keycloak"
|
||||
type: "str"
|
||||
keycloak_service_start_delay:
|
||||
default: "10"
|
||||
description: "Expected delay in ms before the service is expected to be available after start."
|
||||
type: "int"
|
||||
keycloak_service_start_retries:
|
||||
default: "25"
|
||||
description: "How many time should Ansible retry to connect to the service after it was started, before failing."
|
||||
type: "int"
|
||||
keycloak_service_restart_always:
|
||||
default: false
|
||||
description: "systemd restart always behavior activation for keycloak"
|
||||
type: "bool"
|
||||
keycloak_service_restart_on_failure:
|
||||
default: false
|
||||
description: "systemd restart on-failure behavior activation for keycloak"
|
||||
type: "bool"
|
||||
keycloak_service_startlimitintervalsec:
|
||||
default: 300
|
||||
description: "systemd StartLimitIntervalSec for keycloak"
|
||||
type: "int"
|
||||
keycloak_service_startlimitburst:
|
||||
default: 5
|
||||
description: "systemd StartLimitBurst for keycloak"
|
||||
type: "int"
|
||||
keycloak_service_restartsec:
|
||||
default: "5s"
|
||||
description: "systemd RestartSec for keycloak"
|
||||
type: "str"
|
||||
keycloak_no_log:
|
||||
default: true
|
||||
type: "bool"
|
||||
description: "Changes default behavior for no_log for debugging purpose, do not change for production system."
|
||||
keycloak_remote_cache_enabled:
|
||||
default: "{{ True if keycloak_ha_enabled else False }}"
|
||||
description: "Enable remote cache store when in clustered ha configurations"
|
||||
type: "bool"
|
||||
keycloak_db_background_validation:
|
||||
default: false
|
||||
description: "Enable background validation of database connection"
|
||||
type: "bool"
|
||||
keycloak_db_background_validation_millis:
|
||||
default: "{{ 10000 if keycloak_db_background_validation else 0 }}"
|
||||
description: "How frequenly the connection pool is validated in the background"
|
||||
type: 'int'
|
||||
keycloak_db_background_validate_on_match:
|
||||
default: false
|
||||
description: "Enable validate on match for database connections"
|
||||
type: "bool"
|
||||
keycloak_db_valid_conn_sql:
|
||||
required: false
|
||||
description: "Override the default database connection validation query sql"
|
||||
type: "str"
|
||||
keycloak_admin_url:
|
||||
required: false
|
||||
description: "Override the default administration endpoint URL"
|
||||
type: "str"
|
||||
keycloak_jgroups_subnet:
|
||||
required: false
|
||||
description: >
|
||||
Override the subnet match for jgroups cluster formation; if not defined, it will be inferred from local machine route configuration
|
||||
type: "str"
|
||||
keycloak_log_target:
|
||||
default: '/var/log/keycloak'
|
||||
type: "str"
|
||||
description: "Set the destination of the keycloak log folder link"
|
||||
keycloak_jdbc_download_url:
|
||||
description: "Override the default Maven Central download URL for the JDBC driver"
|
||||
type: "str"
|
||||
keycloak_jdbc_download_user:
|
||||
description: "Set a username with which to authenticate when downloading JDBC drivers from an alternative location"
|
||||
type: "str"
|
||||
keycloak_jdbc_download_pass:
|
||||
description: >
|
||||
Set a password with which to authenticate when downloading JDBC drivers from an alternative location (requires keycloak_jdbc_download_user)
|
||||
type: "str"
|
||||
keycloak_jdbc_download_validate_certs:
|
||||
default: true
|
||||
description: "Allow the option to ignore invalid certificates when downloading JDBC drivers from a custom URL"
|
||||
type: "bool"
|
||||
keycloak_binary_download_user:
|
||||
description: "Username for HTTP Basic Auth when downloading Keycloak binary"
|
||||
type: "str"
|
||||
required: false
|
||||
keycloak_binary_download_pass:
|
||||
description: "Password for HTTP Basic Auth when downloading Keycloak binary"
|
||||
type: "str"
|
||||
required: false
|
||||
downstream:
|
||||
options:
|
||||
sso_version:
|
||||
default: "7.6.0"
|
||||
description: "Red Hat Single Sign-On version"
|
||||
type: "str"
|
||||
sso_archive:
|
||||
default: "rh-sso-{{ sso_version }}-server-dist.zip"
|
||||
description: "Red Hat SSO install archive filename"
|
||||
type: "str"
|
||||
sso_dest:
|
||||
default: "/opt/sso"
|
||||
description: "Root installation directory"
|
||||
type: "str"
|
||||
sso_installdir:
|
||||
default: "{{ sso_dest }}/rh-sso-{{ sso_version.split('.')[0] }}.{{ sso_version.split('.')[1] }}"
|
||||
description: "Installation path for Red Hat SSO"
|
||||
type: "str"
|
||||
sso_apply_patches:
|
||||
default: false
|
||||
description: "Install Red Hat SSO most recent cumulative patch"
|
||||
type: "bool"
|
||||
sso_enable:
|
||||
default: true
|
||||
description: "Enable Red Hat Single Sign-on installation"
|
||||
type: "str"
|
||||
sso_offline_install:
|
||||
default: false
|
||||
description: "Perform an offline install"
|
||||
type: "bool"
|
||||
sso_service_name:
|
||||
default: "sso"
|
||||
description: "systemd service name for Single Sign-On"
|
||||
type: "str"
|
||||
sso_service_desc:
|
||||
default: "Red Hat Single Sign-On"
|
||||
description: "systemd description for Red Hat Single Sign-On"
|
||||
type: "str"
|
||||
sso_patch_version:
|
||||
required: false
|
||||
description: "Red Hat Single Sign-On latest cumulative patch version to apply; defaults to latest version when sso_apply_patches is True"
|
||||
type: "str"
|
||||
sso_patch_bundle:
|
||||
default: "rh-sso-{{ sso_patch_version | default('[0-9]+[.][0-9]+[.][0-9]+') }}-patch.zip"
|
||||
description: "Red Hat SSO patch archive filename"
|
||||
type: "str"
|
||||
sso_product_category:
|
||||
default: "CORE.SERVICE.RHSSO"
|
||||
description: "Unified Downloads API category for Single Sign-On"
|
||||
type: "str"
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
collections:
|
||||
- middleware_automation.common
|
||||
- ansible.posix
|
||||
|
||||
galaxy_info:
|
||||
role_name: keycloak
|
||||
namespace: middleware_automation
|
||||
author: Romain Pelisse, Guido Grazioli, Pavan Kumar Motaparthi
|
||||
description: Install keycloak or Red Hat Single Sign-On server configurations
|
||||
company: Red Hat, Inc.
|
||||
|
||||
license: Apache License 2.0
|
||||
|
||||
min_ansible_version: "2.16"
|
||||
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- "8"
|
||||
|
||||
galaxy_tags:
|
||||
- keycloak
|
||||
- redhat
|
||||
- rhel
|
||||
- sso
|
||||
- authentication
|
||||
- identity
|
||||
- security
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
- name: Include firewall config tasks
|
||||
ansible.builtin.include_tasks:
|
||||
file: iptables.yml
|
||||
apply:
|
||||
tags:
|
||||
- firewall
|
||||
when: keycloak_configure_iptables
|
||||
tags:
|
||||
- firewall
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
- name: "Check if packages are already installed" # noqa command-instead-of-module this runs faster
|
||||
ansible.builtin.command: "rpm -q {{ packages_list | join(' ') }}"
|
||||
register: keycloak_rpm_info
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when: ansible_facts.os_family == "RedHat"
|
||||
|
||||
- name: "Add missing packages to the yum install list"
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_packages_to_install: "{{ keycloak_packages_to_install | default([]) + keycloak_rpm_info.stdout_lines | \
|
||||
map('regex_findall', 'package (.+) is not installed$') | default([]) | flatten }}"
|
||||
when: ansible_facts.os_family == "RedHat"
|
||||
|
||||
- name: "Install packages: {{ keycloak_packages_to_install }}"
|
||||
become: "{{ keycloak_fastpackages_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.dnf:
|
||||
name: "{{ keycloak_packages_to_install }}"
|
||||
state: present
|
||||
when:
|
||||
- keycloak_packages_to_install | default([]) | length > 0
|
||||
- ansible_facts.os_family == "RedHat"
|
||||
|
||||
- name: "Install packages: {{ packages_list }}"
|
||||
become: "{{ keycloak_fastpackages_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.package:
|
||||
name: "{{ packages_list }}"
|
||||
state: present
|
||||
when:
|
||||
- packages_list | default([]) | length > 0
|
||||
- ansible_facts.os_family == "Debian"
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
- name: Ensure required package firewalld are installed
|
||||
ansible.builtin.include_tasks: fastpackages.yml
|
||||
vars:
|
||||
packages_list:
|
||||
- firewalld
|
||||
|
||||
- name: Enable and start the firewalld service
|
||||
become: "{{ keycloak_firewalld_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.systemd:
|
||||
name: firewalld
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: "Configure firewall ports for {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_firewalld_require_privilege_escalation | default(true) }}"
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ item }}"
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: true
|
||||
loop:
|
||||
- "{{ keycloak_http_port }}/tcp"
|
||||
- "{{ keycloak_https_port }}/tcp"
|
||||
- "{{ keycloak_management_http_port }}/tcp"
|
||||
- "{{ keycloak_management_https_port }}/tcp"
|
||||
- "{{ keycloak_jgroups_port }}/tcp"
|
||||
- "{{ keycloak_ajp_port }}/tcp"
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
---
|
||||
- name: Validate parameters
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- keycloak_jboss_home is defined
|
||||
- keycloak_service_user is defined
|
||||
- keycloak_dest is defined
|
||||
- keycloak_archive is defined
|
||||
- keycloak_download_url is defined
|
||||
- keycloak_version is defined
|
||||
quiet: true
|
||||
|
||||
- name: Check for an existing deployment
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ keycloak_jboss_home }}"
|
||||
register: keycloak_existing_deploy
|
||||
|
||||
- name: Stop and restart if existing deployment exists and install forced
|
||||
when: keycloak_existing_deploy.stat.exists and keycloak_force_install | bool
|
||||
block:
|
||||
- name: "Stop the old service: {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
failed_when: false
|
||||
ansible.builtin.systemd:
|
||||
name: keycloak
|
||||
state: stopped
|
||||
- name: "Remove the old deployment: {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.file:
|
||||
path: "{{ keycloak_jboss_home }}"
|
||||
state: absent
|
||||
|
||||
- name: Check for an existing deployment after possible forced removal
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ keycloak_jboss_home }}"
|
||||
|
||||
- name: "Create service user/group for {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.user:
|
||||
name: "{{ keycloak_service_user }}"
|
||||
home: /opt/keycloak
|
||||
system: true
|
||||
create_home: false
|
||||
|
||||
- name: "Create install location for {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.file:
|
||||
dest: "{{ keycloak_dest }}"
|
||||
state: directory
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
mode: '0750'
|
||||
|
||||
- name: Create pidfile folder
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.file:
|
||||
dest: "{{ keycloak_service_pidfile | dirname }}"
|
||||
state: directory
|
||||
owner: "{{ keycloak_service_user if keycloak_service_runas else omit }}"
|
||||
group: "{{ keycloak_service_group if keycloak_service_runas else omit }}"
|
||||
mode: '0750'
|
||||
|
||||
## check remote archive
|
||||
- name: Set download archive path
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_archive_full_path: "{{ keycloak_dest }}/{{ keycloak_archive }}"
|
||||
|
||||
- name: Check download archive path
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ keycloak_archive_full_path }}"
|
||||
register: keycloak_archive_path
|
||||
|
||||
## download to controller
|
||||
- name: Check local download archive path
|
||||
ansible.builtin.stat:
|
||||
path: "{{ lookup('env', 'PWD') }}"
|
||||
register: keycloak_local_path
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Download keycloak archive
|
||||
ansible.builtin.get_url: # noqa risky-file-permissions delegated, uses controller host user
|
||||
url: "{{ keycloak_download_url }}"
|
||||
dest: "{{ keycloak_local_path.stat.path }}/{{ keycloak_archive }}"
|
||||
mode: '0644'
|
||||
url_username: "{{ keycloak_binary_download_user | default(omit) }}"
|
||||
url_password: "{{ keycloak_binary_download_pass | default(omit) }}"
|
||||
validate_certs: "{{ keycloak_download_validate_certs | default(omit) }}"
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
when:
|
||||
- keycloak_archive_path is defined
|
||||
- keycloak_archive_path.stat is defined
|
||||
- not keycloak_archive_path.stat.exists
|
||||
- not sso_enable is defined or not sso_enable
|
||||
- not keycloak_offline_install
|
||||
|
||||
- name: Perform download from RHN using Unified Downloads API
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
when:
|
||||
- keycloak_archive_path is defined
|
||||
- keycloak_archive_path.stat is defined
|
||||
- not keycloak_archive_path.stat.exists
|
||||
- sso_enable is defined and sso_enable
|
||||
- not keycloak_offline_install
|
||||
block:
|
||||
- name: Retrieve product download using Unified Downloads API
|
||||
middleware_automation.common.product_search:
|
||||
client_id: "{{ rhn_username }}"
|
||||
client_secret: "{{ rhn_password }}"
|
||||
product_type: DISTRIBUTION
|
||||
product_version: "{{ sso_version.split('.')[:2] | join('.') }}"
|
||||
product_category: "{{ sso_product_category }}"
|
||||
validate_certs: "{{ keycloak_download_validate_certs | default(omit) }}"
|
||||
register: keycloak_rhn_products
|
||||
no_log: "{{ omit_rhn_output | default(true) }}"
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: Determine install zipfile from search results
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_rhn_filtered_products: >-
|
||||
{{ keycloak_rhn_products.results
|
||||
| selectattr('file_name', 'search', 'rh-sso-' + sso_version + '-server-dist.zip$')
|
||||
| list }}
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: Download Red Hat Single Sign-On
|
||||
middleware_automation.common.product_download: # noqa risky-file-permissions delegated, uses controller host user
|
||||
client_id: "{{ rhn_username }}"
|
||||
client_secret: "{{ rhn_password }}"
|
||||
product_id: "{{ (keycloak_rhn_filtered_products | first).id }}"
|
||||
dest: "{{ keycloak_local_path.stat.path }}/{{ keycloak_archive }}"
|
||||
validate_certs: "{{ keycloak_download_validate_certs | default(omit) }}"
|
||||
no_log: "{{ omit_rhn_output | default(true) }}"
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: Download rhsso archive from alternate location
|
||||
ansible.builtin.get_url: # noqa risky-file-permissions delegated, uses controller host user
|
||||
url: "{{ keycloak_rhsso_download_url }}"
|
||||
dest: "{{ keycloak_local_path.stat.path }}/{{ keycloak_archive }}"
|
||||
mode: '0644'
|
||||
validate_certs: "{{ keycloak_download_validate_certs | default(omit) }}"
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
when:
|
||||
- keycloak_archive_path is defined
|
||||
- keycloak_archive_path.stat is defined
|
||||
- not keycloak_archive_path.stat.exists
|
||||
- sso_enable is defined and sso_enable
|
||||
- not keycloak_offline_install
|
||||
- keycloak_rhsso_download_url is defined
|
||||
|
||||
- name: Check downloaded archive
|
||||
ansible.builtin.stat:
|
||||
path: "{{ keycloak_local_path.stat.path }}/{{ keycloak_archive }}"
|
||||
register: keycloak_local_archive_path
|
||||
delegate_to: localhost
|
||||
|
||||
## copy and unpack
|
||||
- name: Copy archive to target nodes
|
||||
ansible.builtin.copy:
|
||||
src: "{{ keycloak_local_path.stat.path }}/{{ keycloak_archive }}"
|
||||
dest: "{{ keycloak_archive_full_path }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
mode: '0640'
|
||||
register: keycloak_new_version_downloaded
|
||||
when:
|
||||
- not keycloak_archive_path.stat.exists
|
||||
- keycloak_local_archive_path.stat is defined
|
||||
- keycloak_local_archive_path.stat.exists
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
|
||||
- name: "Check target directory: {{ keycloak_config.home }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ keycloak_config.home }}"
|
||||
register: keycloak_path_to_workdir
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
|
||||
- name: "Extract archive on target: {{ keycloak_service_desc }}"
|
||||
ansible.builtin.unarchive:
|
||||
remote_src: true
|
||||
src: "{{ keycloak_archive_full_path }}"
|
||||
dest: "{{ keycloak_dest }}"
|
||||
creates: "{{ keycloak_config.home }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
when:
|
||||
- keycloak_new_version_downloaded.changed or not keycloak_path_to_workdir.stat.exists
|
||||
notify:
|
||||
- restart keycloak
|
||||
|
||||
- name: Inform decompression was not executed
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ keycloak_config.home }} already exists and version unchanged, skipping decompression"
|
||||
when:
|
||||
- not keycloak_new_version_downloaded.changed and keycloak_path_to_workdir.stat.exists
|
||||
|
||||
- name: "Reown installation directory to {{ keycloak_service_user }}"
|
||||
ansible.builtin.file:
|
||||
path: "{{ keycloak_config.home }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
recurse: true
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Ensure permissions are correct on existing deploy
|
||||
ansible.builtin.command: chown -R "{{ keycloak_service_user }}:{{ keycloak_service_group }}" "{{ keycloak_config.home }}"
|
||||
when: keycloak_service_runas
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
changed_when: false
|
||||
|
||||
# driver and configuration
|
||||
- name: "Install database driver: {{ keycloak_jdbc_engine }}"
|
||||
ansible.builtin.include_tasks: jdbc_driver.yml
|
||||
when: keycloak_jdbc[keycloak_jdbc_engine].enabled
|
||||
|
||||
- name: "Deploy custom config: {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.template:
|
||||
src: "templates/{{ keycloak_config_override_template }}"
|
||||
dest: "{{ keycloak_config_path_to_standalone_xml }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
mode: '0640'
|
||||
notify:
|
||||
- restart keycloak
|
||||
when: (keycloak_config_override_template | default('', true)) | length > 0
|
||||
|
||||
- name: "Deploy standalone config: {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.template:
|
||||
src: templates/standalone.xml.j2
|
||||
dest: "{{ keycloak_config_path_to_standalone_xml }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
mode: '0640'
|
||||
notify:
|
||||
- restart keycloak
|
||||
when:
|
||||
- not keycloak_ha_enabled
|
||||
- (keycloak_config_override_template | default('', true)) | length == 0
|
||||
|
||||
- name: Create tcpping cluster node list
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_cluster_nodes: >
|
||||
{{ keycloak_cluster_nodes | default([]) + [
|
||||
{
|
||||
"name": item,
|
||||
"address": 'jgroups-' + item,
|
||||
"inventory_host": hostvars[item].ansible_default_ipv4.address | default(item) + '[' + (keycloak_jgroups_port | string) + ']',
|
||||
"value": hostvars[item].ansible_default_ipv4.address | default(item)
|
||||
}
|
||||
] }}
|
||||
loop: "{{ ansible_play_batch }}"
|
||||
when: keycloak_ha_enabled and keycloak_ha_discovery == 'TCPPING'
|
||||
|
||||
- name: "Deploy HA config: {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.template:
|
||||
src: templates/standalone-ha.xml.j2
|
||||
dest: "{{ keycloak_config_path_to_standalone_xml }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
mode: '0640'
|
||||
notify:
|
||||
- restart keycloak
|
||||
when:
|
||||
- keycloak_ha_enabled
|
||||
- not keycloak_remote_cache_enabled
|
||||
- (keycloak_config_override_template | default('', true)) | length == 0
|
||||
|
||||
- name: "Deploy HA config with infinispan: {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.template:
|
||||
src: templates/standalone-infinispan.xml.j2
|
||||
dest: "{{ keycloak_config_path_to_standalone_xml }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
mode: '0640'
|
||||
notify:
|
||||
- restart keycloak
|
||||
when:
|
||||
- keycloak_ha_enabled
|
||||
- keycloak_remote_cache_enabled
|
||||
- (keycloak_config_override_template | default('', true)) | length == 0
|
||||
|
||||
- name: "Deploy profile.properties file to {{ keycloak_config_path_to_properties }}"
|
||||
become: "{{ keycloak_install_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.template:
|
||||
src: keycloak-profile.properties.j2
|
||||
dest: "{{ keycloak_config_path_to_properties }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
mode: '0640'
|
||||
notify:
|
||||
- restart keycloak
|
||||
when: (keycloak_features | default([], true)) | length > 0
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
- name: Ensure required package iptables are installed
|
||||
ansible.builtin.include_tasks: fastpackages.yml
|
||||
vars:
|
||||
packages_list:
|
||||
- iptables
|
||||
|
||||
- name: "Configure firewall ports for {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_iptables_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.iptables:
|
||||
destination_port: "{{ item }}"
|
||||
action: "insert"
|
||||
rule_num: 6 # magic number I forget why
|
||||
chain: "INPUT"
|
||||
policy: "ACCEPT"
|
||||
protocol: tcp
|
||||
loop:
|
||||
- "{{ keycloak_http_port }}"
|
||||
- "{{ keycloak_https_port }}"
|
||||
- "{{ keycloak_management_http_port }}"
|
||||
- "{{ keycloak_management_https_port }}"
|
||||
- "{{ keycloak_jgroups_port }}"
|
||||
- "{{ keycloak_ajp_port }}"
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
- name: "Check module directory: {{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_dir }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_dir }}"
|
||||
register: keycloak_dest_path
|
||||
become: "{{ keycloak_jdbc_driver_require_privilege_escalation | default(true) }}"
|
||||
|
||||
- name: "Set up module dir for JDBC Driver {{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }}"
|
||||
ansible.builtin.file:
|
||||
path: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_dir }}"
|
||||
state: directory
|
||||
recurse: true
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
mode: '0750'
|
||||
become: "{{ keycloak_jdbc_driver_require_privilege_escalation | default(true) }}"
|
||||
when:
|
||||
- not dest_path.stat.exists
|
||||
- name: "Verify valid parameters for download credentials when specified"
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
When JDBC driver download credentials are set, both the username and the password MUST be set
|
||||
when: >
|
||||
(keycloak_jdbc_download_user is undefined and keycloak_jdbc_download_pass is not undefined) or
|
||||
(keycloak_jdbc_download_pass is undefined and keycloak_jdbc_download_user is not undefined)
|
||||
|
||||
- name: "Retrieve JDBC Driver from {{ keycloak_jdbc[keycloak_jdbc_engine].driver_jar_url }}"
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_jar_url }}"
|
||||
dest: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_dir }}/{{ keycloak_jdbc[keycloak_jdbc_engine].driver_jar_filename }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
url_username: "{{ keycloak_jdbc_download_user | default(omit) }}"
|
||||
url_password: "{{ keycloak_jdbc_download_pass | default(omit) }}"
|
||||
validate_certs: "{{ keycloak_jdbc_download_validate_certs | default(omit) }}"
|
||||
mode: '0640'
|
||||
become: "{{ keycloak_jdbc_driver_require_privilege_escalation | default(true) }}"
|
||||
|
||||
- name: "Deploy module.xml for JDBC Driver"
|
||||
ansible.builtin.template:
|
||||
src: "templates/jdbc_driver_module.xml.j2"
|
||||
dest: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_dir }}/module.xml"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
mode: '0640'
|
||||
become: "{{ keycloak_jdbc_driver_require_privilege_escalation | default(true) }}"
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
# tasks file for keycloak
|
||||
- name: Check prerequisites
|
||||
ansible.builtin.include_tasks:
|
||||
file: prereqs.yml
|
||||
apply:
|
||||
tags:
|
||||
- prereqs
|
||||
tags:
|
||||
- prereqs
|
||||
|
||||
- name: Distro specific tasks
|
||||
ansible.builtin.include_tasks:
|
||||
file: "{{ ansible_os_family | lower }}.yml"
|
||||
apply:
|
||||
tags:
|
||||
- unbound
|
||||
tags:
|
||||
- unbound
|
||||
|
||||
- name: Include install tasks
|
||||
ansible.builtin.include_tasks:
|
||||
file: install.yml
|
||||
apply:
|
||||
tags:
|
||||
- install
|
||||
tags:
|
||||
- install
|
||||
|
||||
- name: Include systemd tasks
|
||||
ansible.builtin.include_tasks:
|
||||
file: systemd.yml
|
||||
apply:
|
||||
tags:
|
||||
- systemd
|
||||
tags:
|
||||
- systemd
|
||||
|
||||
- name: Include patch install tasks
|
||||
ansible.builtin.include_tasks: rhsso_patch.yml
|
||||
when:
|
||||
- sso_apply_patches is defined and sso_apply_patches
|
||||
- sso_enable is defined and sso_enable
|
||||
- ansible_facts.os_family == "RedHat"
|
||||
tags:
|
||||
- install
|
||||
- patch
|
||||
|
||||
- name: Link default logs directory
|
||||
ansible.builtin.file:
|
||||
state: link
|
||||
src: "{{ keycloak_jboss_home }}/standalone/log"
|
||||
dest: "{{ keycloak_log_target }}"
|
||||
become: "{{ keycloak_require_privilege_escalation | default(true) }}"
|
||||
|
||||
- name: Set admin credentials and restart if not already created
|
||||
block:
|
||||
- name: Check admin credentials by generating a token (supposed to fail on first installation)
|
||||
ansible.builtin.uri:
|
||||
url: "{{ keycloak_url }}/auth/realms/master/protocol/openid-connect/token"
|
||||
method: POST
|
||||
body: "client_id={{ keycloak_auth_client }}&username={{ keycloak_admin_user }}&password={{ keycloak_admin_password }}&grant_type=password"
|
||||
validate_certs: "{{ keycloak_validate_certs | default(omit) }}"
|
||||
register: keycloak_auth_response
|
||||
until: keycloak_auth_response.status == 200
|
||||
retries: 2
|
||||
delay: 2
|
||||
rescue:
|
||||
- name: "Create admin user: {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.command:
|
||||
args:
|
||||
argv:
|
||||
- "{{ keycloak_jboss_home }}/bin/add-user-keycloak.sh"
|
||||
- "-rmaster"
|
||||
- "-u{{ keycloak_admin_user }}"
|
||||
- "-p{{ keycloak_admin_password }}"
|
||||
changed_when: true
|
||||
become: "{{ keycloak_require_privilege_escalation | default(true) }}"
|
||||
- name: "Restart service: {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.include_tasks: tasks/restart_keycloak.yml
|
||||
- name: "Wait until service becomes active: {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.uri:
|
||||
url: "{{ keycloak_config.health_url }}"
|
||||
validate_certs: "{{ keycloak_validate_certs | default(omit) }}"
|
||||
register: keycloak_status
|
||||
until: keycloak_status.status == 200
|
||||
retries: 25
|
||||
delay: 10
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
- name: Validate admin console password
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (keycloak_admin_password | default('', true)) | length > 12
|
||||
quiet: true
|
||||
fail_msg: >
|
||||
The console administrator password is empty or invalid. Please set the keycloak_admin_password variable to a 12+ char long string
|
||||
success_msg: "{{ 'Console administrator password OK' }}"
|
||||
|
||||
- name: Validate configuration
|
||||
ansible.builtin.assert:
|
||||
that: >
|
||||
(keycloak_ha_enabled and keycloak_db_enabled) or
|
||||
(not keycloak_ha_enabled and keycloak_db_enabled) or
|
||||
(not keycloak_ha_enabled and not keycloak_db_enabled)
|
||||
quiet: true
|
||||
fail_msg: "Cannot install HA setup without a backend database service. Check keycloak_ha_enabled and keycloak_db_enabled"
|
||||
success_msg: "{{ 'Configuring HA' if keycloak_ha_enabled else 'Configuring standalone' }}"
|
||||
|
||||
- name: Validate credentials
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (rhn_username is defined and sso_enable is defined and sso_enable) or not sso_enable is defined or not sso_enable or keycloak_offline_install
|
||||
- (rhn_password is defined and sso_enable is defined and sso_enable) or not sso_enable is defined or not sso_enable or keycloak_offline_install
|
||||
quiet: true
|
||||
fail_msg: "Cannot install Red Hat SSO without RHN credentials. Check rhn_username and rhn_password are defined"
|
||||
success_msg: "Installing {{ keycloak_service_desc }}"
|
||||
|
||||
- name: Validate persistence configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- keycloak_jdbc_engine is defined and keycloak_jdbc_engine in [ 'postgres', 'mariadb', 'sqlserver' ]
|
||||
- (keycloak_jdbc_url | default('', true)) | length > 0
|
||||
- (keycloak_db_user | default('', true)) | length > 0
|
||||
- (keycloak_db_pass | default('', true)) | length > 0
|
||||
quiet: true
|
||||
fail_msg: "Configuration for the JDBC persistence is invalid or incomplete"
|
||||
success_msg: "Configuring JDBC persistence using {{ keycloak_jdbc_engine }} database"
|
||||
when: keycloak_db_enabled
|
||||
|
||||
- name: Validate OS family
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_os_family in ["RedHat", "Debian"]
|
||||
quiet: true
|
||||
fail_msg: "Can only install on RedHat or Debian OS families; found {{ ansible_os_family }}"
|
||||
success_msg: "Installing on {{ ansible_os_family }}"
|
||||
|
||||
- name: Load OS specific variables
|
||||
ansible.builtin.include_vars: "vars/{{ ansible_os_family | lower }}.yml"
|
||||
tags:
|
||||
- always
|
||||
|
||||
- name: Ensure required packages are installed
|
||||
ansible.builtin.include_tasks: fastpackages.yml
|
||||
vars:
|
||||
packages_list: "{{ keycloak_prereq_package_list }}"
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
- name: Include firewall config tasks
|
||||
ansible.builtin.include_tasks:
|
||||
file: firewalld.yml
|
||||
apply:
|
||||
tags:
|
||||
- firewall
|
||||
when: keycloak_configure_firewalld
|
||||
tags:
|
||||
- firewall
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
- name: "Restart and enable service: {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.systemd:
|
||||
name: keycloak
|
||||
enabled: true
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
become: "{{ keycloak_restart_require_privilege_escalation | default(true) }}"
|
||||
delegate_to: "{{ ansible_play_hosts | first }}"
|
||||
run_once: true
|
||||
|
||||
- name: "Wait until service becomes active: {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.uri:
|
||||
url: "{{ keycloak_config.health_url }}"
|
||||
validate_certs: "{{ keycloak_validate_certs | default(omit) }}"
|
||||
register: keycloak_status
|
||||
until: keycloak_status.status == 200
|
||||
delegate_to: "{{ ansible_play_hosts | first }}"
|
||||
run_once: true
|
||||
retries: "{{ keycloak_service_start_retries }}"
|
||||
delay: "{{ keycloak_service_start_delay }}"
|
||||
|
||||
- name: "Restart and enable service (remaining nodes): {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.systemd:
|
||||
name: keycloak
|
||||
enabled: true
|
||||
state: restarted
|
||||
become: "{{ keycloak_restart_require_privilege_escalation | default(true) }}"
|
||||
when: inventory_hostname != ansible_play_hosts | first
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
- name: Ensure required params for CLI have been provided
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- cli_query is defined
|
||||
fail_msg: "Missing required parameters to execute CLI."
|
||||
quiet: true
|
||||
|
||||
- name: "Execute CLI query: {{ cli_query }}"
|
||||
ansible.builtin.command: >
|
||||
{{ keycloak_config.cli_path }} --connect --command='{{ cli_query }}' --controller={{ keycloak_host }}:{{ keycloak_management_http_port }}
|
||||
changed_when: false
|
||||
register: keycloak_cli_result
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
---
|
||||
## check remote patch archive
|
||||
- name: Set download patch archive path
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_patch_archive: "{{ keycloak_dest }}/{{ sso_patch_bundle }}"
|
||||
keycloak_patch_bundle: "{{ sso_patch_bundle }}"
|
||||
keycloak_patch_version: "{{ sso_patch_version }}"
|
||||
when: sso_patch_version is defined
|
||||
|
||||
- name: Check download patch archive path
|
||||
ansible.builtin.stat:
|
||||
path: "{{ keycloak_patch_archive }}"
|
||||
register: keycloak_patch_archive_path
|
||||
when: sso_patch_version is defined
|
||||
become: "{{ keycloak_rhsso_patch_require_privilege_escalation | default(true) }}"
|
||||
|
||||
- name: Perform patch download from RHN via Unified Downloads API
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
when:
|
||||
- sso_enable is defined and sso_enable
|
||||
- not keycloak_offline_install
|
||||
- sso_apply_patches
|
||||
block:
|
||||
- name: Retrieve product download using Unified Downloads API
|
||||
middleware_automation.common.product_search:
|
||||
client_id: "{{ rhn_username }}"
|
||||
client_secret: "{{ rhn_password }}"
|
||||
product_type: BUGFIX
|
||||
product_version: "{{ sso_version.split('.')[:2] | join('.') }}"
|
||||
product_category: "{{ sso_product_category }}"
|
||||
validate_certs: "{{ keycloak_download_validate_certs | default(omit) }}"
|
||||
register: keycloak_rhn_products
|
||||
no_log: "{{ omit_rhn_output | default(true) }}"
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: Determine patch versions list
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_filtered_versions: "{{ keycloak_rhn_products.results | map(attribute='file_name') | \
|
||||
select('match', '^[^/]*/rh-sso-.*[0-9]*[.][0-9]*[.][0-9]*.*$') | \
|
||||
map('regex_replace', '[^/]*/rh-sso-([0-9]*[.][0-9]*[.][0-9]*(-[0-9])?)-.*', '\\1') | list | unique }}"
|
||||
when: (sso_patch_version | default('', true)) | length == 0
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: Determine latest version
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_sso_latest_version: "{{ keycloak_filtered_versions | middleware_automation.common.version_sort | last }}"
|
||||
when: (sso_patch_version | default('', true)) | length == 0
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: Determine install zipfile from search results
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_rhn_filtered_products: "{{ keycloak_rhn_products.results | selectattr('file_name', 'match', '[^/]*/rh-sso-' + keycloak_sso_latest_version + '-patch.zip$') }}"
|
||||
keycloak_patch_bundle: "rh-sso-{{ keycloak_sso_latest_version }}-patch.zip"
|
||||
keycloak_patch_version: "{{ keycloak_sso_latest_version }}"
|
||||
when: (sso_patch_version | default('', true)) | length == 0
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: "Determine selected patch from supplied version: {{ sso_patch_version }}"
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_rhn_filtered_products: "{{ keycloak_rhn_products.results | selectattr('file_name', 'match', '[^/]*/' + sso_patch_bundle + '$') }}"
|
||||
keycloak_patch_bundle: "{{ sso_patch_bundle }}"
|
||||
keycloak_patch_version: "{{ sso_patch_version }}"
|
||||
when: sso_patch_version is defined
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: Download Red Hat Single Sign-On patch
|
||||
middleware_automation.common.product_download: # noqa risky-file-permissions delegated, uses controller host user
|
||||
client_id: "{{ rhn_username }}"
|
||||
client_secret: "{{ rhn_password }}"
|
||||
product_id: "{{ (keycloak_rhn_filtered_products | sort | last).id }}"
|
||||
dest: "{{ keycloak_local_path.stat.path }}/{{ keycloak_patch_bundle }}"
|
||||
validate_certs: "{{ keycloak_download_validate_certs | default(omit) }}"
|
||||
no_log: "{{ omit_rhn_output | default(true) }}"
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: Set download patch archive path
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_patch_archive: "{{ keycloak_dest }}/{{ keycloak_patch_bundle }}"
|
||||
|
||||
- name: Check download patch archive path
|
||||
ansible.builtin.stat:
|
||||
path: "{{ keycloak_patch_archive }}"
|
||||
register: keycloak_patch_archive_path
|
||||
become: "{{ keycloak_rhsso_patch_require_privilege_escalation | default(true) }}"
|
||||
|
||||
## copy and unpack
|
||||
- name: Copy patch archive to target nodes
|
||||
ansible.builtin.copy:
|
||||
src: "{{ keycloak_local_path.stat.path }}/{{ keycloak_patch_bundle }}"
|
||||
dest: "{{ keycloak_patch_archive }}"
|
||||
owner: "{{ keycloak_service_user }}"
|
||||
group: "{{ keycloak_service_group }}"
|
||||
mode: '0640'
|
||||
register: keycloak_new_version_downloaded
|
||||
when:
|
||||
- not keycloak_patch_archive_path.stat.exists
|
||||
- keycloak_local_archive_path.stat is defined
|
||||
- keycloak_local_archive_path.stat.exists
|
||||
become: "{{ keycloak_rhsso_patch_require_privilege_escalation | default(true) }}"
|
||||
|
||||
- name: "Check installed patches"
|
||||
ansible.builtin.include_tasks: rhsso_cli.yml
|
||||
vars:
|
||||
cli_query: "patch info"
|
||||
args:
|
||||
apply:
|
||||
become: "{{ keycloak_rhsso_patch_require_privilege_escalation | default(true) }}"
|
||||
become_user: "{{ keycloak_service_user }}"
|
||||
|
||||
- name: "Perform patching"
|
||||
when:
|
||||
- keycloak_cli_result is defined
|
||||
- keycloak_cli_result.stdout is defined
|
||||
- keycloak_patch_version | regex_replace('-[0-9]$', '') not in keycloak_cli_result.stdout
|
||||
block:
|
||||
- name: "Apply patch to server: {{ keycloak_patch_version }}"
|
||||
ansible.builtin.include_tasks: rhsso_cli.yml
|
||||
vars:
|
||||
cli_query: "patch apply {{ keycloak_patch_archive }}"
|
||||
args:
|
||||
apply:
|
||||
become: "{{ keycloak_rhsso_patch_require_privilege_escalation | default(true) }}"
|
||||
become_user: "{{ keycloak_service_user }}"
|
||||
|
||||
- name: "Restart server to ensure patch content is running"
|
||||
ansible.builtin.include_tasks: rhsso_cli.yml
|
||||
vars:
|
||||
cli_query: "shutdown --restart"
|
||||
when:
|
||||
- keycloak_cli_result.rc == 0
|
||||
args:
|
||||
apply:
|
||||
become: "{{ keycloak_rhsso_patch_require_privilege_escalation | default(true) }}"
|
||||
become_user: "{{ keycloak_service_user }}"
|
||||
|
||||
- name: "Wait until service becomes active: {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.uri:
|
||||
url: "{{ keycloak_config.health_url }}"
|
||||
validate_certs: "{{ keycloak_validate_certs | default(omit) }}"
|
||||
register: keycloak_status
|
||||
until: keycloak_status.status == 200
|
||||
retries: 25
|
||||
delay: 10
|
||||
|
||||
- name: "Query installed patch after restart"
|
||||
ansible.builtin.include_tasks: rhsso_cli.yml
|
||||
vars:
|
||||
cli_query: "patch info"
|
||||
args:
|
||||
apply:
|
||||
become: "{{ keycloak_rhsso_patch_require_privilege_escalation | default(true) }}"
|
||||
become_user: "{{ keycloak_service_user }}"
|
||||
|
||||
- name: "Verify installed patch version"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- keycloak_patch_version not in keycloak_cli_result.stdout
|
||||
fail_msg: "Patch installation failed"
|
||||
success_msg: "Patch installation successful"
|
||||
|
||||
- name: "Skipping patch"
|
||||
ansible.builtin.debug:
|
||||
msg: "Cumulative patch {{ keycloak_patch_version }} already installed, skipping patch installation."
|
||||
when:
|
||||
- keycloak_cli_result is defined
|
||||
- keycloak_cli_result.stdout is defined
|
||||
- keycloak_patch_version in keycloak_cli_result.stdout
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
- name: "Start service: {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.systemd:
|
||||
name: keycloak
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
become: "{{ keycloak_start_require_privilege_escalation | default(true) }}"
|
||||
|
||||
- name: "Wait until service becomes active: {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.uri:
|
||||
url: "{{ keycloak_config.health_url }}"
|
||||
validate_certs: "{{ keycloak_validate_certs | default(omit) }}"
|
||||
register: keycloak_status
|
||||
until: keycloak_status.status == 200
|
||||
retries: "{{ keycloak_service_start_retries }}"
|
||||
delay: "{{ keycloak_service_start_delay }}"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
- name: "Stop {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.systemd:
|
||||
name: keycloak
|
||||
enabled: true
|
||||
state: stopped
|
||||
become: "{{ keycloak_stop_require_privilege_escalation | default(true) }}"
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
- name: "Configure service script wrapper: {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_systemd_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.template:
|
||||
src: keycloak-service.sh.j2
|
||||
dest: "{{ keycloak_dest }}/keycloak-service.sh"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
notify:
|
||||
- restart keycloak
|
||||
|
||||
- name: "Configure sysconfig file for service: {{ keycloak_config.service_name }}"
|
||||
become: "{{ keycloak_systemd_require_privilege_escalation | default(true) }}"
|
||||
ansible.builtin.template:
|
||||
src: keycloak-sysconfig.j2
|
||||
dest: "{{ keycloak_sysconf_file }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify:
|
||||
- restart keycloak
|
||||
|
||||
- name: "Configure systemd unit file for service: {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.template:
|
||||
src: keycloak.service.j2
|
||||
dest: /etc/systemd/system/keycloak.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
become: "{{ keycloak_systemd_require_privilege_escalation | default(true) }}"
|
||||
register: keycloak_systemdunit
|
||||
notify:
|
||||
- restart keycloak
|
||||
|
||||
- name: "Start and wait for service (first node db): {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.include_tasks: start_keycloak.yml
|
||||
run_once: true
|
||||
when: keycloak_db_enabled
|
||||
|
||||
- name: "Start and wait for service (remaining nodes): {{ keycloak_config.service_name }}"
|
||||
ansible.builtin.include_tasks: start_keycloak.yml
|
||||
|
||||
- name: Check service status
|
||||
ansible.builtin.command: "systemctl status keycloak"
|
||||
register: keycloak_service_status
|
||||
changed_when: false
|
||||
|
||||
- name: Verify service status
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- keycloak_service_status is defined
|
||||
- keycloak_service_status.stdout is defined
|
||||
|
||||
- name: Flush handlers
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
|
@ -0,0 +1,761 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
{{ ansible_managed | comment('xml') }}
|
||||
<server xmlns="urn:jboss:domain:16.0">
|
||||
<extensions>
|
||||
<extension module="org.jboss.as.clustering.infinispan"/>
|
||||
<extension module="org.jboss.as.clustering.jgroups"/>
|
||||
<extension module="org.jboss.as.connector"/>
|
||||
<extension module="org.jboss.as.deployment-scanner"/>
|
||||
<extension module="org.jboss.as.ee"/>
|
||||
<extension module="org.jboss.as.ejb3"/>
|
||||
<extension module="org.jboss.as.jaxrs"/>
|
||||
<extension module="org.jboss.as.jmx"/>
|
||||
<extension module="org.jboss.as.jpa"/>
|
||||
<extension module="org.jboss.as.logging"/>
|
||||
<extension module="org.jboss.as.mail"/>
|
||||
<extension module="org.jboss.as.modcluster"/>
|
||||
<extension module="org.jboss.as.naming"/>
|
||||
<extension module="org.jboss.as.remoting"/>
|
||||
<extension module="org.jboss.as.security"/>
|
||||
<extension module="org.jboss.as.transactions"/>
|
||||
<extension module="org.jboss.as.weld"/>
|
||||
<extension module="org.keycloak.keycloak-server-subsystem"/>
|
||||
<extension module="org.wildfly.extension.bean-validation"/>
|
||||
<extension module="org.wildfly.extension.core-management"/>
|
||||
<extension module="org.wildfly.extension.elytron"/>
|
||||
<extension module="org.wildfly.extension.health"/>
|
||||
<extension module="org.wildfly.extension.io"/>
|
||||
<extension module="org.wildfly.extension.metrics"/>
|
||||
<extension module="org.wildfly.extension.request-controller"/>
|
||||
<extension module="org.wildfly.extension.security.manager"/>
|
||||
<extension module="org.wildfly.extension.undertow"/>
|
||||
</extensions>
|
||||
<management>
|
||||
<security-realms>
|
||||
<security-realm name="ManagementRealm">
|
||||
<authentication>
|
||||
<local default-user="$local" skip-group-loading="true"/>
|
||||
<properties path="mgmt-users.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authentication>
|
||||
<authorization map-groups-to-roles="false">
|
||||
<properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
<security-realm name="ApplicationRealm">
|
||||
<server-identities>
|
||||
<ssl>
|
||||
<keystore path="application.keystore" relative-to="jboss.server.config.dir" keystore-password="password" alias="server" key-password="password" generate-self-signed-certificate-host="localhost"/>
|
||||
</ssl>
|
||||
</server-identities>
|
||||
<authentication>
|
||||
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
|
||||
<properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authentication>
|
||||
<authorization>
|
||||
<properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
</security-realms>
|
||||
<audit-log>
|
||||
<formatters>
|
||||
<json-formatter name="json-formatter"/>
|
||||
</formatters>
|
||||
<handlers>
|
||||
<file-handler name="file" formatter="json-formatter" path="audit-log.log" relative-to="jboss.server.data.dir"/>
|
||||
</handlers>
|
||||
<logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="file"/>
|
||||
</handlers>
|
||||
</logger>
|
||||
</audit-log>
|
||||
<management-interfaces>
|
||||
<http-interface security-realm="ManagementRealm">
|
||||
<http-upgrade enabled="true"/>
|
||||
<socket-binding http="management-http"/>
|
||||
</http-interface>
|
||||
</management-interfaces>
|
||||
<access-control provider="simple">
|
||||
<role-mapping>
|
||||
<role name="SuperUser">
|
||||
<include>
|
||||
<user name="$local"/>
|
||||
</include>
|
||||
</role>
|
||||
</role-mapping>
|
||||
</access-control>
|
||||
</management>
|
||||
<profile>
|
||||
<subsystem xmlns="urn:jboss:domain:logging:8.0">
|
||||
<console-handler name="CONSOLE">
|
||||
<level name="INFO"/>
|
||||
<formatter>
|
||||
<named-formatter name="COLOR-PATTERN"/>
|
||||
</formatter>
|
||||
</console-handler>
|
||||
<periodic-rotating-file-handler name="FILE" autoflush="true">
|
||||
<formatter>
|
||||
<named-formatter name="PATTERN"/>
|
||||
</formatter>
|
||||
<file relative-to="jboss.server.log.dir" path="server.log"/>
|
||||
<suffix value=".yyyy-MM-dd"/>
|
||||
<append value="true"/>
|
||||
</periodic-rotating-file-handler>
|
||||
<logger category="com.arjuna">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="io.jaegertracing.Configuration">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="org.jboss.as.config">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="sun.rmi">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="org.keycloak.cluster.infinispan">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="org.keycloak.connections.infinispan">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="org.keycloak.models.cache.infinispan">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="org.keycloak.models.sessions.infinispan">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<root-logger>
|
||||
<level name="INFO"/>
|
||||
<handlers>
|
||||
<handler name="CONSOLE"/>
|
||||
<handler name="FILE"/>
|
||||
</handlers>
|
||||
</root-logger>
|
||||
<formatter name="PATTERN">
|
||||
<pattern-formatter pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
<formatter name="COLOR-PATTERN">
|
||||
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:bean-validation:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:datasources:6.0">
|
||||
<datasources>
|
||||
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %}
|
||||
<connection-url>{{ keycloak_jdbc[keycloak_jdbc_engine].connection_url }}</connection-url>
|
||||
<driver>{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }}</driver>
|
||||
<pool>
|
||||
<max-pool-size>20</max-pool-size>
|
||||
</pool>
|
||||
<security>
|
||||
<user-name>{{ keycloak_jdbc[keycloak_jdbc_engine].db_user }}</user-name>
|
||||
<password>{{ keycloak_jdbc[keycloak_jdbc_engine].db_password }}</password>
|
||||
</security>
|
||||
{% else %}
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
{% endif %}
|
||||
</datasource>
|
||||
<drivers>
|
||||
{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %}
|
||||
<driver name="{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }}" module="{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }}">
|
||||
<driver-class>{{ keycloak_jdbc[keycloak_jdbc_engine].driver_class }}</driver-class>
|
||||
<xa-datasource-class>{{ keycloak_jdbc[keycloak_jdbc_engine].xa_datasource_class }}</xa-datasource-class>
|
||||
</driver>
|
||||
{% endif %}
|
||||
<driver name="h2" module="com.h2database.h2">
|
||||
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
|
||||
</driver>
|
||||
</drivers>
|
||||
</datasources>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
|
||||
<deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ee:6.0">
|
||||
<spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
|
||||
<concurrent>
|
||||
<context-services>
|
||||
<context-service name="default" jndi-name="java:jboss/ee/concurrency/context/default" use-transaction-setup-provider="true"/>
|
||||
</context-services>
|
||||
<managed-thread-factories>
|
||||
<managed-thread-factory name="default" jndi-name="java:jboss/ee/concurrency/factory/default" context-service="default"/>
|
||||
</managed-thread-factories>
|
||||
<managed-executor-services>
|
||||
<managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-termination-period="0" hung-task-threshold="60000" keepalive-time="5000"/>
|
||||
</managed-executor-services>
|
||||
<managed-scheduled-executor-services>
|
||||
<managed-scheduled-executor-service name="default" jndi-name="java:jboss/ee/concurrency/scheduler/default" context-service="default" hung-task-termination-period="0" hung-task-threshold="60000" keepalive-time="3000"/>
|
||||
</managed-scheduled-executor-services>
|
||||
</concurrent>
|
||||
<default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/ExampleDS" managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default" managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ejb3:9.0">
|
||||
<session-bean>
|
||||
<stateless>
|
||||
<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
|
||||
</stateless>
|
||||
<stateful default-access-timeout="5000" cache-ref="distributable" passivation-disabled-cache-ref="simple"/>
|
||||
<singleton default-access-timeout="5000"/>
|
||||
</session-bean>
|
||||
<pools>
|
||||
<bean-instance-pools>
|
||||
<strict-max-pool name="mdb-strict-max-pool" derive-size="from-cpu-count" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
<strict-max-pool name="slsb-strict-max-pool" derive-size="from-worker-pools" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
</bean-instance-pools>
|
||||
</pools>
|
||||
<caches>
|
||||
<cache name="simple"/>
|
||||
<cache name="distributable" passivation-store-ref="infinispan" aliases="passivating clustered"/>
|
||||
</caches>
|
||||
<passivation-stores>
|
||||
<passivation-store name="infinispan" cache-container="ejb" max-size="10000"/>
|
||||
</passivation-stores>
|
||||
<async thread-pool-name="default"/>
|
||||
<timer-service thread-pool-name="default" default-data-store="default-file-store">
|
||||
<data-stores>
|
||||
<file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
|
||||
</data-stores>
|
||||
</timer-service>
|
||||
<remote cluster="ejb" connectors="http-remoting-connector" thread-pool-name="default">
|
||||
<channel-creation-options>
|
||||
<option name="MAX_OUTBOUND_MESSAGES" value="1234" type="remoting"/>
|
||||
</channel-creation-options>
|
||||
</remote>
|
||||
<thread-pools>
|
||||
<thread-pool name="default">
|
||||
<max-threads count="10"/>
|
||||
<keepalive-time time="60" unit="seconds"/>
|
||||
</thread-pool>
|
||||
</thread-pools>
|
||||
<default-security-domain value="other"/>
|
||||
<default-missing-method-permissions-deny-access value="true"/>
|
||||
<statistics enabled="${wildfly.ejb3.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<log-system-exceptions value="true"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:elytron:13.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
|
||||
<providers>
|
||||
<aggregate-providers name="combined-providers">
|
||||
<providers name="elytron"/>
|
||||
<providers name="openssl"/>
|
||||
</aggregate-providers>
|
||||
<provider-loader name="elytron" module="org.wildfly.security.elytron"/>
|
||||
<provider-loader name="openssl" module="org.wildfly.openssl"/>
|
||||
</providers>
|
||||
<audit-logging>
|
||||
<file-audit-log name="local-audit" path="audit.log" relative-to="jboss.server.log.dir" format="JSON"/>
|
||||
</audit-logging>
|
||||
<security-domains>
|
||||
<security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local"/>
|
||||
</security-domain>
|
||||
<security-domain name="ManagementDomain" default-realm="ManagementRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ManagementRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local" role-mapper="super-user-mapper"/>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
<security-realms>
|
||||
<identity-realm name="local" identity="$local"/>
|
||||
<properties-realm name="ApplicationRealm">
|
||||
<users-properties path="application-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ApplicationRealm"/>
|
||||
<groups-properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
<properties-realm name="ManagementRealm">
|
||||
<users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
|
||||
<groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
</security-realms>
|
||||
<mappers>
|
||||
<simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
|
||||
<permission-mapping>
|
||||
<principal name="anonymous"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
<permission-mapping match-all="true">
|
||||
<permission-set name="login-permission"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
</simple-permission-mapper>
|
||||
<constant-realm-mapper name="local" realm-name="local"/>
|
||||
<simple-role-decoder name="groups-to-roles" attribute="groups"/>
|
||||
<constant-role-mapper name="super-user-mapper">
|
||||
<role name="SuperUser"/>
|
||||
</constant-role-mapper>
|
||||
</mappers>
|
||||
<permission-sets>
|
||||
<permission-set name="login-permission">
|
||||
<permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
|
||||
</permission-set>
|
||||
<permission-set name="default-permissions">
|
||||
<permission class-name="org.wildfly.extension.batch.jberet.deployment.BatchPermission" module="org.wildfly.extension.batch.jberet" target-name="*"/>
|
||||
<permission class-name="org.wildfly.transaction.client.RemoteTransactionPermission" module="org.wildfly.transaction.client"/>
|
||||
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
|
||||
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
|
||||
</permission-set>
|
||||
</permission-sets>
|
||||
<http>
|
||||
<http-authentication-factory name="management-http-authentication" security-domain="ManagementDomain" http-server-mechanism-factory="global">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="DIGEST">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</http-authentication-factory>
|
||||
<provider-http-server-mechanism-factory name="global"/>
|
||||
</http>
|
||||
<sasl>
|
||||
<sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ApplicationRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<configurable-sasl-server-factory name="configured" sasl-server-factory="elytron">
|
||||
<properties>
|
||||
<property name="wildfly.sasl.local-user.default-user" value="$local"/>
|
||||
</properties>
|
||||
</configurable-sasl-server-factory>
|
||||
<mechanism-provider-filtering-sasl-server-factory name="elytron" sasl-server-factory="global">
|
||||
<filters>
|
||||
<filter provider-name="WildFlyElytron"/>
|
||||
</filters>
|
||||
</mechanism-provider-filtering-sasl-server-factory>
|
||||
<provider-sasl-server-factory name="global"/>
|
||||
</sasl>
|
||||
<tls>
|
||||
<key-stores>
|
||||
<key-store name="applicationKS">
|
||||
<credential-reference clear-text="password"/>
|
||||
<implementation type="JKS"/>
|
||||
<file path="application.keystore" relative-to="jboss.server.config.dir"/>
|
||||
</key-store>
|
||||
</key-stores>
|
||||
<key-managers>
|
||||
<key-manager name="applicationKM" key-store="applicationKS" generate-self-signed-certificate-host="localhost">
|
||||
<credential-reference clear-text="password"/>
|
||||
</key-manager>
|
||||
</key-managers>
|
||||
<server-ssl-contexts>
|
||||
<server-ssl-context name="applicationSSC" key-manager="applicationKM"/>
|
||||
</server-ssl-contexts>
|
||||
</tls>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:health:1.0" security-enabled="false"/>
|
||||
<subsystem xmlns="urn:jboss:domain:infinispan:12.0">
|
||||
<cache-container name="ejb" default-cache="dist" aliases="sfsb" modules="org.wildfly.clustering.ejb.infinispan">
|
||||
<transport lock-timeout="60000"/>
|
||||
<distributed-cache name="dist">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store/>
|
||||
</distributed-cache>
|
||||
</cache-container>
|
||||
<cache-container name="keycloak" modules="org.keycloak.keycloak-model-infinispan">
|
||||
<transport lock-timeout="60000"/>
|
||||
<local-cache name="realms">
|
||||
<heap-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="users">
|
||||
<heap-memory size="10000"/>
|
||||
</local-cache>
|
||||
{% for cachename in [ "sessions", "offlineSessions", "clientSessions", "offlineClientSessions", "loginFailures", "actionTokens", "authenticationSessions" ] %}
|
||||
<distributed-cache name="{{ cachename }}">
|
||||
<remote-store cache="{{ cachename }}"
|
||||
remote-servers="remote-cache"
|
||||
passivation="false"
|
||||
fetch-state="false"
|
||||
purge="false"
|
||||
preload="false"
|
||||
shared="true">
|
||||
<property name="rawValues">true</property>
|
||||
<property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
|
||||
<property name="remoteStoreSecurityEnabled">false</property>
|
||||
<property name="infinispan.client.hotrod.auth_username">{{ keycloak_remotecache.username }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_password">{{ keycloak_remotecache.password }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_realm">{{ keycloak_remotecache.realm | default('default') }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_server_name">{{ keycloak_remotecache.server_name }}</property>
|
||||
<property name="infinispan.client.hotrod.sasl_mechanism">{{ keycloak_remotecache.sasl_mechanism }}</property>
|
||||
<property name="infinispan.client.hotrod.use_ssl">{{ keycloak_remotecache.use_ssl }}</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_file_name">{{ keycloak_remotecache.trust_store_path }}</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_type">JKS</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_password">{{ keycloak_remotecache.trust_store_password }}</property>
|
||||
<property name="infinispan.client.hotrod.client_intelligence">TOPOLOGY_AWARE</property>
|
||||
</remote-store>
|
||||
</distributed-cache>
|
||||
{% endfor %}
|
||||
<replicated-cache name="work">
|
||||
<remote-store cache="work"
|
||||
remote-servers="remote-cache"
|
||||
passivation="false"
|
||||
fetch-state="false"
|
||||
purge="false"
|
||||
preload="false"
|
||||
shared="true">
|
||||
<property name="rawValues">true</property>
|
||||
<property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
|
||||
<property name="remoteStoreSecurityEnabled">false</property>
|
||||
<property name="infinispan.client.hotrod.auth_username">{{ keycloak_remotecache.username }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_password">{{ keycloak_remotecache.password }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_realm">{{ keycloak_remotecache.realm | default('default') }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_server_name">{{ keycloak_remotecache.server_name }}</property>
|
||||
<property name="infinispan.client.hotrod.sasl_mechanism">{{ keycloak_remotecache.sasl_mechanism }}</property>
|
||||
<property name="infinispan.client.hotrod.use_ssl">{{ keycloak_remotecache.use_ssl }}</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_file_name">{{ keycloak_remotecache.trust_store_path }}</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_type">JKS</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_password">{{ keycloak_remotecache.trust_store_password }}</property>
|
||||
<property name="infinispan.client.hotrod.client_intelligence">TOPOLOGY_AWARE</property>
|
||||
</remote-store>
|
||||
</replicated-cache>
|
||||
<local-cache name="authorization">
|
||||
<heap-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="keys">
|
||||
<heap-memory size="1000"/>
|
||||
<expiration max-idle="3600000"/>
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<cache-container name="server" default-cache="default" aliases="singleton cluster" modules="org.wildfly.clustering.server">
|
||||
<transport lock-timeout="60000"/>
|
||||
<replicated-cache name="default">
|
||||
<transaction mode="BATCH"/>
|
||||
</replicated-cache>
|
||||
</cache-container>
|
||||
<cache-container name="web" default-cache="dist" modules="org.wildfly.clustering.web.infinispan">
|
||||
<transport lock-timeout="60000"/>
|
||||
<replicated-cache name="sso">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
</replicated-cache>
|
||||
<distributed-cache name="dist">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store/>
|
||||
</distributed-cache>
|
||||
<distributed-cache name="routing"/>
|
||||
</cache-container>
|
||||
<cache-container name="hibernate" modules="org.infinispan.hibernate-cache">
|
||||
<transport lock-timeout="60000"/>
|
||||
<local-cache name="local-query">
|
||||
<heap-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</local-cache>
|
||||
<invalidation-cache name="entity">
|
||||
<transaction mode="NON_XA"/>
|
||||
<heap-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</invalidation-cache>
|
||||
<replicated-cache name="timestamps"/>
|
||||
</cache-container>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:io:3.0">
|
||||
<worker name="default"/>
|
||||
<buffer-pool name="default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jaxrs:2.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:jca:5.0">
|
||||
<archive-validation enabled="true" fail-on-error="true" fail-on-warn="false"/>
|
||||
<bean-validation enabled="true"/>
|
||||
<default-workmanager>
|
||||
<short-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</short-running-threads>
|
||||
<long-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</long-running-threads>
|
||||
</default-workmanager>
|
||||
<cached-connection-manager/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jgroups:8.0">
|
||||
<channels default="ee">
|
||||
<channel name="ee" stack="tcp" cluster="ejb"/>
|
||||
</channels>
|
||||
<stacks>
|
||||
<stack name="tcp">
|
||||
<transport site="${jboss.node.name}" type="TCP" socket-binding="jgroups-tcp"/>
|
||||
{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %}
|
||||
<protocol type="JDBC_PING">
|
||||
<property name="datasource_jndi_name">java:jboss/datasources/KeycloakDS</property>
|
||||
<property name="initialize_sql">{{ keycloak_jdbc[keycloak_jdbc_engine].initialize_db }}</property>
|
||||
<property name="insert_single_sql">INSERT INTO JGROUPSPING (own_addr, cluster_name, ping_data) values (?, ?, ?)</property>
|
||||
<property name="delete_single_sql">DELETE FROM JGROUPSPING WHERE own_addr=? AND cluster_name=?</property>
|
||||
<property name="select_all_pingdata_sql">SELECT ping_data FROM JGROUPSPING WHERE cluster_name=?</property>
|
||||
</protocol>
|
||||
{% endif %}
|
||||
<protocol type="MERGE3"/>
|
||||
<protocol type="FD_SOCK"/>
|
||||
<protocol type="FD_ALL"/>
|
||||
<protocol type="VERIFY_SUSPECT"/>
|
||||
<protocol type="pbcast.NAKACK2"/>
|
||||
<protocol type="UNICAST3"/>
|
||||
<protocol type="pbcast.STABLE"/>
|
||||
<protocol type="pbcast.GMS">
|
||||
<property name="join_timeout">30000</property>
|
||||
</protocol>
|
||||
<protocol type="MFC"/>
|
||||
<protocol type="FRAG2"/>
|
||||
</stack>
|
||||
</stacks>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jmx:1.3">
|
||||
<expose-resolved-model/>
|
||||
<expose-expression-model/>
|
||||
<remoting-connector/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jpa:1.1">
|
||||
<jpa default-extended-persistence-inheritance="DEEP"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
|
||||
<web-context>auth</web-context>
|
||||
<providers>
|
||||
<provider>
|
||||
classpath:${jboss.home.dir}/providers/*
|
||||
</provider>
|
||||
</providers>
|
||||
<master-realm-name>master</master-realm-name>
|
||||
<scheduled-task-interval>900</scheduled-task-interval>
|
||||
<theme>
|
||||
<staticMaxAge>2592000</staticMaxAge>
|
||||
<cacheThemes>true</cacheThemes>
|
||||
<cacheTemplates>true</cacheTemplates>
|
||||
<dir>${jboss.home.dir}/themes</dir>
|
||||
</theme>
|
||||
{% if keycloak_ha_enabled %}
|
||||
<spi name="dblock">
|
||||
<provider name="jpa" enabled="true">
|
||||
<properties>
|
||||
<property name="lockWaitTimeout" value="900"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
{% endif %}
|
||||
<spi name="eventsStore">
|
||||
<provider name="jpa" enabled="true">
|
||||
<properties>
|
||||
<property name="exclude-events" value="["REFRESH_TOKEN"]"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="userCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="userSessionPersister">
|
||||
<default-provider>jpa</default-provider>
|
||||
</spi>
|
||||
<spi name="timer">
|
||||
<default-provider>basic</default-provider>
|
||||
</spi>
|
||||
<spi name="connectionsHttpClient">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsJpa">
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="dataSource" value="java:jboss/datasources/KeycloakDS"/>
|
||||
<property name="initializeEmpty" value="true"/>
|
||||
<property name="migrationStrategy" value="update"/>
|
||||
<property name="migrationExport" value="${jboss.home.dir}/keycloak-database-update.sql"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="realmCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsInfinispan">
|
||||
<default-provider>default</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="cacheContainer" value="java:jboss/infinispan/container/keycloak"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="jta-lookup">
|
||||
<default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>
|
||||
<provider name="jboss" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="publicKeyStorage">
|
||||
<provider name="infinispan" enabled="true">
|
||||
<properties>
|
||||
<property name="minTimeBetweenRequests" value="10"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="x509cert-lookup">
|
||||
<default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="hostname">
|
||||
<default-provider>default</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="frontendUrl" value="{{ keycloak_modcluster.frontend_url }}"/>
|
||||
<property name="forceBackendUrlToFrontendUrl" value="true"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:mail:4.0">
|
||||
<mail-session name="default" jndi-name="java:jboss/mail/Default">
|
||||
<smtp-server outbound-socket-binding-ref="mail-smtp"/>
|
||||
</mail-session>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:metrics:1.0" security-enabled="false" exposed-subsystems="*" prefix="${wildfly.metrics.prefix:jboss}"/>
|
||||
{% if keycloak_modcluster.enabled %}
|
||||
<subsystem xmlns="urn:jboss:domain:modcluster:5.0">
|
||||
<proxy name="default" advertise="false" listener="ajp" proxies="proxy1">
|
||||
<dynamic-load-provider>
|
||||
<load-metric type="cpu"/>
|
||||
</dynamic-load-provider>
|
||||
</proxy>
|
||||
</subsystem>
|
||||
{% endif %}
|
||||
<subsystem xmlns="urn:jboss:domain:naming:2.0">
|
||||
<remote-naming/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:remoting:4.0">
|
||||
<http-connector name="http-remoting-connector" connector-ref="default" security-realm="ApplicationRealm"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:request-controller:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:security:2.0">
|
||||
<security-domains>
|
||||
<security-domain name="other" cache-type="default">
|
||||
<authentication>
|
||||
<login-module code="Remoting" flag="optional">
|
||||
<module-option name="password-stacking" value="useFirstPass"/>
|
||||
</login-module>
|
||||
<login-module code="RealmDirect" flag="required">
|
||||
<module-option name="password-stacking" value="useFirstPass"/>
|
||||
</login-module>
|
||||
</authentication>
|
||||
</security-domain>
|
||||
<security-domain name="jboss-web-policy" cache-type="default">
|
||||
<authorization>
|
||||
<policy-module code="Delegating" flag="required"/>
|
||||
</authorization>
|
||||
</security-domain>
|
||||
<security-domain name="jaspitest" cache-type="default">
|
||||
<authentication-jaspi>
|
||||
<login-module-stack name="dummy">
|
||||
<login-module code="Dummy" flag="optional"/>
|
||||
</login-module-stack>
|
||||
<auth-module code="Dummy"/>
|
||||
</authentication-jaspi>
|
||||
</security-domain>
|
||||
<security-domain name="jboss-ejb-policy" cache-type="default">
|
||||
<authorization>
|
||||
<policy-module code="Delegating" flag="required"/>
|
||||
</authorization>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:security-manager:1.0">
|
||||
<deployment-permissions>
|
||||
<maximum-set>
|
||||
<permission class="java.security.AllPermission"/>
|
||||
</maximum-set>
|
||||
</deployment-permissions>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:transactions:6.0">
|
||||
<core-environment node-identifier="{{ inventory_hostname | default('${jboss.tx.node.id:1}') }}">
|
||||
<process-id>
|
||||
<uuid/>
|
||||
</process-id>
|
||||
</core-environment>
|
||||
<recovery-environment socket-binding="txn-recovery-environment" status-socket-binding="txn-status-manager"/>
|
||||
<coordinator-environment statistics-enabled="${wildfly.transactions.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<object-store path="tx-object-store" relative-to="jboss.server.data.dir"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:undertow:12.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<buffer-cache name="default"/>
|
||||
<server name="default-server">
|
||||
<ajp-listener name="ajp" socket-binding="ajp"/>
|
||||
<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true" proxy-address-forwarding="true"/>
|
||||
<https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true"/>
|
||||
<host name="default-host" alias="localhost">
|
||||
<location name="/" handler="welcome-content"/>
|
||||
<http-invoker security-realm="ApplicationRealm"/>
|
||||
<filter-ref name="proxy-peer"/>
|
||||
</host>
|
||||
</server>
|
||||
<servlet-container name="default">
|
||||
<jsp-config/>
|
||||
<websockets/>
|
||||
</servlet-container>
|
||||
<handlers>
|
||||
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
|
||||
</handlers>
|
||||
<filters>
|
||||
<filter name="proxy-peer" module="io.undertow.core"
|
||||
class-name="io.undertow.server.handlers.ProxyPeerAddressHandler"/>
|
||||
</filters>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:weld:4.0"/>
|
||||
</profile>
|
||||
<interfaces>
|
||||
<interface name="management">
|
||||
<inet-address value="{{ keycloak_management_port_bind_address }}"/>
|
||||
</interface>
|
||||
<interface name="jgroups">
|
||||
{% if ansible_default_ipv4 is defined %}
|
||||
<subnet-match value="{{ (ansible_default_ipv4.network + '/' + ansible_default_ipv4.netmask) | ansible.utils.ipaddr('net') }}"/>
|
||||
{% else %}
|
||||
<any-address />
|
||||
{% endif %}
|
||||
</interface>
|
||||
<interface name="public">
|
||||
<inet-address value="{{ keycloak_bind_address }}"/>
|
||||
</interface>
|
||||
</interfaces>
|
||||
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:{{ keycloak_jboss_port_offset }}}">
|
||||
<socket-binding name="ajp" port="{{ keycloak_ajp_port }}"/>
|
||||
<socket-binding name="http" port="{{ keycloak_http_port }}"/>
|
||||
<socket-binding name="https" port="{{ keycloak_https_port }}"/>
|
||||
<socket-binding name="management-http" interface="management" port="{{ keycloak_management_http_port }}"/>
|
||||
<socket-binding name="management-https" interface="management" port="{{ keycloak_management_https_port }}"/>
|
||||
<socket-binding name="jgroups-tcp" interface="jgroups" port="{{ keycloak_jgroups_port }}"/>
|
||||
<socket-binding name="txn-recovery-environment" port="4712"/>
|
||||
<socket-binding name="txn-status-manager" port="4713"/>
|
||||
<outbound-socket-binding name="mail-smtp">
|
||||
<remote-destination host="${jboss.mail.server.host:localhost}" port="${jboss.mail.server.port:25}"/>
|
||||
</outbound-socket-binding>
|
||||
{% if keycloak_modcluster.enabled %}
|
||||
<outbound-socket-binding name="proxy1">
|
||||
<remote-destination host="{{ keycloak_modcluster.reverse_proxy_url | default('localhost') }}" port="6666"/>
|
||||
</outbound-socket-binding>
|
||||
{% endif %}
|
||||
<outbound-socket-binding name="remote-cache">
|
||||
<remote-destination host="{{ keycloak_remotecache.server_name | default('localhost') }}" port="${remote.cache.port:11222}"/>
|
||||
</outbound-socket-binding>
|
||||
</socket-binding-group>
|
||||
</server>
|
||||
|
|
@ -0,0 +1,658 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
{{ ansible_managed | comment('xml') }}
|
||||
<server xmlns="urn:jboss:domain:16.0">
|
||||
<extensions>
|
||||
<extension module="org.jboss.as.clustering.infinispan"/>
|
||||
<extension module="org.jboss.as.connector"/>
|
||||
<extension module="org.jboss.as.deployment-scanner"/>
|
||||
<extension module="org.jboss.as.ee"/>
|
||||
<extension module="org.jboss.as.ejb3"/>
|
||||
<extension module="org.jboss.as.jaxrs"/>
|
||||
<extension module="org.jboss.as.jmx"/>
|
||||
<extension module="org.jboss.as.jpa"/>
|
||||
<extension module="org.jboss.as.logging"/>
|
||||
<extension module="org.jboss.as.mail"/>
|
||||
<extension module="org.jboss.as.modcluster"/>
|
||||
<extension module="org.jboss.as.naming"/>
|
||||
<extension module="org.jboss.as.remoting"/>
|
||||
<extension module="org.jboss.as.security"/>
|
||||
<extension module="org.jboss.as.transactions"/>
|
||||
<extension module="org.jboss.as.weld"/>
|
||||
<extension module="org.keycloak.keycloak-server-subsystem"/>
|
||||
<extension module="org.wildfly.extension.bean-validation"/>
|
||||
<extension module="org.wildfly.extension.core-management"/>
|
||||
<extension module="org.wildfly.extension.elytron"/>
|
||||
<extension module="org.wildfly.extension.health"/>
|
||||
<extension module="org.wildfly.extension.io"/>
|
||||
<extension module="org.wildfly.extension.metrics"/>
|
||||
<extension module="org.wildfly.extension.request-controller"/>
|
||||
<extension module="org.wildfly.extension.security.manager"/>
|
||||
<extension module="org.wildfly.extension.undertow"/>
|
||||
</extensions>
|
||||
<management>
|
||||
<security-realms>
|
||||
<security-realm name="ManagementRealm">
|
||||
<authentication>
|
||||
<local default-user="$local" skip-group-loading="true"/>
|
||||
<properties path="mgmt-users.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authentication>
|
||||
<authorization map-groups-to-roles="false">
|
||||
<properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
<security-realm name="ApplicationRealm">
|
||||
<server-identities>
|
||||
<ssl>
|
||||
<keystore path="application.keystore" relative-to="jboss.server.config.dir" keystore-password="password" alias="server" key-password="password" generate-self-signed-certificate-host="localhost"/>
|
||||
</ssl>
|
||||
</server-identities>
|
||||
<authentication>
|
||||
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
|
||||
<properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authentication>
|
||||
<authorization>
|
||||
<properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
</security-realms>
|
||||
<audit-log>
|
||||
<formatters>
|
||||
<json-formatter name="json-formatter"/>
|
||||
</formatters>
|
||||
<handlers>
|
||||
<file-handler name="file" formatter="json-formatter" path="audit-log.log" relative-to="jboss.server.data.dir"/>
|
||||
</handlers>
|
||||
<logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="file"/>
|
||||
</handlers>
|
||||
</logger>
|
||||
</audit-log>
|
||||
<management-interfaces>
|
||||
<http-interface security-realm="ManagementRealm">
|
||||
<http-upgrade enabled="true"/>
|
||||
<socket-binding http="management-http"/>
|
||||
</http-interface>
|
||||
</management-interfaces>
|
||||
<access-control provider="simple">
|
||||
<role-mapping>
|
||||
<role name="SuperUser">
|
||||
<include>
|
||||
<user name="$local"/>
|
||||
</include>
|
||||
</role>
|
||||
</role-mapping>
|
||||
</access-control>
|
||||
</management>
|
||||
<profile>
|
||||
<subsystem xmlns="urn:jboss:domain:logging:8.0">
|
||||
<console-handler name="CONSOLE">
|
||||
<level name="INFO"/>
|
||||
<formatter>
|
||||
<named-formatter name="COLOR-PATTERN"/>
|
||||
</formatter>
|
||||
</console-handler>
|
||||
<periodic-rotating-file-handler name="FILE" autoflush="true">
|
||||
<formatter>
|
||||
<named-formatter name="PATTERN"/>
|
||||
</formatter>
|
||||
<file relative-to="jboss.server.log.dir" path="server.log"/>
|
||||
<suffix value=".yyyy-MM-dd"/>
|
||||
<append value="true"/>
|
||||
</periodic-rotating-file-handler>
|
||||
<logger category="com.arjuna">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="io.jaegertracing.Configuration">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="org.jboss.as.config">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="sun.rmi">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<root-logger>
|
||||
<level name="INFO"/>
|
||||
<handlers>
|
||||
<handler name="CONSOLE"/>
|
||||
<handler name="FILE"/>
|
||||
</handlers>
|
||||
</root-logger>
|
||||
<formatter name="PATTERN">
|
||||
<pattern-formatter pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
<formatter name="COLOR-PATTERN">
|
||||
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:bean-validation:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:datasources:6.0">
|
||||
<datasources>
|
||||
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %}
|
||||
<connection-url>{{ keycloak_jdbc[keycloak_jdbc_engine].connection_url }}</connection-url>
|
||||
<driver>{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }}</driver>
|
||||
<pool>
|
||||
<max-pool-size>20</max-pool-size>
|
||||
</pool>
|
||||
<security>
|
||||
<user-name>{{ keycloak_jdbc[keycloak_jdbc_engine].db_user }}</user-name>
|
||||
<password>{{ keycloak_jdbc[keycloak_jdbc_engine].db_password }}</password>
|
||||
</security>
|
||||
{% else %}
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
{% endif %}
|
||||
</datasource>
|
||||
<drivers>
|
||||
{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %}
|
||||
<driver name="{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }}" module="{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }}">
|
||||
<driver-class>{{ keycloak_jdbc[keycloak_jdbc_engine].driver_class }}</driver-class>
|
||||
<xa-datasource-class>{{ keycloak_jdbc[keycloak_jdbc_engine].xa_datasource_class }}</xa-datasource-class>
|
||||
</driver>
|
||||
{% endif %}
|
||||
<driver name="h2" module="com.h2database.h2">
|
||||
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
|
||||
</driver>
|
||||
</drivers>
|
||||
</datasources>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
|
||||
<deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ee:6.0">
|
||||
<spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
|
||||
<concurrent>
|
||||
<context-services>
|
||||
<context-service name="default" jndi-name="java:jboss/ee/concurrency/context/default" use-transaction-setup-provider="true"/>
|
||||
</context-services>
|
||||
<managed-thread-factories>
|
||||
<managed-thread-factory name="default" jndi-name="java:jboss/ee/concurrency/factory/default" context-service="default"/>
|
||||
</managed-thread-factories>
|
||||
<managed-executor-services>
|
||||
<managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-termination-period="0" hung-task-threshold="60000" keepalive-time="5000"/>
|
||||
</managed-executor-services>
|
||||
<managed-scheduled-executor-services>
|
||||
<managed-scheduled-executor-service name="default" jndi-name="java:jboss/ee/concurrency/scheduler/default" context-service="default" hung-task-termination-period="0" hung-task-threshold="60000" keepalive-time="3000"/>
|
||||
</managed-scheduled-executor-services>
|
||||
</concurrent>
|
||||
<default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/ExampleDS" managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default" managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ejb3:9.0">
|
||||
<session-bean>
|
||||
<stateless>
|
||||
<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
|
||||
</stateless>
|
||||
<stateful default-access-timeout="5000" cache-ref="simple" passivation-disabled-cache-ref="simple"/>
|
||||
<singleton default-access-timeout="5000"/>
|
||||
</session-bean>
|
||||
<pools>
|
||||
<bean-instance-pools>
|
||||
<strict-max-pool name="mdb-strict-max-pool" derive-size="from-cpu-count" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
<strict-max-pool name="slsb-strict-max-pool" derive-size="from-worker-pools" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
</bean-instance-pools>
|
||||
</pools>
|
||||
<caches>
|
||||
<cache name="simple"/>
|
||||
<cache name="distributable" passivation-store-ref="infinispan" aliases="passivating clustered"/>
|
||||
</caches>
|
||||
<passivation-stores>
|
||||
<passivation-store name="infinispan" cache-container="ejb" max-size="10000"/>
|
||||
</passivation-stores>
|
||||
<async thread-pool-name="default"/>
|
||||
<timer-service thread-pool-name="default" default-data-store="default-file-store">
|
||||
<data-stores>
|
||||
<file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
|
||||
</data-stores>
|
||||
</timer-service>
|
||||
<remote cluster="ejb" connectors="http-remoting-connector" thread-pool-name="default">
|
||||
<channel-creation-options>
|
||||
<option name="MAX_OUTBOUND_MESSAGES" value="1234" type="remoting"/>
|
||||
</channel-creation-options>
|
||||
</remote>
|
||||
<thread-pools>
|
||||
<thread-pool name="default">
|
||||
<max-threads count="10"/>
|
||||
<keepalive-time time="60" unit="seconds"/>
|
||||
</thread-pool>
|
||||
</thread-pools>
|
||||
<default-security-domain value="other"/>
|
||||
<default-missing-method-permissions-deny-access value="true"/>
|
||||
<statistics enabled="${wildfly.ejb3.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<log-system-exceptions value="true"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:elytron:13.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
|
||||
<providers>
|
||||
<aggregate-providers name="combined-providers">
|
||||
<providers name="elytron"/>
|
||||
<providers name="openssl"/>
|
||||
</aggregate-providers>
|
||||
<provider-loader name="elytron" module="org.wildfly.security.elytron"/>
|
||||
<provider-loader name="openssl" module="org.wildfly.openssl"/>
|
||||
</providers>
|
||||
<audit-logging>
|
||||
<file-audit-log name="local-audit" path="audit.log" relative-to="jboss.server.log.dir" format="JSON"/>
|
||||
</audit-logging>
|
||||
<security-domains>
|
||||
<security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local"/>
|
||||
</security-domain>
|
||||
<security-domain name="ManagementDomain" default-realm="ManagementRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ManagementRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local" role-mapper="super-user-mapper"/>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
<security-realms>
|
||||
<identity-realm name="local" identity="$local"/>
|
||||
<properties-realm name="ApplicationRealm">
|
||||
<users-properties path="application-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ApplicationRealm"/>
|
||||
<groups-properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
<properties-realm name="ManagementRealm">
|
||||
<users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
|
||||
<groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
</security-realms>
|
||||
<mappers>
|
||||
<simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
|
||||
<permission-mapping>
|
||||
<principal name="anonymous"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
<permission-mapping match-all="true">
|
||||
<permission-set name="login-permission"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
</simple-permission-mapper>
|
||||
<constant-realm-mapper name="local" realm-name="local"/>
|
||||
<simple-role-decoder name="groups-to-roles" attribute="groups"/>
|
||||
<constant-role-mapper name="super-user-mapper">
|
||||
<role name="SuperUser"/>
|
||||
</constant-role-mapper>
|
||||
</mappers>
|
||||
<permission-sets>
|
||||
<permission-set name="login-permission">
|
||||
<permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
|
||||
</permission-set>
|
||||
<permission-set name="default-permissions">
|
||||
<permission class-name="org.wildfly.extension.batch.jberet.deployment.BatchPermission" module="org.wildfly.extension.batch.jberet" target-name="*"/>
|
||||
<permission class-name="org.wildfly.transaction.client.RemoteTransactionPermission" module="org.wildfly.transaction.client"/>
|
||||
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
|
||||
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
|
||||
</permission-set>
|
||||
</permission-sets>
|
||||
<http>
|
||||
<http-authentication-factory name="management-http-authentication" security-domain="ManagementDomain" http-server-mechanism-factory="global">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="DIGEST">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</http-authentication-factory>
|
||||
<provider-http-server-mechanism-factory name="global"/>
|
||||
</http>
|
||||
<sasl>
|
||||
<sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ApplicationRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<configurable-sasl-server-factory name="configured" sasl-server-factory="elytron">
|
||||
<properties>
|
||||
<property name="wildfly.sasl.local-user.default-user" value="$local"/>
|
||||
</properties>
|
||||
</configurable-sasl-server-factory>
|
||||
<mechanism-provider-filtering-sasl-server-factory name="elytron" sasl-server-factory="global">
|
||||
<filters>
|
||||
<filter provider-name="WildFlyElytron"/>
|
||||
</filters>
|
||||
</mechanism-provider-filtering-sasl-server-factory>
|
||||
<provider-sasl-server-factory name="global"/>
|
||||
</sasl>
|
||||
<tls>
|
||||
<key-stores>
|
||||
<key-store name="applicationKS">
|
||||
<credential-reference clear-text="password"/>
|
||||
<implementation type="JKS"/>
|
||||
<file path="application.keystore" relative-to="jboss.server.config.dir"/>
|
||||
</key-store>
|
||||
</key-stores>
|
||||
<key-managers>
|
||||
<key-manager name="applicationKM" key-store="applicationKS" generate-self-signed-certificate-host="localhost">
|
||||
<credential-reference clear-text="password"/>
|
||||
</key-manager>
|
||||
</key-managers>
|
||||
<server-ssl-contexts>
|
||||
<server-ssl-context name="applicationSSC" key-manager="applicationKM"/>
|
||||
</server-ssl-contexts>
|
||||
</tls>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:health:1.0" security-enabled="false"/>
|
||||
<subsystem xmlns="urn:jboss:domain:infinispan:12.0">
|
||||
<cache-container name="ejb" default-cache="passivation" aliases="sfsb" modules="org.wildfly.clustering.ejb.infinispan">
|
||||
<local-cache name="passivation">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store passivation="true" purge="false"/>
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<cache-container name="keycloak" modules="org.keycloak.keycloak-model-infinispan">
|
||||
<local-cache name="realms">
|
||||
<heap-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="users">
|
||||
<heap-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="sessions"/>
|
||||
<local-cache name="authenticationSessions"/>
|
||||
<local-cache name="offlineSessions"/>
|
||||
<local-cache name="clientSessions"/>
|
||||
<local-cache name="offlineClientSessions"/>
|
||||
<local-cache name="loginFailures"/>
|
||||
<local-cache name="work"/>
|
||||
<local-cache name="authorization">
|
||||
<heap-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="keys">
|
||||
<heap-memory size="1000"/>
|
||||
<expiration max-idle="3600000"/>
|
||||
</local-cache>
|
||||
<local-cache name="actionTokens">
|
||||
<heap-memory size="-1"/>
|
||||
<expiration interval="300000" max-idle="-1"/>
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<cache-container name="server" default-cache="default" modules="org.wildfly.clustering.server">
|
||||
<local-cache name="default">
|
||||
<transaction mode="BATCH"/>
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<cache-container name="web" default-cache="passivation" modules="org.wildfly.clustering.web.infinispan">
|
||||
<local-cache name="passivation">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store passivation="true" purge="false"/>
|
||||
</local-cache>
|
||||
<local-cache name="sso">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
</local-cache>
|
||||
<local-cache name="routing"/>
|
||||
</cache-container>
|
||||
<cache-container name="hibernate" modules="org.infinispan.hibernate-cache">
|
||||
<local-cache name="entity">
|
||||
<heap-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</local-cache>
|
||||
<local-cache name="local-query">
|
||||
<heap-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</local-cache>
|
||||
<local-cache name="timestamps"/>
|
||||
</cache-container>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:io:3.0">
|
||||
<worker name="default"/>
|
||||
<buffer-pool name="default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jaxrs:2.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:jca:5.0">
|
||||
<archive-validation enabled="true" fail-on-error="true" fail-on-warn="false"/>
|
||||
<bean-validation enabled="true"/>
|
||||
<default-workmanager>
|
||||
<short-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</short-running-threads>
|
||||
<long-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</long-running-threads>
|
||||
</default-workmanager>
|
||||
<cached-connection-manager/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jmx:1.3">
|
||||
<expose-resolved-model/>
|
||||
<expose-expression-model/>
|
||||
<remoting-connector/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jpa:1.1">
|
||||
<jpa default-extended-persistence-inheritance="DEEP"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
|
||||
<web-context>auth</web-context>
|
||||
<providers>
|
||||
<provider>
|
||||
classpath:${jboss.home.dir}/providers/*
|
||||
</provider>
|
||||
</providers>
|
||||
<master-realm-name>master</master-realm-name>
|
||||
<scheduled-task-interval>900</scheduled-task-interval>
|
||||
<theme>
|
||||
<staticMaxAge>2592000</staticMaxAge>
|
||||
<cacheThemes>true</cacheThemes>
|
||||
<cacheTemplates>true</cacheTemplates>
|
||||
<dir>${jboss.home.dir}/themes</dir>
|
||||
</theme>
|
||||
{% if keycloak_ha_enabled %}
|
||||
<spi name="dblock">
|
||||
<provider name="jpa" enabled="true">
|
||||
<properties>
|
||||
<property name="lockWaitTimeout" value="900"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
{% endif %}
|
||||
<spi name="eventsStore">
|
||||
<provider name="jpa" enabled="true">
|
||||
<properties>
|
||||
<property name="exclude-events" value="["REFRESH_TOKEN"]"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="userCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="userSessionPersister">
|
||||
<default-provider>jpa</default-provider>
|
||||
</spi>
|
||||
<spi name="timer">
|
||||
<default-provider>basic</default-provider>
|
||||
</spi>
|
||||
<spi name="connectionsHttpClient">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsJpa">
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="dataSource" value="java:jboss/datasources/KeycloakDS"/>
|
||||
<property name="initializeEmpty" value="true"/>
|
||||
<property name="migrationStrategy" value="update"/>
|
||||
<property name="migrationExport" value="${jboss.home.dir}/keycloak-database-update.sql"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="realmCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsInfinispan">
|
||||
<default-provider>default</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="cacheContainer" value="java:jboss/infinispan/container/keycloak"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="jta-lookup">
|
||||
<default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>
|
||||
<provider name="jboss" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="publicKeyStorage">
|
||||
<provider name="infinispan" enabled="true">
|
||||
<properties>
|
||||
<property name="minTimeBetweenRequests" value="10"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="x509cert-lookup">
|
||||
<default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="hostname">
|
||||
<default-provider>default</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="frontendUrl" value="{{ keycloak_modcluster.frontend_url }}"/>
|
||||
<property name="forceBackendUrlToFrontendUrl" value="true"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:mail:4.0">
|
||||
<mail-session name="default" jndi-name="java:jboss/mail/Default">
|
||||
<smtp-server outbound-socket-binding-ref="mail-smtp"/>
|
||||
</mail-session>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:metrics:1.0" security-enabled="false" exposed-subsystems="*" prefix="${wildfly.metrics.prefix:jboss}"/>
|
||||
{% if keycloak_modcluster.enabled %}
|
||||
<subsystem xmlns="urn:jboss:domain:modcluster:5.0">
|
||||
<proxy name="default" advertise="false" listener="ajp" proxies="proxy1">
|
||||
<dynamic-load-provider>
|
||||
<load-metric type="cpu"/>
|
||||
</dynamic-load-provider>
|
||||
</proxy>
|
||||
</subsystem>
|
||||
{% endif %}
|
||||
<subsystem xmlns="urn:jboss:domain:naming:2.0">
|
||||
<remote-naming/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:remoting:4.0">
|
||||
<http-connector name="http-remoting-connector" connector-ref="default" security-realm="ApplicationRealm"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:request-controller:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:security:2.0">
|
||||
<security-domains>
|
||||
<security-domain name="other" cache-type="default">
|
||||
<authentication>
|
||||
<login-module code="Remoting" flag="optional">
|
||||
<module-option name="password-stacking" value="useFirstPass"/>
|
||||
</login-module>
|
||||
<login-module code="RealmDirect" flag="required">
|
||||
<module-option name="password-stacking" value="useFirstPass"/>
|
||||
</login-module>
|
||||
</authentication>
|
||||
</security-domain>
|
||||
<security-domain name="jboss-web-policy" cache-type="default">
|
||||
<authorization>
|
||||
<policy-module code="Delegating" flag="required"/>
|
||||
</authorization>
|
||||
</security-domain>
|
||||
<security-domain name="jaspitest" cache-type="default">
|
||||
<authentication-jaspi>
|
||||
<login-module-stack name="dummy">
|
||||
<login-module code="Dummy" flag="optional"/>
|
||||
</login-module-stack>
|
||||
<auth-module code="Dummy"/>
|
||||
</authentication-jaspi>
|
||||
</security-domain>
|
||||
<security-domain name="jboss-ejb-policy" cache-type="default">
|
||||
<authorization>
|
||||
<policy-module code="Delegating" flag="required"/>
|
||||
</authorization>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:security-manager:1.0">
|
||||
<deployment-permissions>
|
||||
<maximum-set>
|
||||
<permission class="java.security.AllPermission"/>
|
||||
</maximum-set>
|
||||
</deployment-permissions>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:transactions:6.0">
|
||||
<core-environment node-identifier="${jboss.tx.node.id:1}">
|
||||
<process-id>
|
||||
<uuid/>
|
||||
</process-id>
|
||||
</core-environment>
|
||||
<recovery-environment socket-binding="txn-recovery-environment" status-socket-binding="txn-status-manager"/>
|
||||
<coordinator-environment statistics-enabled="${wildfly.transactions.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<object-store path="tx-object-store" relative-to="jboss.server.data.dir"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:undertow:12.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<buffer-cache name="default"/>
|
||||
<server name="default-server">
|
||||
<ajp-listener name="ajp" socket-binding="ajp"/>
|
||||
<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"/>
|
||||
<https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true"/>
|
||||
<host name="default-host" alias="localhost">
|
||||
<location name="/" handler="welcome-content"/>
|
||||
<http-invoker security-realm="ApplicationRealm"/>
|
||||
</host>
|
||||
</server>
|
||||
<servlet-container name="default">
|
||||
<jsp-config/>
|
||||
<websockets/>
|
||||
</servlet-container>
|
||||
<handlers>
|
||||
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
|
||||
</handlers>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:weld:4.0"/>
|
||||
</profile>
|
||||
<interfaces>
|
||||
<interface name="management">
|
||||
<inet-address value="{{ keycloak_management_port_bind_address }}"/>
|
||||
</interface>
|
||||
<interface name="public">
|
||||
<inet-address value="{{ keycloak_bind_address }}"/>
|
||||
</interface>
|
||||
</interfaces>
|
||||
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:{{ keycloak_jboss_port_offset }}}">
|
||||
<socket-binding name="ajp" port="{{ keycloak_ajp_port }}"/>
|
||||
<socket-binding name="http" port="{{ keycloak_http_port }}"/>
|
||||
<socket-binding name="https" port="{{ keycloak_https_port }}"/>
|
||||
<socket-binding name="management-http" interface="management" port="{{ keycloak_management_http_port }}"/>
|
||||
<socket-binding name="management-https" interface="management" port="{{ keycloak_management_https_port }}"/>
|
||||
<socket-binding name="txn-recovery-environment" port="4712"/>
|
||||
<socket-binding name="txn-status-manager" port="4713"/>
|
||||
<outbound-socket-binding name="mail-smtp">
|
||||
<remote-destination host="${jboss.mail.server.host:localhost}" port="${jboss.mail.server.port:25}"/>
|
||||
</outbound-socket-binding>
|
||||
{% if keycloak_modcluster.enabled %}
|
||||
<outbound-socket-binding name="proxy1">
|
||||
<remote-destination host="{{ keycloak_modcluster.reverse_proxy_url | default('localhost') }}" port="6666"/>
|
||||
</outbound-socket-binding>
|
||||
{% endif %}
|
||||
</socket-binding-group>
|
||||
</server>
|
||||
|
|
@ -0,0 +1,759 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
|
||||
<server xmlns="urn:jboss:domain:10.0">
|
||||
<extensions>
|
||||
<extension module="org.jboss.as.clustering.infinispan"/>
|
||||
<extension module="org.jboss.as.clustering.jgroups"/>
|
||||
<extension module="org.jboss.as.connector"/>
|
||||
<extension module="org.jboss.as.deployment-scanner"/>
|
||||
<extension module="org.jboss.as.ee"/>
|
||||
<extension module="org.jboss.as.ejb3"/>
|
||||
<extension module="org.jboss.as.jaxrs"/>
|
||||
<extension module="org.jboss.as.jmx"/>
|
||||
<extension module="org.jboss.as.jpa"/>
|
||||
<extension module="org.jboss.as.logging"/>
|
||||
<extension module="org.jboss.as.mail"/>
|
||||
<extension module="org.jboss.as.modcluster"/>
|
||||
<extension module="org.jboss.as.naming"/>
|
||||
<extension module="org.jboss.as.remoting"/>
|
||||
<extension module="org.jboss.as.security"/>
|
||||
<extension module="org.jboss.as.transactions"/>
|
||||
<extension module="org.jboss.as.weld"/>
|
||||
<extension module="org.keycloak.keycloak-server-subsystem"/>
|
||||
<extension module="org.wildfly.extension.bean-validation"/>
|
||||
<extension module="org.wildfly.extension.core-management"/>
|
||||
<extension module="org.wildfly.extension.elytron"/>
|
||||
<extension module="org.wildfly.extension.io"/>
|
||||
<extension module="org.wildfly.extension.microprofile.config-smallrye"/>
|
||||
<extension module="org.wildfly.extension.microprofile.health-smallrye"/>
|
||||
<extension module="org.wildfly.extension.microprofile.metrics-smallrye"/>
|
||||
<extension module="org.wildfly.extension.request-controller"/>
|
||||
<extension module="org.wildfly.extension.security.manager"/>
|
||||
<extension module="org.wildfly.extension.undertow"/>
|
||||
</extensions>
|
||||
<management>
|
||||
<security-realms>
|
||||
<security-realm name="ManagementRealm">
|
||||
<authentication>
|
||||
<local default-user="$local" skip-group-loading="true"/>
|
||||
<properties path="mgmt-users.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authentication>
|
||||
<authorization map-groups-to-roles="false">
|
||||
<properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
<security-realm name="ApplicationRealm">
|
||||
<server-identities>
|
||||
<ssl>
|
||||
<keystore path="application.keystore" relative-to="jboss.server.config.dir" keystore-password="password"
|
||||
alias="server" key-password="password" generate-self-signed-certificate-host="localhost"/>
|
||||
</ssl>
|
||||
</server-identities>
|
||||
<authentication>
|
||||
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
|
||||
<properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authentication>
|
||||
<authorization>
|
||||
<properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
</security-realms>
|
||||
<audit-log>
|
||||
<formatters>
|
||||
<json-formatter name="json-formatter"/>
|
||||
</formatters>
|
||||
<handlers>
|
||||
<file-handler name="file" formatter="json-formatter" path="audit-log.log" relative-to="jboss.server.data.dir"/>
|
||||
</handlers>
|
||||
<logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="file"/>
|
||||
</handlers>
|
||||
</logger>
|
||||
</audit-log>
|
||||
<management-interfaces>
|
||||
<http-interface security-realm="ManagementRealm">
|
||||
<http-upgrade enabled="true"/>
|
||||
<socket-binding http="management-http"/>
|
||||
</http-interface>
|
||||
</management-interfaces>
|
||||
<access-control provider="simple">
|
||||
<role-mapping>
|
||||
<role name="SuperUser">
|
||||
<include>
|
||||
<user name="$local"/>
|
||||
</include>
|
||||
</role>
|
||||
</role-mapping>
|
||||
</access-control>
|
||||
</management>
|
||||
<profile>
|
||||
<subsystem xmlns="urn:jboss:domain:logging:8.0">
|
||||
<console-handler name="CONSOLE">
|
||||
<level name="INFO"/>
|
||||
<formatter>
|
||||
<named-formatter name="COLOR-PATTERN"/>
|
||||
</formatter>
|
||||
</console-handler>
|
||||
<periodic-rotating-file-handler name="FILE" autoflush="true">
|
||||
<formatter>
|
||||
<named-formatter name="PATTERN"/>
|
||||
</formatter>
|
||||
<file relative-to="jboss.server.log.dir" path="server.log"/>
|
||||
<suffix value=".yyyy-MM-dd"/>
|
||||
<append value="true"/>
|
||||
</periodic-rotating-file-handler>
|
||||
<logger category="com.arjuna">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="io.jaegertracing.Configuration">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="org.jboss.as.config">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="sun.rmi">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="org.keycloak.cluster.infinispan">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="org.keycloak.connections.infinispan">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="org.keycloak.models.cache.infinispan">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="org.keycloak.models.sessions.infinispan">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<root-logger>
|
||||
<level name="INFO"/>
|
||||
<handlers>
|
||||
<handler name="CONSOLE"/>
|
||||
<handler name="FILE"/>
|
||||
</handlers>
|
||||
</root-logger>
|
||||
<formatter name="PATTERN">
|
||||
<pattern-formatter pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
<formatter name="COLOR-PATTERN">
|
||||
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:bean-validation:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:datasources:5.0">
|
||||
<datasources>
|
||||
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
{% if keycloak_jdbc.postgres.enabled %}
|
||||
<connection-url>{{ keycloak_jdbc.postgres.connection_url }}</connection-url>
|
||||
<driver>{{ keycloak_jdbc.postgres.driver_module_name }}</driver>
|
||||
<pool>
|
||||
<max-pool-size>20</max-pool-size>
|
||||
</pool>
|
||||
<security>
|
||||
<user-name>{{ keycloak_jdbc.postgres.db_user }}</user-name>
|
||||
<password>{{ keycloak_jdbc.postgres.db_password }}</password>
|
||||
</security>
|
||||
{% else %}
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
{% endif %}
|
||||
</datasource>
|
||||
<drivers>
|
||||
{% if keycloak_jdbc.postgres.enabled %}
|
||||
<driver name="{{ keycloak_jdbc.postgres.driver_module_name }}" module="{{ keycloak_jdbc.postgres.driver_module_name }}">
|
||||
<driver-class>org.postgresql.Driver</driver-class>
|
||||
<xa-datasource-class>org.postgresql.xa.PGXADataSource</xa-datasource-class>
|
||||
</driver>
|
||||
{% endif %}
|
||||
<driver name="h2" module="com.h2database.h2">
|
||||
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
|
||||
</driver>
|
||||
</drivers>
|
||||
</datasources>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
|
||||
<deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ee:4.0">
|
||||
<spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
|
||||
<concurrent>
|
||||
<context-services>
|
||||
<context-service name="default" jndi-name="java:jboss/ee/concurrency/context/default" use-transaction-setup-provider="true"/>
|
||||
</context-services>
|
||||
<managed-thread-factories>
|
||||
<managed-thread-factory name="default" jndi-name="java:jboss/ee/concurrency/factory/default" context-service="default"/>
|
||||
</managed-thread-factories>
|
||||
<managed-executor-services>
|
||||
<managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-threshold="60000" keepalive-time="5000"/>
|
||||
</managed-executor-services>
|
||||
<managed-scheduled-executor-services>
|
||||
<managed-scheduled-executor-service name="default" jndi-name="java:jboss/ee/concurrency/scheduler/default" context-service="default" hung-task-threshold="60000" keepalive-time="3000"/>
|
||||
</managed-scheduled-executor-services>
|
||||
</concurrent>
|
||||
<default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/ExampleDS"
|
||||
managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default"
|
||||
managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ejb3:6.0">
|
||||
<session-bean>
|
||||
<stateless>
|
||||
<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
|
||||
</stateless>
|
||||
<stateful default-access-timeout="5000" cache-ref="simple" passivation-disabled-cache-ref="simple"/>
|
||||
<singleton default-access-timeout="5000"/>
|
||||
</session-bean>
|
||||
<pools>
|
||||
<bean-instance-pools>
|
||||
<strict-max-pool name="mdb-strict-max-pool" derive-size="from-cpu-count" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
<strict-max-pool name="slsb-strict-max-pool" derive-size="from-worker-pools" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
</bean-instance-pools>
|
||||
</pools>
|
||||
<caches>
|
||||
<cache name="simple"/>
|
||||
<cache name="distributable" passivation-store-ref="infinispan" aliases="passivating clustered"/>
|
||||
</caches>
|
||||
<passivation-stores>
|
||||
<passivation-store name="infinispan" cache-container="ejb" max-size="10000"/>
|
||||
</passivation-stores>
|
||||
<async thread-pool-name="default"/>
|
||||
<timer-service thread-pool-name="default" default-data-store="default-file-store">
|
||||
<data-stores>
|
||||
<file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
|
||||
</data-stores>
|
||||
</timer-service>
|
||||
<remote cluster="ejb" connector-ref="http-remoting-connector" thread-pool-name="default">
|
||||
<channel-creation-options>
|
||||
<option name="MAX_OUTBOUND_MESSAGES" value="1234" type="remoting"/>
|
||||
</channel-creation-options>
|
||||
</remote>
|
||||
<thread-pools>
|
||||
<thread-pool name="default">
|
||||
<max-threads count="10"/>
|
||||
<keepalive-time time="60" unit="seconds"/>
|
||||
</thread-pool>
|
||||
</thread-pools>
|
||||
<default-security-domain value="other"/>
|
||||
<default-missing-method-permissions-deny-access value="true"/>
|
||||
<statistics enabled="${wildfly.ejb3.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<log-system-exceptions value="true"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:elytron:8.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
|
||||
<providers>
|
||||
<aggregate-providers name="combined-providers">
|
||||
<providers name="elytron"/>
|
||||
<providers name="openssl"/>
|
||||
</aggregate-providers>
|
||||
<provider-loader name="elytron" module="org.wildfly.security.elytron"/>
|
||||
<provider-loader name="openssl" module="org.wildfly.openssl"/>
|
||||
</providers>
|
||||
<audit-logging>
|
||||
<file-audit-log name="local-audit" path="audit.log" relative-to="jboss.server.log.dir" format="JSON"/>
|
||||
</audit-logging>
|
||||
<security-domains>
|
||||
<security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local"/>
|
||||
</security-domain>
|
||||
<security-domain name="ManagementDomain" default-realm="ManagementRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ManagementRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local" role-mapper="super-user-mapper"/>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
<security-realms>
|
||||
<identity-realm name="local" identity="$local"/>
|
||||
<properties-realm name="ApplicationRealm">
|
||||
<users-properties path="application-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ApplicationRealm"/>
|
||||
<groups-properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
<properties-realm name="ManagementRealm">
|
||||
<users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
|
||||
<groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
</security-realms>
|
||||
<mappers>
|
||||
<simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
|
||||
<permission-mapping>
|
||||
<principal name="anonymous"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
<permission-mapping match-all="true">
|
||||
<permission-set name="login-permission"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
</simple-permission-mapper>
|
||||
<constant-realm-mapper name="local" realm-name="local"/>
|
||||
<simple-role-decoder name="groups-to-roles" attribute="groups"/>
|
||||
<constant-role-mapper name="super-user-mapper">
|
||||
<role name="SuperUser"/>
|
||||
</constant-role-mapper>
|
||||
</mappers>
|
||||
<permission-sets>
|
||||
<permission-set name="login-permission">
|
||||
<permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
|
||||
</permission-set>
|
||||
<permission-set name="default-permissions">
|
||||
<permission class-name="org.wildfly.extension.batch.jberet.deployment.BatchPermission" module="org.wildfly.extension.batch.jberet" target-name="*"/>
|
||||
<permission class-name="org.wildfly.transaction.client.RemoteTransactionPermission" module="org.wildfly.transaction.client"/>
|
||||
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
|
||||
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
|
||||
</permission-set>
|
||||
</permission-sets>
|
||||
<http>
|
||||
<http-authentication-factory name="management-http-authentication" security-domain="ManagementDomain" http-server-mechanism-factory="global">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="DIGEST">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</http-authentication-factory>
|
||||
<provider-http-server-mechanism-factory name="global"/>
|
||||
</http>
|
||||
<sasl>
|
||||
<sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ApplicationRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<configurable-sasl-server-factory name="configured" sasl-server-factory="elytron">
|
||||
<properties>
|
||||
<property name="wildfly.sasl.local-user.default-user" value="$local"/>
|
||||
</properties>
|
||||
</configurable-sasl-server-factory>
|
||||
<mechanism-provider-filtering-sasl-server-factory name="elytron" sasl-server-factory="global">
|
||||
<filters>
|
||||
<filter provider-name="WildFlyElytron"/>
|
||||
</filters>
|
||||
</mechanism-provider-filtering-sasl-server-factory>
|
||||
<provider-sasl-server-factory name="global"/>
|
||||
</sasl>
|
||||
<tls>
|
||||
<key-stores>
|
||||
<key-store name="applicationKS">
|
||||
<credential-reference clear-text="password"/>
|
||||
<implementation type="JKS"/>
|
||||
<file path="application.keystore" relative-to="jboss.server.config.dir"/>
|
||||
</key-store>
|
||||
</key-stores>
|
||||
<key-managers>
|
||||
<key-manager name="applicationKM" key-store="applicationKS">
|
||||
<credential-reference clear-text="password"/>
|
||||
</key-manager>
|
||||
</key-managers>
|
||||
<server-ssl-contexts>
|
||||
<server-ssl-context name="applicationSSC" key-manager="applicationKM"/>
|
||||
</server-ssl-contexts>
|
||||
</tls>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:infinispan:9.0">
|
||||
<cache-container name="ejb" default-cache="passivation" aliases="sfsb" module="org.wildfly.clustering.ejb.infinispan">
|
||||
<local-cache name="passivation">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store passivation="true" purge="false"/>
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<cache-container name="keycloak" module="org.keycloak.keycloak-model-infinispan">
|
||||
<transport lock-timeout="60000"/>
|
||||
<local-cache name="realms">
|
||||
<object-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="users">
|
||||
<object-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="authenticationSessions"/>
|
||||
{% for cachename in [ "sessions", "offlineSessions", "clientSessions", "offlineClientSessions", "loginFailures", "actionTokens" ] %}
|
||||
<distributed-cache name="{{ cachename }}">
|
||||
<remote-store cache="{{ cachename }}"
|
||||
remote-servers="remote-cache"
|
||||
passivation="false"
|
||||
fetch-state="false"
|
||||
purge="false"
|
||||
preload="false"
|
||||
shared="true">
|
||||
<property name="rawValues">true</property>
|
||||
<property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
|
||||
<property name="remoteStoreSecurityEnabled">false</property>
|
||||
<property name="infinispan.client.hotrod.auth_username">{{ keycloak_remotecache.username }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_password">{{ keycloak_remotecache.password }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_realm">{{ keycloak_remotecache.realm | default('default') }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_server_name">{{ keycloak_remotecache.server_name }}</property>
|
||||
<property name="infinispan.client.hotrod.sasl_mechanism">{{ keycloak_remotecache.sasl_mechanism | default('SCRAM-SHA-512') }}</property>
|
||||
<property name="infinispan.client.hotrod.use_ssl">false</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_file_name">{{ keycloak_remotecache.trust_store_path | default('/etc/truststore/truststore.jks') }}</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_type">JKS</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_password">{{ keycloak_remotecache.trust_store_password | default("changeme") }}</property>
|
||||
<property name="infinispan.client.hotrod.client_intelligence">TOPOLOGY_AWARE</property>
|
||||
</remote-store>
|
||||
</distributed-cache>
|
||||
{% endfor %}
|
||||
<replicated-cache name="work">
|
||||
<remote-store cache="work"
|
||||
remote-servers="remote-cache"
|
||||
passivation="false"
|
||||
fetch-state="false"
|
||||
purge="false"
|
||||
preload="false"
|
||||
shared="true">
|
||||
<property name="rawValues">true</property>
|
||||
<property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
|
||||
<property name="remoteStoreSecurityEnabled">false</property>
|
||||
<property name="infinispan.client.hotrod.auth_username">{{ keycloak_remotecache.username }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_password">{{ keycloak_remotecache.password }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_realm">{{ keycloak_remotecache.realm | default('default') }}</property>
|
||||
<property name="infinispan.client.hotrod.auth_server_name">{{ keycloak_remotecache.server_name }}</property>
|
||||
<property name="infinispan.client.hotrod.sasl_mechanism">{{ keycloak_remotecache.sasl_mechanism | default('SCRAM-SHA-512') }}</property>
|
||||
<property name="infinispan.client.hotrod.use_ssl">false</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_file_name">{{ keycloak_remotecache.trust_store_path | default('/etc/truststore/truststore.jks') }}</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_type">JKS</property>
|
||||
<property name="infinispan.client.hotrod.trust_store_password">{{ keycloak_remotecache.trust_store_password | default("changeme") }}</property>
|
||||
<property name="infinispan.client.hotrod.client_intelligence">TOPOLOGY_AWARE</property>
|
||||
</remote-store>
|
||||
</replicated-cache>
|
||||
<local-cache name="authorization">
|
||||
<object-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="keys">
|
||||
<object-memory size="1000"/>
|
||||
<expiration max-idle="3600000"/>
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
|
||||
<local-cache name="default">
|
||||
<transaction mode="BATCH"/>
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<cache-container name="web" default-cache="passivation" module="org.wildfly.clustering.web.infinispan">
|
||||
<local-cache name="passivation">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store passivation="true" purge="false"/>
|
||||
</local-cache>
|
||||
<local-cache name="sso">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
</local-cache>
|
||||
<local-cache name="routing"/>
|
||||
</cache-container>
|
||||
<cache-container name="hibernate" module="org.infinispan.hibernate-cache">
|
||||
<local-cache name="entity">
|
||||
<object-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</local-cache>
|
||||
<local-cache name="local-query">
|
||||
<object-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</local-cache>
|
||||
<local-cache name="timestamps"/>
|
||||
</cache-container>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:io:3.0">
|
||||
<worker name="default"/>
|
||||
<buffer-pool name="default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jaxrs:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:jca:5.0">
|
||||
<archive-validation enabled="true" fail-on-error="true" fail-on-warn="false"/>
|
||||
<bean-validation enabled="true"/>
|
||||
<default-workmanager>
|
||||
<short-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</short-running-threads>
|
||||
<long-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</long-running-threads>
|
||||
</default-workmanager>
|
||||
<cached-connection-manager/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jgroups:7.0">
|
||||
<channels default="ee">
|
||||
<channel name="ee" stack="tcp" cluster="ejb"/>
|
||||
</channels>
|
||||
<stacks>
|
||||
<stack name="tcp">
|
||||
<transport site="${jboss.node.name}" type="TCP" socket-binding="jgroups-tcp"/>
|
||||
{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %}
|
||||
<protocol type="JDBC_PING">
|
||||
<property name="datasource_jndi_name">java:jboss/datasources/KeycloakDS</property>
|
||||
<property name="initialize_sql">{{ keycloak_jdbc[keycloak_jdbc_engine].initialize_db }}</property>
|
||||
<property name="insert_single_sql">INSERT INTO JGROUPSPING (own_addr, cluster_name, ping_data) values (?, ?, ?)</property>
|
||||
<property name="delete_single_sql">DELETE FROM JGROUPSPING WHERE own_addr=? AND cluster_name=?</property>
|
||||
<property name="select_all_pingdata_sql">SELECT ping_data FROM JGROUPSPING WHERE cluster_name=?</property>
|
||||
</protocol>
|
||||
{% endif %}
|
||||
<protocol type="MERGE3"/>
|
||||
<protocol type="FD_SOCK"/>
|
||||
<protocol type="FD_ALL"/>
|
||||
<protocol type="VERIFY_SUSPECT"/>
|
||||
<protocol type="pbcast.NAKACK2"/>
|
||||
<protocol type="UNICAST3"/>
|
||||
<protocol type="pbcast.STABLE"/>
|
||||
<protocol type="pbcast.GMS">
|
||||
<property name="join_timeout">30000</property>
|
||||
</protocol>
|
||||
<protocol type="MFC"/>
|
||||
<protocol type="FRAG2"/>
|
||||
</stack>
|
||||
</stacks>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jmx:1.3">
|
||||
<expose-resolved-model/>
|
||||
<expose-expression-model/>
|
||||
<remoting-connector/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jpa:1.1">
|
||||
<jpa default-datasource="" default-extended-persistence-inheritance="DEEP"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
|
||||
<web-context>auth</web-context>
|
||||
<providers>
|
||||
<provider>
|
||||
classpath:${jboss.home.dir}/providers/*
|
||||
</provider>
|
||||
</providers>
|
||||
<master-realm-name>master</master-realm-name>
|
||||
<scheduled-task-interval>900</scheduled-task-interval>
|
||||
<theme>
|
||||
<staticMaxAge>2592000</staticMaxAge>
|
||||
<cacheThemes>true</cacheThemes>
|
||||
<cacheTemplates>true</cacheTemplates>
|
||||
<dir>${jboss.home.dir}/themes</dir>
|
||||
</theme>
|
||||
<spi name="eventsStore">
|
||||
<provider name="jpa" enabled="true">
|
||||
<properties>
|
||||
<property name="exclude-events" value="["REFRESH_TOKEN"]"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="userCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="userSessionPersister">
|
||||
<default-provider>jpa</default-provider>
|
||||
</spi>
|
||||
<spi name="timer">
|
||||
<default-provider>basic</default-provider>
|
||||
</spi>
|
||||
<spi name="connectionsHttpClient">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsJpa">
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="dataSource" value="java:jboss/datasources/KeycloakDS"/>
|
||||
<property name="initializeEmpty" value="true"/>
|
||||
<property name="migrationStrategy" value="update"/>
|
||||
<property name="migrationExport" value="${jboss.home.dir}/keycloak-database-update.sql"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="realmCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsInfinispan">
|
||||
<default-provider>default</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="cacheContainer" value="java:jboss/infinispan/container/keycloak"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="jta-lookup">
|
||||
<default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>
|
||||
<provider name="jboss" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="publicKeyStorage">
|
||||
<provider name="infinispan" enabled="true">
|
||||
<properties>
|
||||
<property name="minTimeBetweenRequests" value="10"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="x509cert-lookup">
|
||||
<default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="hostname">
|
||||
<default-provider>default</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="frontendUrl" value="{{ keycloak_modcluster.frontend_url }}"/>
|
||||
<property name="forceBackendUrlToFrontendUrl" value="true"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:mail:3.0">
|
||||
<mail-session name="default" jndi-name="java:jboss/mail/Default">
|
||||
<smtp-server outbound-socket-binding-ref="mail-smtp"/>
|
||||
</mail-session>
|
||||
</subsystem>
|
||||
{% if keycloak_modcluster.enabled %}
|
||||
<subsystem xmlns="urn:jboss:domain:modcluster:5.0">
|
||||
<proxy name="default" advertise-socket="modcluster" listener="ajp" proxies="proxy1">
|
||||
<dynamic-load-provider>
|
||||
<load-metric type="cpu"/>
|
||||
</dynamic-load-provider>
|
||||
</proxy>
|
||||
</subsystem>
|
||||
{% endif %}
|
||||
<subsystem xmlns="urn:jboss:domain:naming:2.0">
|
||||
<remote-naming/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:remoting:4.0">
|
||||
<http-connector name="http-remoting-connector" connector-ref="default" security-realm="ApplicationRealm"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:request-controller:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:security:2.0">
|
||||
<security-domains>
|
||||
<security-domain name="other" cache-type="default">
|
||||
<authentication>
|
||||
<login-module code="Remoting" flag="optional">
|
||||
<module-option name="password-stacking" value="useFirstPass"/>
|
||||
</login-module>
|
||||
<login-module code="RealmDirect" flag="required">
|
||||
<module-option name="password-stacking" value="useFirstPass"/>
|
||||
</login-module>
|
||||
</authentication>
|
||||
</security-domain>
|
||||
<security-domain name="jboss-web-policy" cache-type="default">
|
||||
<authorization>
|
||||
<policy-module code="Delegating" flag="required"/>
|
||||
</authorization>
|
||||
</security-domain>
|
||||
<security-domain name="jaspitest" cache-type="default">
|
||||
<authentication-jaspi>
|
||||
<login-module-stack name="dummy">
|
||||
<login-module code="Dummy" flag="optional"/>
|
||||
</login-module-stack>
|
||||
<auth-module code="Dummy"/>
|
||||
</authentication-jaspi>
|
||||
</security-domain>
|
||||
<security-domain name="jboss-ejb-policy" cache-type="default">
|
||||
<authorization>
|
||||
<policy-module code="Delegating" flag="required"/>
|
||||
</authorization>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:security-manager:1.0">
|
||||
<deployment-permissions>
|
||||
<maximum-set>
|
||||
<permission class="java.security.AllPermission"/>
|
||||
</maximum-set>
|
||||
</deployment-permissions>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:transactions:5.0">
|
||||
<core-environment node-identifier="{{ inventory_hostname | default('${jboss.tx.node.id:1}') }}">
|
||||
<process-id>
|
||||
<uuid/>
|
||||
</process-id>
|
||||
</core-environment>
|
||||
<recovery-environment socket-binding="txn-recovery-environment" status-socket-binding="txn-status-manager"/>
|
||||
<coordinator-environment statistics-enabled="${wildfly.transactions.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<object-store path="tx-object-store" relative-to="jboss.server.data.dir"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:undertow:10.0" default-server="default-server" default-virtual-host="default-host"
|
||||
default-servlet-container="default" default-security-domain="other"
|
||||
statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<buffer-cache name="default"/>
|
||||
<server name="default-server">
|
||||
<ajp-listener name="ajp" socket-binding="ajp"/>
|
||||
<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true" proxy-address-forwarding="true"/>
|
||||
<https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true"/>
|
||||
<host name="default-host" alias="localhost">
|
||||
<location name="/" handler="welcome-content"/>
|
||||
<http-invoker security-realm="ApplicationRealm"/>
|
||||
<filter-ref name="proxy-peer"/>
|
||||
</host>
|
||||
</server>
|
||||
<servlet-container name="default">
|
||||
<jsp-config/>
|
||||
<websockets/>
|
||||
</servlet-container>
|
||||
<handlers>
|
||||
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
|
||||
</handlers>
|
||||
<filters>
|
||||
<filter name="proxy-peer" module="io.undertow.core"
|
||||
class-name="io.undertow.server.handlers.ProxyPeerAddressHandler"/>
|
||||
</filters>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:weld:4.0"/>
|
||||
<subsystem xmlns="urn:wildfly:microprofile-config-smallrye:1.0"/>
|
||||
<subsystem xmlns="urn:wildfly:microprofile-health-smallrye:2.0" security-enabled="false"
|
||||
empty-liveness-checks-status="${env.MP_HEALTH_EMPTY_LIVENESS_CHECKS_STATUS:UP}"
|
||||
empty-readiness-checks-status="${env.MP_HEALTH_EMPTY_READINESS_CHECKS_STATUS:UP}"/>
|
||||
<subsystem xmlns="urn:wildfly:microprofile-metrics-smallrye:2.0" security-enabled="false"
|
||||
exposed-subsystems="*" prefix="${wildfly.metrics.prefix:wildfly}"/>
|
||||
</profile>
|
||||
<interfaces>
|
||||
<interface name="management">
|
||||
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
|
||||
</interface>
|
||||
<interface name="jgroups">
|
||||
{% if ansible_default_ipv4 is defined %}
|
||||
<subnet-match value="{{ (ansible_default_ipv4.network + '/' + ansible_default_ipv4.netmask) | ansible.utils.ipaddr('net') }}"/>
|
||||
{% else %}
|
||||
<any-address />
|
||||
{% endif %}
|
||||
</interface>
|
||||
<interface name="public">
|
||||
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
|
||||
</interface>
|
||||
</interfaces>
|
||||
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:{{ keycloak_jboss_port_offset }}}">
|
||||
<socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>
|
||||
<socket-binding name="http" port="${jboss.http.port:8080}"/>
|
||||
<socket-binding name="https" port="${jboss.https.port:8443}"/>
|
||||
<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
|
||||
<socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
|
||||
<socket-binding name="jgroups-tcp" interface="jgroups" port="7600"/>
|
||||
<socket-binding name="modcluster" multicast-address="${jboss.modcluster.multicast.address:224.0.1.105}" multicast-port="23364"/>
|
||||
<socket-binding name="txn-recovery-environment" port="4712"/>
|
||||
<socket-binding name="txn-status-manager" port="4713"/>
|
||||
<outbound-socket-binding name="mail-smtp">
|
||||
<remote-destination host="${jboss.mail.server.host:localhost}" port="${jboss.mail.server.port:25}"/>
|
||||
</outbound-socket-binding>
|
||||
{% if keycloak_modcluster.enabled %}
|
||||
<outbound-socket-binding name="proxy1">
|
||||
<remote-destination host="{{ keycloak_modcluster.reverse_proxy_url | default('localhost') }}" port="6666"/>
|
||||
</outbound-socket-binding>
|
||||
{% endif %}
|
||||
<outbound-socket-binding name="remote-cache">
|
||||
<remote-destination host="{{ keycloak_remotecache.server_name | default('localhost') }}" port="${remote.cache.port:11222}"/>
|
||||
</outbound-socket-binding>
|
||||
</socket-binding-group>
|
||||
</server>
|
||||
|
|
@ -0,0 +1,619 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
|
||||
<server xmlns="urn:jboss:domain:10.0">
|
||||
<extensions>
|
||||
<extension module="org.jboss.as.clustering.infinispan"/>
|
||||
<extension module="org.jboss.as.connector"/>
|
||||
<extension module="org.jboss.as.deployment-scanner"/>
|
||||
<extension module="org.jboss.as.ee"/>
|
||||
<extension module="org.jboss.as.ejb3"/>
|
||||
<extension module="org.jboss.as.jaxrs"/>
|
||||
<extension module="org.jboss.as.jmx"/>
|
||||
<extension module="org.jboss.as.jpa"/>
|
||||
<extension module="org.jboss.as.logging"/>
|
||||
<extension module="org.jboss.as.mail"/>
|
||||
<extension module="org.jboss.as.modcluster"/>
|
||||
<extension module="org.jboss.as.naming"/>
|
||||
<extension module="org.jboss.as.remoting"/>
|
||||
<extension module="org.jboss.as.security"/>
|
||||
<extension module="org.jboss.as.transactions"/>
|
||||
<extension module="org.jboss.as.weld"/>
|
||||
<extension module="org.keycloak.keycloak-server-subsystem"/>
|
||||
<extension module="org.wildfly.extension.bean-validation"/>
|
||||
<extension module="org.wildfly.extension.core-management"/>
|
||||
<extension module="org.wildfly.extension.elytron"/>
|
||||
<extension module="org.wildfly.extension.io"/>
|
||||
<extension module="org.wildfly.extension.microprofile.config-smallrye"/>
|
||||
<extension module="org.wildfly.extension.microprofile.health-smallrye"/>
|
||||
<extension module="org.wildfly.extension.microprofile.metrics-smallrye"/>
|
||||
<extension module="org.wildfly.extension.request-controller"/>
|
||||
<extension module="org.wildfly.extension.security.manager"/>
|
||||
<extension module="org.wildfly.extension.undertow"/>
|
||||
</extensions>
|
||||
<management>
|
||||
<security-realms>
|
||||
<security-realm name="ManagementRealm">
|
||||
<authentication>
|
||||
<local default-user="$local" skip-group-loading="true"/>
|
||||
<properties path="mgmt-users.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authentication>
|
||||
<authorization map-groups-to-roles="false">
|
||||
<properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
<security-realm name="ApplicationRealm">
|
||||
<server-identities>
|
||||
<ssl>
|
||||
<keystore path="application.keystore" relative-to="jboss.server.config.dir" keystore-password="password"
|
||||
alias="server" key-password="password" generate-self-signed-certificate-host="localhost"/>
|
||||
</ssl>
|
||||
</server-identities>
|
||||
<authentication>
|
||||
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
|
||||
<properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authentication>
|
||||
<authorization>
|
||||
<properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
</security-realms>
|
||||
<audit-log>
|
||||
<formatters>
|
||||
<json-formatter name="json-formatter"/>
|
||||
</formatters>
|
||||
<handlers>
|
||||
<file-handler name="file" formatter="json-formatter" path="audit-log.log" relative-to="jboss.server.data.dir"/>
|
||||
</handlers>
|
||||
<logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="file"/>
|
||||
</handlers>
|
||||
</logger>
|
||||
</audit-log>
|
||||
<management-interfaces>
|
||||
<http-interface security-realm="ManagementRealm">
|
||||
<http-upgrade enabled="true"/>
|
||||
<socket-binding http="management-http"/>
|
||||
</http-interface>
|
||||
</management-interfaces>
|
||||
<access-control provider="simple">
|
||||
<role-mapping>
|
||||
<role name="SuperUser">
|
||||
<include>
|
||||
<user name="$local"/>
|
||||
</include>
|
||||
</role>
|
||||
</role-mapping>
|
||||
</access-control>
|
||||
</management>
|
||||
<profile>
|
||||
<subsystem xmlns="urn:jboss:domain:logging:8.0">
|
||||
<console-handler name="CONSOLE">
|
||||
<level name="INFO"/>
|
||||
<formatter>
|
||||
<named-formatter name="COLOR-PATTERN"/>
|
||||
</formatter>
|
||||
</console-handler>
|
||||
<periodic-rotating-file-handler name="FILE" autoflush="true">
|
||||
<formatter>
|
||||
<named-formatter name="PATTERN"/>
|
||||
</formatter>
|
||||
<file relative-to="jboss.server.log.dir" path="server.log"/>
|
||||
<suffix value=".yyyy-MM-dd"/>
|
||||
<append value="true"/>
|
||||
</periodic-rotating-file-handler>
|
||||
<logger category="com.arjuna">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="io.jaegertracing.Configuration">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="org.jboss.as.config">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="sun.rmi">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<root-logger>
|
||||
<level name="INFO"/>
|
||||
<handlers>
|
||||
<handler name="CONSOLE"/>
|
||||
<handler name="FILE"/>
|
||||
</handlers>
|
||||
</root-logger>
|
||||
<formatter name="PATTERN">
|
||||
<pattern-formatter pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
<formatter name="COLOR-PATTERN">
|
||||
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:bean-validation:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:datasources:5.0">
|
||||
<datasources>
|
||||
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
<drivers>
|
||||
<driver name="h2" module="com.h2database.h2">
|
||||
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
|
||||
</driver>
|
||||
</drivers>
|
||||
</datasources>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
|
||||
<deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ee:4.0">
|
||||
<spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
|
||||
<concurrent>
|
||||
<context-services>
|
||||
<context-service name="default" jndi-name="java:jboss/ee/concurrency/context/default" use-transaction-setup-provider="true"/>
|
||||
</context-services>
|
||||
<managed-thread-factories>
|
||||
<managed-thread-factory name="default" jndi-name="java:jboss/ee/concurrency/factory/default" context-service="default"/>
|
||||
</managed-thread-factories>
|
||||
<managed-executor-services>
|
||||
<managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-threshold="60000" keepalive-time="5000"/>
|
||||
</managed-executor-services>
|
||||
<managed-scheduled-executor-services>
|
||||
<managed-scheduled-executor-service name="default" jndi-name="java:jboss/ee/concurrency/scheduler/default" context-service="default" hung-task-threshold="60000" keepalive-time="3000"/>
|
||||
</managed-scheduled-executor-services>
|
||||
</concurrent>
|
||||
<default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/ExampleDS"
|
||||
managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default"
|
||||
managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ejb3:6.0">
|
||||
<session-bean>
|
||||
<stateless>
|
||||
<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
|
||||
</stateless>
|
||||
<stateful default-access-timeout="5000" cache-ref="simple" passivation-disabled-cache-ref="simple"/>
|
||||
<singleton default-access-timeout="5000"/>
|
||||
</session-bean>
|
||||
<pools>
|
||||
<bean-instance-pools>
|
||||
<strict-max-pool name="mdb-strict-max-pool" derive-size="from-cpu-count" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
<strict-max-pool name="slsb-strict-max-pool" derive-size="from-worker-pools" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
</bean-instance-pools>
|
||||
</pools>
|
||||
<caches>
|
||||
<cache name="simple"/>
|
||||
<cache name="distributable" passivation-store-ref="infinispan" aliases="passivating clustered"/>
|
||||
</caches>
|
||||
<passivation-stores>
|
||||
<passivation-store name="infinispan" cache-container="ejb" max-size="10000"/>
|
||||
</passivation-stores>
|
||||
<async thread-pool-name="default"/>
|
||||
<timer-service thread-pool-name="default" default-data-store="default-file-store">
|
||||
<data-stores>
|
||||
<file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
|
||||
</data-stores>
|
||||
</timer-service>
|
||||
<remote connector-ref="http-remoting-connector" thread-pool-name="default">
|
||||
<channel-creation-options>
|
||||
<option name="MAX_OUTBOUND_MESSAGES" value="1234" type="remoting"/>
|
||||
</channel-creation-options>
|
||||
</remote>
|
||||
<thread-pools>
|
||||
<thread-pool name="default">
|
||||
<max-threads count="10"/>
|
||||
<keepalive-time time="60" unit="seconds"/>
|
||||
</thread-pool>
|
||||
</thread-pools>
|
||||
<default-security-domain value="other"/>
|
||||
<default-missing-method-permissions-deny-access value="true"/>
|
||||
<statistics enabled="${wildfly.ejb3.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<log-system-exceptions value="true"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:io:3.0">
|
||||
<worker name="default"/>
|
||||
<buffer-pool name="default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:infinispan:9.0">
|
||||
<cache-container name="keycloak">
|
||||
<local-cache name="realms">
|
||||
<object-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="users">
|
||||
<object-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="sessions"/>
|
||||
<local-cache name="authenticationSessions"/>
|
||||
<local-cache name="offlineSessions"/>
|
||||
<local-cache name="clientSessions"/>
|
||||
<local-cache name="offlineClientSessions"/>
|
||||
<local-cache name="loginFailures"/>
|
||||
<local-cache name="work"/>
|
||||
<local-cache name="authorization">
|
||||
<object-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="keys">
|
||||
<object-memory size="1000"/>
|
||||
<expiration max-idle="3600000"/>
|
||||
</local-cache>
|
||||
<local-cache name="actionTokens">
|
||||
<object-memory size="-1"/>
|
||||
<expiration max-idle="-1" interval="300000"/>
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
|
||||
<local-cache name="default">
|
||||
<transaction mode="BATCH"/>
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<cache-container name="web" default-cache="passivation" module="org.wildfly.clustering.web.infinispan">
|
||||
<local-cache name="passivation">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store passivation="true" purge="false"/>
|
||||
</local-cache>
|
||||
<local-cache name="sso">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
</local-cache>
|
||||
<local-cache name="routing"/>
|
||||
</cache-container>
|
||||
<cache-container name="ejb" aliases="sfsb" default-cache="passivation" module="org.wildfly.clustering.ejb.infinispan">
|
||||
<local-cache name="passivation">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store passivation="true" purge="false"/>
|
||||
</local-cache>
|
||||
</cache-container>
|
||||
<cache-container name="hibernate" module="org.infinispan.hibernate-cache">
|
||||
<local-cache name="entity">
|
||||
<object-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</local-cache>
|
||||
<local-cache name="local-query">
|
||||
<object-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</local-cache>
|
||||
<local-cache name="timestamps"/>
|
||||
</cache-container>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jaxrs:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:jca:5.0">
|
||||
<archive-validation enabled="true" fail-on-error="true" fail-on-warn="false"/>
|
||||
<bean-validation enabled="true"/>
|
||||
<default-workmanager>
|
||||
<short-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</short-running-threads>
|
||||
<long-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</long-running-threads>
|
||||
</default-workmanager>
|
||||
<cached-connection-manager/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jmx:1.3">
|
||||
<expose-resolved-model/>
|
||||
<expose-expression-model/>
|
||||
<remoting-connector/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jpa:1.1">
|
||||
<jpa default-datasource="" default-extended-persistence-inheritance="DEEP"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:mail:3.0">
|
||||
<mail-session name="default" jndi-name="java:jboss/mail/Default">
|
||||
<smtp-server outbound-socket-binding-ref="mail-smtp"/>
|
||||
</mail-session>
|
||||
</subsystem>
|
||||
{% if keycloak_modcluster.enabled %}
|
||||
<subsystem xmlns="urn:jboss:domain:modcluster:5.0">
|
||||
<proxy name="default" advertise-socket="modcluster" listener="ajp" proxies="proxy1">
|
||||
<dynamic-load-provider>
|
||||
<load-metric type="cpu"/>
|
||||
</dynamic-load-provider>
|
||||
</proxy>
|
||||
</subsystem>
|
||||
{% endif %}
|
||||
<subsystem xmlns="urn:jboss:domain:naming:2.0">
|
||||
<remote-naming/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:remoting:4.0">
|
||||
<http-connector name="http-remoting-connector" connector-ref="default" security-realm="ApplicationRealm"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:request-controller:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:security-manager:1.0">
|
||||
<deployment-permissions>
|
||||
<maximum-set>
|
||||
<permission class="java.security.AllPermission"/>
|
||||
</maximum-set>
|
||||
</deployment-permissions>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:elytron:8.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
|
||||
<providers>
|
||||
<aggregate-providers name="combined-providers">
|
||||
<providers name="elytron"/>
|
||||
<providers name="openssl"/>
|
||||
</aggregate-providers>
|
||||
<provider-loader name="elytron" module="org.wildfly.security.elytron"/>
|
||||
<provider-loader name="openssl" module="org.wildfly.openssl"/>
|
||||
</providers>
|
||||
<audit-logging>
|
||||
<file-audit-log name="local-audit" path="audit.log" relative-to="jboss.server.log.dir" format="JSON"/>
|
||||
</audit-logging>
|
||||
<security-domains>
|
||||
<security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local"/>
|
||||
</security-domain>
|
||||
<security-domain name="ManagementDomain" default-realm="ManagementRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ManagementRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local" role-mapper="super-user-mapper"/>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
<security-realms>
|
||||
<identity-realm name="local" identity="$local"/>
|
||||
<properties-realm name="ApplicationRealm">
|
||||
<users-properties path="application-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ApplicationRealm"/>
|
||||
<groups-properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
<properties-realm name="ManagementRealm">
|
||||
<users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
|
||||
<groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
</security-realms>
|
||||
<mappers>
|
||||
<simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
|
||||
<permission-mapping>
|
||||
<principal name="anonymous"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
<permission-mapping match-all="true">
|
||||
<permission-set name="login-permission"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
</simple-permission-mapper>
|
||||
<constant-realm-mapper name="local" realm-name="local"/>
|
||||
<simple-role-decoder name="groups-to-roles" attribute="groups"/>
|
||||
<constant-role-mapper name="super-user-mapper">
|
||||
<role name="SuperUser"/>
|
||||
</constant-role-mapper>
|
||||
</mappers>
|
||||
<permission-sets>
|
||||
<permission-set name="login-permission">
|
||||
<permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
|
||||
</permission-set>
|
||||
<permission-set name="default-permissions">
|
||||
<permission class-name="org.wildfly.extension.batch.jberet.deployment.BatchPermission" module="org.wildfly.extension.batch.jberet" target-name="*"/>
|
||||
<permission class-name="org.wildfly.transaction.client.RemoteTransactionPermission" module="org.wildfly.transaction.client"/>
|
||||
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
|
||||
</permission-set>
|
||||
</permission-sets>
|
||||
<http>
|
||||
<http-authentication-factory name="management-http-authentication" security-domain="ManagementDomain" http-server-mechanism-factory="global">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="DIGEST">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</http-authentication-factory>
|
||||
<provider-http-server-mechanism-factory name="global"/>
|
||||
</http>
|
||||
<sasl>
|
||||
<sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ApplicationRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<configurable-sasl-server-factory name="configured" sasl-server-factory="elytron">
|
||||
<properties>
|
||||
<property name="wildfly.sasl.local-user.default-user" value="$local"/>
|
||||
</properties>
|
||||
</configurable-sasl-server-factory>
|
||||
<mechanism-provider-filtering-sasl-server-factory name="elytron" sasl-server-factory="global">
|
||||
<filters>
|
||||
<filter provider-name="WildFlyElytron"/>
|
||||
</filters>
|
||||
</mechanism-provider-filtering-sasl-server-factory>
|
||||
<provider-sasl-server-factory name="global"/>
|
||||
</sasl>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:security:2.0">
|
||||
<security-domains>
|
||||
<security-domain name="other" cache-type="default">
|
||||
<authentication>
|
||||
<login-module code="Remoting" flag="optional">
|
||||
<module-option name="password-stacking" value="useFirstPass"/>
|
||||
</login-module>
|
||||
<login-module code="RealmDirect" flag="required">
|
||||
<module-option name="password-stacking" value="useFirstPass"/>
|
||||
</login-module>
|
||||
</authentication>
|
||||
</security-domain>
|
||||
<security-domain name="jboss-web-policy" cache-type="default">
|
||||
<authorization>
|
||||
<policy-module code="Delegating" flag="required"/>
|
||||
</authorization>
|
||||
</security-domain>
|
||||
<security-domain name="jaspitest" cache-type="default">
|
||||
<authentication-jaspi>
|
||||
<login-module-stack name="dummy">
|
||||
<login-module code="Dummy" flag="optional"/>
|
||||
</login-module-stack>
|
||||
<auth-module code="Dummy"/>
|
||||
</authentication-jaspi>
|
||||
</security-domain>
|
||||
<security-domain name="jboss-ejb-policy" cache-type="default">
|
||||
<authorization>
|
||||
<policy-module code="Delegating" flag="required"/>
|
||||
</authorization>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:transactions:5.0">
|
||||
<core-environment node-identifier="${jboss.tx.node.id:1}">
|
||||
<process-id>
|
||||
<uuid/>
|
||||
</process-id>
|
||||
</core-environment>
|
||||
<recovery-environment socket-binding="txn-recovery-environment" status-socket-binding="txn-status-manager"/>
|
||||
<coordinator-environment statistics-enabled="${wildfly.transactions.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<object-store path="tx-object-store" relative-to="jboss.server.data.dir"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:weld:4.0"/>
|
||||
<subsystem xmlns="urn:wildfly:microprofile-config-smallrye:1.0"/>
|
||||
<subsystem xmlns="urn:wildfly:microprofile-health-smallrye:2.0" security-enabled="false"
|
||||
empty-liveness-checks-status="${env.MP_HEALTH_EMPTY_LIVENESS_CHECKS_STATUS:UP}" empty-readiness-checks-status="${env.MP_HEALTH_EMPTY_READINESS_CHECKS_STATUS:UP}"/>
|
||||
<subsystem xmlns="urn:wildfly:microprofile-metrics-smallrye:2.0" security-enabled="false" exposed-subsystems="*" prefix="${wildfly.metrics.prefix:wildfly}"/>
|
||||
<subsystem xmlns="urn:jboss:domain:undertow:10.0" default-server="default-server" default-virtual-host="default-host"
|
||||
default-servlet-container="default" default-security-domain="other" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<buffer-cache name="default"/>
|
||||
<server name="default-server">
|
||||
<ajp-listener name="ajp" socket-binding="ajp"/>
|
||||
<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"/>
|
||||
<https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true"/>
|
||||
<host name="default-host" alias="localhost">
|
||||
<location name="/" handler="welcome-content"/>
|
||||
<http-invoker security-realm="ApplicationRealm"/>
|
||||
</host>
|
||||
</server>
|
||||
<servlet-container name="default">
|
||||
<jsp-config/>
|
||||
<websockets/>
|
||||
</servlet-container>
|
||||
<handlers>
|
||||
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
|
||||
</handlers>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
|
||||
<web-context>auth</web-context>
|
||||
<providers>
|
||||
<provider>classpath:${jboss.home.dir}/providers/*</provider>
|
||||
</providers>
|
||||
<master-realm-name>master</master-realm-name>
|
||||
<scheduled-task-interval>900</scheduled-task-interval>
|
||||
<theme>
|
||||
<staticMaxAge>2592000</staticMaxAge>
|
||||
<cacheThemes>true</cacheThemes>
|
||||
<cacheTemplates>true</cacheTemplates>
|
||||
<dir>${jboss.home.dir}/themes</dir>
|
||||
</theme>
|
||||
<spi name="eventsStore">
|
||||
<provider name="jpa" enabled="true">
|
||||
<properties>
|
||||
<property name="exclude-events" value="["REFRESH_TOKEN"]"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="userCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="userSessionPersister">
|
||||
<default-provider>jpa</default-provider>
|
||||
</spi>
|
||||
<spi name="timer">
|
||||
<default-provider>basic</default-provider>
|
||||
</spi>
|
||||
<spi name="connectionsHttpClient">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsJpa">
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="dataSource" value="java:jboss/datasources/KeycloakDS"/>
|
||||
<property name="initializeEmpty" value="true"/>
|
||||
<property name="migrationStrategy" value="update"/>
|
||||
<property name="migrationExport" value="${jboss.home.dir}/keycloak-database-update.sql"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="realmCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsInfinispan">
|
||||
<default-provider>default</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="cacheContainer" value="java:jboss/infinispan/container/keycloak"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="jta-lookup">
|
||||
<default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>
|
||||
<provider name="jboss" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="publicKeyStorage">
|
||||
<provider name="infinispan" enabled="true">
|
||||
<properties>
|
||||
<property name="minTimeBetweenRequests" value="10"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="x509cert-lookup">
|
||||
<default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="hostname">
|
||||
<default-provider>default</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="frontendUrl" value="{{ keycloak_modcluster.frontend_url }}"/>
|
||||
<property name="forceBackendUrlToFrontendUrl" value="true"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
</subsystem>
|
||||
</profile>
|
||||
<interfaces>
|
||||
<interface name="management">
|
||||
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
|
||||
</interface>
|
||||
<interface name="public">
|
||||
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
|
||||
</interface>
|
||||
</interfaces>
|
||||
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:{{ keycloak_jboss_port_offset }}}">
|
||||
<socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>
|
||||
<socket-binding name="http" port="${jboss.http.port:8080}"/>
|
||||
<socket-binding name="https" port="${jboss.https.port:8443}"/>
|
||||
<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
|
||||
<socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
|
||||
<socket-binding name="modcluster" multicast-address="${jboss.modcluster.multicast.address:224.0.1.105}" multicast-port="23364"/>
|
||||
<socket-binding name="txn-recovery-environment" port="4712"/>
|
||||
<socket-binding name="txn-status-manager" port="4713"/>
|
||||
<outbound-socket-binding name="mail-smtp">
|
||||
<remote-destination host="localhost" port="25"/>
|
||||
</outbound-socket-binding>
|
||||
{% if keycloak_modcluster.enabled %}
|
||||
<outbound-socket-binding name="proxy1">
|
||||
<remote-destination host="{{ keycloak_modcluster.reverse_proxy_url | default('localhost') }}" port="6666"/>
|
||||
</outbound-socket-binding>
|
||||
{% endif %}
|
||||
</socket-binding-group>
|
||||
</server>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module xmlns="urn:jboss:module:1.0" name="{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }}">
|
||||
<resources>
|
||||
<resource-root path="{{ keycloak_jdbc[keycloak_jdbc_engine].driver_jar_filename }}"/>
|
||||
</resources>
|
||||
<dependencies>
|
||||
<module name="javax.api"/>
|
||||
<module name="javax.transaction.api"/>
|
||||
</dependencies>
|
||||
</module>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{% for feature in keycloak_config.features %}
|
||||
feature.{{ feature.name }}={{ feature.status | default('enabled') }}
|
||||
{% endfor %}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
#!/bin/bash -eu
|
||||
{{ ansible_managed | comment }}
|
||||
|
||||
set +u -o pipefail
|
||||
|
||||
checkEnvVar() {
|
||||
local envVar=${1}
|
||||
local msg=${2}
|
||||
local exitCode=${3}
|
||||
|
||||
if [ -z "${envVar}" ]; then
|
||||
echo "${msg}"
|
||||
exit "${exitCode}"
|
||||
fi
|
||||
}
|
||||
# SystemD will do for the unit, decomment
|
||||
# for testing outside systemd
|
||||
. /etc/sysconfig/keycloak
|
||||
|
||||
readonly KEYCLOAK_HOME={{ keycloak_config.home }}
|
||||
readonly KEYCLOAK_BIND_ADDRESS=${KEYCLOAK_BIND_ADDRESS}
|
||||
readonly KEYCLOAK_HTTP_PORT=${KEYCLOAK_HTTP_PORT}
|
||||
readonly KEYCLOAK_HTTPS_PORT=${KEYCLOAK_HTTPS_PORT}
|
||||
readonly KEYCLOAK_MANAGEMENT_HTTP_PORT=${KEYCLOAK_MANAGEMENT_HTTP_PORT}
|
||||
readonly KEYCLOAK_MANAGEMENT_HTTPS_PORT=${KEYCLOAK_MANAGEMENT_HTTPS_PORT}
|
||||
readonly KEYCLOAK_PIDFILE={{ keycloak_service_pidfile }}
|
||||
|
||||
set -u
|
||||
if [ ! -d "${KEYCLOAK_HOME}" ]; then
|
||||
echo "KEYCLOAK_HOME (${KEYCLOAK_HOME}) is not a directory or does not exists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checkEnvVar "${KEYCLOAK_PIDFILE}" 'KEYCLOAK_PIDFILE not provided' 2
|
||||
|
||||
getKeycloakPID() {
|
||||
|
||||
local pid
|
||||
if [ -e "${KEYCLOAK_PIDFILE}" ]; then
|
||||
cat "${KEYCLOAK_PIDFILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
statusKeycloak() {
|
||||
|
||||
if [ "$(isKeyCloakRunning)" -eq 1 ]; then
|
||||
echo "Keycloak is running (PID:$(getKeycloakPID ))"
|
||||
else
|
||||
echo "Keycloak is NOT running."
|
||||
fi
|
||||
}
|
||||
|
||||
isKeyCloakRunning() {
|
||||
set +e
|
||||
local statusKeycloak=0
|
||||
local pid=$(getKeycloakPID )
|
||||
if [ -n "${pid}" ]; then
|
||||
kill -0 "${pid}" 2> /dev/null
|
||||
if [ "${?}" -eq 0 ]; then
|
||||
statusKeycloak=1
|
||||
fi
|
||||
fi
|
||||
set -e
|
||||
echo "${statusKeycloak}"
|
||||
}
|
||||
|
||||
startKeycloak() {
|
||||
checkEnvVar "${KEYCLOAK_BIND_ADDRESS}" 'KEYCLOAK_BIND_ADDRESS not provided' 2
|
||||
checkEnvVar "${KEYCLOAK_HTTP_PORT}" 'KEYCLOAK_HTTP_PORT not provided' 4
|
||||
checkEnvVar "${KEYCLOAK_HTTPS_PORT}" 'KEYCLOAK_HTTPS_PORT not provided' 5
|
||||
checkEnvVar "${KEYCLOAK_MANAGEMENT_HTTP_PORT}" 'KEYCLOAK_MANAGEMENT_HTTP_PORT not provided' 6
|
||||
checkEnvVar "${KEYCLOAK_MANAGEMENT_HTTPS_PORT}" 'KEYCLOAK_MANAGEMENT_HTTPS_PORT not provided' 7
|
||||
|
||||
if [ "$(isKeyCloakRunning)" -eq 1 ]; then
|
||||
statusKeycloak
|
||||
else
|
||||
LAUNCH_JBOSS_IN_BACKGROUND=1 JBOSS_PIDFILE=${KEYCLOAK_PIDFILE} ${KEYCLOAK_HOME}/bin/standalone.sh \
|
||||
-Djboss.bind.address=${KEYCLOAK_BIND_ADDRESS} \
|
||||
-Djboss.http.port=${KEYCLOAK_HTTP_PORT} \
|
||||
-Djboss.https.port=${KEYCLOAK_HTTPS_PORT} \
|
||||
-Djboss.management.http.port=${KEYCLOAK_MANAGEMENT_HTTP_PORT} \
|
||||
-Djboss.management.https.port=${KEYCLOAK_MANAGEMENT_HTTPS_PORT} \
|
||||
-Djboss.node.name={{ inventory_hostname }} \
|
||||
{% if keycloak_prefer_ipv4 %}-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv4Addresses=true {% endif %}\
|
||||
{% if keycloak_config_standalone_xml is defined %}--server-config={{ keycloak_config_standalone_xml }}{% endif %} &
|
||||
while [ ! -f ${KEYCLOAK_PIDFILE} ]; do sleep 1; done
|
||||
fi
|
||||
}
|
||||
|
||||
stopKeycloak() {
|
||||
local pid=$(getKeycloakPID)
|
||||
if [ -n "${pid}" ]; then
|
||||
set +e
|
||||
kill ${pid} 2> /dev/null
|
||||
kill -0 "${pid}" 2> /dev/null
|
||||
# if [ "${?}" -eq 0 ]; then
|
||||
# sleep 5
|
||||
# kill -9 "${pid}" > /dev/null
|
||||
# fi
|
||||
set -e
|
||||
deletePidFile
|
||||
fi
|
||||
}
|
||||
|
||||
deletePidFile() {
|
||||
rm -f "${KEYCLOAK_PIDFILE}"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
startKeycloak
|
||||
;;
|
||||
stop)
|
||||
stopKeycloak
|
||||
;;
|
||||
restart)
|
||||
stopKeycloak
|
||||
startKeycloak
|
||||
;;
|
||||
status)
|
||||
statusKeycloak
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 start|stop|restart" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{{ ansible_managed | comment }}
|
||||
JAVA_OPTS='{{ keycloak_java_opts }}'
|
||||
JAVA_HOME={{ keycloak_java_home | default(keycloak_pkg_java_home, true) }}
|
||||
JBOSS_HOME={{ keycloak_config.home }}
|
||||
KEYCLOAK_BIND_ADDRESS={{ keycloak_bind_address }}
|
||||
KEYCLOAK_HTTP_PORT={{ keycloak_http_port }}
|
||||
KEYCLOAK_HTTPS_PORT={{ keycloak_https_port }}
|
||||
KEYCLOAK_MANAGEMENT_HTTP_PORT={{ keycloak_management_http_port }}
|
||||
KEYCLOAK_MANAGEMENT_HTTPS_PORT={{ keycloak_management_https_port }}
|
||||
JBOSS_PIDFILE='{{ keycloak_service_pidfile }}'
|
||||
|
||||
WILDFLY_OPTS=-Djboss.bind.address=${KEYCLOAK_BIND_ADDRESS} \
|
||||
-Djboss.http.port=${KEYCLOAK_HTTP_PORT} \
|
||||
-Djboss.https.port=${KEYCLOAK_HTTPS_PORT} \
|
||||
-Djboss.management.http.port=${KEYCLOAK_MANAGEMENT_HTTP_PORT} \
|
||||
-Djboss.management.https.port=${KEYCLOAK_MANAGEMENT_HTTPS_PORT} \
|
||||
-Djboss.node.name={{ inventory_hostname }} \
|
||||
{% if keycloak_prefer_ipv4 %}-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv4Addresses=true {% endif %}\
|
||||
{% if keycloak_config_standalone_xml is defined %}--server-config={{ keycloak_config_standalone_xml }}{% endif %}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{{ ansible_managed | comment }}
|
||||
[Unit]
|
||||
Description={{ keycloak_config.service_name }} Server
|
||||
After=network.target
|
||||
StartLimitIntervalSec={{ keycloak_service_startlimitintervalsec }}
|
||||
StartLimitBurst={{ keycloak_service_startlimitburst }}
|
||||
|
||||
|
||||
[Service]
|
||||
{% if keycloak_service_runas %}
|
||||
User={{ keycloak_service_user }}
|
||||
Group={{ keycloak_service_group }}
|
||||
{% endif -%}
|
||||
EnvironmentFile=-{{ keycloak_sysconf_file }}
|
||||
PIDFile={{ keycloak_service_pidfile }}
|
||||
ExecStart={{ keycloak_config.home }}/bin/standalone.sh $WILDFLY_OPTS
|
||||
WorkingDirectory={{ keycloak_config.home }}
|
||||
TimeoutStartSec=30
|
||||
TimeoutStopSec=30
|
||||
LimitNOFILE=102642
|
||||
{% if keycloak_service_restart_always %}
|
||||
Restart=always
|
||||
{% elif keycloak_service_restart_on_failure %}
|
||||
Restart=on-failure
|
||||
{% endif %}
|
||||
RestartSec={{ keycloak_service_restartsec }}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue