diff --git a/.ansible-lint b/.ansible-lint index 8e4b5ca..5472a97 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -5,8 +5,6 @@ exclude_paths: - molecule/ - .ansible-lint - .yamllint - - meta/ - - playbooks/roles/ rulesdir: - ../../ansible-lint-custom-rules/rules/ @@ -21,23 +19,11 @@ warn_list: - experimental - ignore-errors - no-handler + - fqcn-builtins - no-log-password - - jinja[spacing] - - jinja[invalid] - - meta-no-tags - - name[casing] - - fqcn[action] - - schema[meta] - - key-order[task] - - blocked_modules - - run-once[task] skip_list: - vars_should_not_be_used - - file_is_small_enough - - file_has_valid_name - - name[template] - - var-naming[no-role-prefix] use_default_rules: true parseable: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a622526..7c96b1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,28 +1,51 @@ --- name: CI -on: +"on": push: branches: - main pull_request: - workflow_dispatch: - inputs: - debug_verbosity: - description: 'ANSIBLE_VERBOSITY envvar value' - required: false - schedule: - - cron: '15 6 * * *' jobs: ci: - uses: ansible-middleware/github-actions/.github/workflows/cish.yml@main - secrets: inherit - with: - fqcn: 'middleware_automation/keycloak' - debug_verbosity: "${{ github.event.inputs.debug_verbosity }}" - molecule_tests: >- - [ "debian", "quarkus", "quarkus_ha", "quarkus_ha_remote" ] - podman_tests_current: >- - [ "default", "quarkus_devmode", "quarkus_upgrade" ] - podman_tests_next: >- - [ "default", "quarkus_devmode", "quarkus_upgrade" ] + runs-on: ubuntu-latest + strategy: + matrix: + python_version: ["3.9"] + steps: + - name: Check out code + uses: actions/checkout@v2 + with: + path: ansible_collections/middleware_automation/keycloak + + - name: Set up Python ${{ matrix.python_version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python_version }} + + - name: Install yamllint, ansible and molecule + run: | + python -m pip install --upgrade pip + pip install yamllint 'molecule[docker]~=3.5.2' ansible-core flake8 ansible-lint voluptuous + pip install -r ansible_collections/middleware_automation/keycloak/requirements.txt + + - name: Install ansible-lint custom rules + uses: actions/checkout@v2 + with: + repository: ansible-middleware/ansible-lint-custom-rules + path: ansible_collections/ansible-lint-custom-rules/ + + - name: Create default collection path + run: | + mkdir -p /home/runner/.ansible/collections/ansible_collections + + - name: Run sanity tests + run: ansible-test sanity --docker -v --color --python ${{ matrix.python_version }} --exclude changelogs/fragments/.gitignore + working-directory: ./ansible_collections/middleware_automation/keycloak + + - name: Run molecule test + run: molecule test --all + working-directory: ./ansible_collections/middleware_automation/keycloak + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 540fe4f..7345971 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,11 +8,54 @@ on: - "[0-9]+.[0-9]+.[0-9]+" workflow_dispatch: +env: + COLORTERM: 'yes' + TERM: 'xterm-256color' + PYTEST_ADDOPTS: '--color=yes' + jobs: docs: - uses: ansible-middleware/github-actions/.github/workflows/docs.yml@main - secrets: inherit - with: - fqcn: 'middleware_automation/keycloak' - collection_fqcn: 'middleware_automation.keycloak' - historical_docs: 'false' + runs-on: ubuntu-latest + if: github.repository == 'ansible-middleware/keycloak' + permissions: + actions: write + checks: write + contents: write + deployments: write + packages: write + pages: write + steps: + - name: Check out code + uses: actions/checkout@v2 + with: + path: ansible_collections/middleware_automation/keycloak + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install doc dependencies + run: | + python -m pip install --upgrade pip + pip install -r ansible_collections/middleware_automation/keycloak/docs/requirements.txt + pip install -r ansible_collections/middleware_automation/keycloak/requirements.txt + sudo apt install -y sed hub + + - name: Create default collection path + run: | + mkdir -p /home/runner/.ansible/collections/ansible_collections + + - name: Create changelog and documentation + uses: ansible-middleware/collection-docs-action@main + with: + collection_fqcn: middleware_automation.keycloak + collection_repo: ansible-middleware/keycloak + dependencies: false + commit_changelog: false + commit_ghpages: true + changelog_release: false + generate_docs: true + path: ansible_collections/middleware_automation/keycloak + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0d14d8..f8cdffe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,27 +2,96 @@ name: Release collection on: workflow_dispatch: - inputs: - release_summary: - description: 'Optional release summary for changelogs' - required: false jobs: release: - uses: ansible-middleware/github-actions/.github/workflows/release.yml@main - with: - collection_fqcn: 'middleware_automation.keycloak' - downstream_name: 'rhbk' - release_summary: "${{ github.event.inputs.release_summary }}" - secrets: - galaxy_token: ${{ secrets.ANSIBLE_GALAXY_API_KEY }} - jira_webhook: ${{ secrets.JIRA_WEBHOOK_CREATE_VERSION }} + runs-on: ubuntu-latest + if: github.repository == 'ansible-middleware/keycloak' + permissions: + actions: write + checks: write + contents: write + deployments: write + packages: write + pages: write + outputs: + tag_version: ${{ steps.get_version.outputs.TAG_VERSION }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.TRIGGERING_PAT }} + + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: "3.x" + + - name: Get current version + id: get_version + run: echo "::set-output name=TAG_VERSION::$(grep version galaxy.yml | awk -F'"' '{ print $2 }')" + + - name: Check if tag exists + id: check_tag + run: echo "::set-output name=TAG_EXISTS::$(git tag | grep ${{ steps.get_version.outputs.TAG_VERSION }})" + + - name: Fail if tag exists + if: ${{ steps.get_version.outputs.TAG_VERSION == steps.check_tag.outputs.TAG_EXISTS }} + uses: actions/github-script@v3 + with: + script: | + core.setFailed('Release tag already exists') + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ansible-core antsibull + sudo apt install -y sed hub + + - name: Build collection + run: | + ansible-galaxy collection build . + + - name: Create changelog and documentation + uses: ansible-middleware/collection-docs-action@main + with: + collection_fqcn: middleware_automation.keycloak + collection_repo: ansible-middleware/keycloak + dependencies: false + commit_changelog: true + commit_ghpages: false + changelog_release: true + generate_docs: false + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish collection + env: + ANSIBLE_GALAXY_API_KEY: ${{ secrets.ANSIBLE_GALAXY_API_KEY }} + run: | + ansible-galaxy collection publish *.tar.gz --api-key $ANSIBLE_GALAXY_API_KEY + + - name: Create release tag + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git tag -a ${{ steps.get_version.outputs.TAG_VERSION }} -m "Release v${{ steps.get_version.outputs.TAG_VERSION }}" || true + git push origin --tags + + - name: Publish Release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.get_version.outputs.TAG_VERSION }} + files: "*.tar.gz" + body_path: gh-release.md dispatch: needs: release strategy: matrix: - repo: ['ansible-middleware/ansible-middleware-ee'] + repo: ['ansible-middleware/cross-dc-rhsso-demo', 'ansible-middleware/flange-demo', 'ansible-middleware/ansible-middleware-ee'] runs-on: ubuntu-latest steps: - name: Repository Dispatch diff --git a/.github/workflows/traffic.yml b/.github/workflows/traffic.yml deleted file mode 100644 index d997f4e..0000000 --- a/.github/workflows/traffic.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Collect traffic stats -on: - schedule: - - cron: "51 23 * * 0" - workflow_dispatch: - -jobs: - traffic: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - ref: "gh-pages" - - - name: GitHub traffic - uses: sangonzal/repository-traffic-action@v.0.1.6 - env: - TRAFFIC_ACTION_TOKEN: ${{ secrets.TRIGGERING_PAT }} - - - name: Commit changes - uses: EndBug/add-and-commit@v4 - with: - author_name: Ansible Middleware - message: "GitHub traffic" - add: "./traffic/*" - ref: "gh-pages" diff --git a/.gitignore b/.gitignore index ce41aef..ef79fe9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ *.zip .tmp .cache -.vscode/ -__pycache__/ docs/plugins/ docs/roles/ docs/_build/ @@ -11,6 +9,3 @@ docs/_build/ .mypy_cache/ *.retry changelogs/.plugin-cache.yaml -*.pem -*.key -*.p12 diff --git a/.yamllint b/.yamllint index 10e554e..fa1f1fc 100644 --- a/.yamllint +++ b/.yamllint @@ -15,8 +15,7 @@ rules: commas: max-spaces-after: -1 level: error - comments: - min-spaces-from-content: 1 + comments: disable comments-indentation: disable document-start: disable empty-lines: @@ -31,8 +30,4 @@ rules: new-lines: type: unix trailing-spaces: disable - truthy: disable - octal-values: - forbid-implicit-octal: true - forbid-explicit-octal: true - + truthy: disable \ No newline at end of file diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b290328..c0aca3c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,439 +1,14 @@ -============================================= -middleware\_automation.keycloak Release Notes -============================================= +============================================ +middleware_automation.keycloak Release Notes +============================================ .. contents:: Topics This changelog describes changes after version 0.2.6. -v3.0.1 -====== - -Minor Changes -------------- - -- Version update to 26.0.8 / rhbk 26.0.11 `#277 `_ - -Bugfixes --------- - -- Trigger rebuild handler on envvars file change `#276 `_ - -v3.0.0 -====== - -Minor Changes -------------- - -- Add theme cache invalidation handler `#252 `_ -- keycloak_realm: change url variables to defaults `#268 `_ - -Breaking Changes / Porting Guide --------------------------------- - -- Bump major and ansible-core versions `#266 `_ -- Rename parameters to follow upstream `#270 `_ -- Update for keycloak v26 `#254 `_ - -Bugfixes --------- - -- Access token lifespan is too short for ansible run `#251 `_ -- Load environment vars during kc rebuild `#274 `_ -- Rebuild config and restart service for local providers `#250 `_ -- Rename and honour parameter ``keycloak_quarkus_http_host`` `#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 `_ - -v2.4.2 -====== - -Minor Changes -------------- - -- New parameter ``keycloak_quarkus_download_path`` `#239 `_ - -Bugfixes --------- - -- Add wait_for_port number parameter `#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 `_ -- Update minimum ansible-core version > 2.15 `#232 `_ - -v2.3.0 -====== - -Major Changes -------------- - -- Allow for custom providers hosted on maven repositories `#223 `_ -- Restart handler strategy behaviour `#231 `_ - -Minor Changes -------------- - -- Add support for policy files `#225 `_ -- Allow to add extra custom env vars in sysconfig file `#229 `_ -- Download from alternate URL with optional http authentication `#220 `_ -- Update Keycloak to version 24.0.4 `#218 `_ -- ``proxy-header`` enhancement `#227 `_ - -Bugfixes --------- - -- ``kc.sh build`` uses configured jdk `#211 `_ - -v2.2.2 -====== - -Minor Changes -------------- - -- Copying of key material for TLS configuration `#210 `_ -- Validate certs parameter for JDBC driver downloads `#207 `_ - -Bugfixes --------- - -- Turn off controller privilege escalation `#209 `_ - -v2.2.1 -====== - -Release Summary ---------------- - -Internal release, documentation or test changes only. - -Bugfixes --------- - -- JDBC provider: fix clause in argument validation `#204 `_ - -v2.2.0 -====== - -Major Changes -------------- - -- Support java keystore for configuration of sensitive options `#189 `_ - -Minor Changes -------------- - -- Add ``wait_for_port`` and ``wait_for_log`` systemd unit logic `#199 `_ -- Customize jdbc driver downloads, optional authentication `#202 `_ -- Keystore-based vault SPI configuration `#196 `_ -- New ``keycloak_quarkus_hostname_strict_https`` parameter `#195 `_ -- Providers config and custom providers `#201 `_ -- Remove administrator credentials from files once keycloak is bootstrapped `#197 `_ -- Update keycloak to 24.0 `#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 `_ -- Debian/Ubuntu compatibility `#178 `_ -- Use ``keycloak_realm`` as default for sub-entities `#180 `_ - -Bugfixes --------- - -- Fix permissions on controller-side downloaded artifacts `#184 `_ -- JVM args moved to ``JAVA_OPTS`` envvar (instead of JAVA_OPTS_APPEND) `#186 `_ -- Unrelax configuration file permissions `#191 `_ -- Utilize comment filter for ``ansible_managed`` annotations `#176 `_ - -v2.1.0 -====== - -Major Changes -------------- - -- Implement infinispan TCPPING discovery protocol `#159 `_ - -Minor Changes -------------- - -- Set enable-recovery when xa transactions are enabled `#167 `_ -- keycloak_quarkus: Allow configuring log rotate options in quarkus configuration `#161 `_ -- keycloak_quarkus: ``sticky-session`` for infinispan routes `#163 `_ - -Breaking Changes / Porting Guide --------------------------------- - -- keycloak_quarkus: renamed infinispan host list configuration `#157 `_ - -Bugfixes --------- - -- keycloak_quarkus: fix custom JAVA_HOME parameter name `#171 `_ - -v2.0.2 -====== - -Minor Changes -------------- - -- keycloak_quarkus: Add support for sqlserver jdbc driver `#148 `_ -- keycloak_quarkus: allow configuration of ``hostname-strict-backchannel`` `#152 `_ -- keycloak_quarkus: systemd restart behavior `#145 `_ - -Bugfixes --------- - -- keycloak_quarkus: Use ``keycloak_quarkus_java_opts`` `#154 `_ -- keycloak_quarkus: allow ports <1024 (e.g. :443) in systemd unit `#150 `_ - -v2.0.1 -====== - -Minor Changes -------------- - -- keycloak_quarkus: add hostname-strict parameter `#139 `_ -- keycloak_quarkus: update to version 23.0.1 `#133 `_ - -Bugfixes --------- - -- keycloak_quarkus: template requires lowercase boolean values `#138 `_ - -v2.0.0 -====== - -Minor Changes -------------- - -- Add new parameter for port offset configuration `#124 `_ -- Update Keycloak to version 22.0.5 `#122 `_ - -Breaking Changes / Porting Guide --------------------------------- - -- Add support for more http-related configs `#115 `_ -- Update minimum ansible-core version > 2.14 `#119 `_ -- keycloak_quarkus: enable config of key store and trust store `#116 `_ - -v1.3.0 -====== - -Major Changes -------------- - -- Run service as ``keycloak_service_user`` `#106 `_ - -Minor Changes -------------- - -- keycloak_quarkus: Update Keycloak to version 22.0.3 `#112 `_ -- keycloak_quarkus: fix admin console redirect when running locally `#111 `_ -- keycloak_quarkus: skip proxy config if ``keycloak_quarkus_proxy_mode`` is ``none`` `#109 `_ - -Bugfixes --------- - -- keycloak_quarkus: fix validation failure upon port configuration change `#113 `_ - -v1.2.8 -====== - -Minor Changes -------------- - -- keycloak_quarkus: set openjdk 17 as default `#103 `_ -- keycloak_quarkus: update to version 22.0.1 `#107 `_ - -Bugfixes --------- - -- Fix incorrect checks for ``keycloak_jgroups_subnet`` `#98 `_ -- Undefine ``keycloak_db_valid_conn_sql`` default `#91 `_ -- Update bindep.txt package python3-devel to support RHEL9 `#105 `_ - -v1.2.7 -====== - -Minor Changes -------------- - -- Allow to override jgroups subnet `#93 `_ -- keycloak-quarkus: update keycloakx to v21.1.1 `#92 `_ - -v1.2.6 -====== - -Minor Changes -------------- - -- Add profile features enabling/disabling `#87 `_ -- Improve service restart behavior configuration `#88 `_ -- Update default xa_datasource_class value for mariadb jdbc configuration `#89 `_ - -Bugfixes --------- - -- Handle WFLYCTL0117 when background validation millis is 0 `#90 `_ - -v1.2.5 -====== - -Minor Changes -------------- - -- Add configuration for database connection pool validation `#85 `_ -- Allow to configure administration endpoint URL `#86 `_ -- Allow to force backend URLs to frontend URLs `#84 `_ -- Introduce systemd unit restart behavior `#81 `_ - -v1.2.4 -====== - -Minor Changes -------------- - -- Add ``sqlserver`` to keycloak role jdbc configurations `#78 `_ -- Add configurability for XA transactions `#73 `_ - -Bugfixes --------- - -- Fix deprecation warning for ``ipaddr`` `#77 `_ -- Fix undefined facts when offline patching sso `#71 `_ - -v1.2.1 -====== - -Minor Changes -------------- - -- Allow to setup keycloak HA cluster without remote cache store `#68 `_ - -Bugfixes --------- - -- Pass attributes to realm clients `#69 `_ - -v1.2.0 -====== - -Major Changes -------------- - -- Provide config for multiple modcluster proxies `#60 `_ - -Minor Changes -------------- - -- Allow to configure TCPPING for cluster discovery `#62 `_ -- Drop community.general from dependencies `#61 `_ -- Switch middleware_automation.redhat_csp_download for middleware_automation.common `#63 `_ -- Switch to middleware_automation.common for rh-sso patching `#64 `_ - -v1.1.1 -====== - -Bugfixes --------- - -- keycloak-quarkus: fix ``cache-config-file`` path in keycloak.conf.j2 template `#53 `_ - -v1.1.0 -====== - -Minor Changes -------------- - -- Update keycloak to 18.0.2 - sso to 7.6.1 `#46 `_ -- Variable ``keycloak_no_log`` controls ansible ``no_log`` parameter (for debugging purposes) `#47 `_ -- Variables to override service start retries and delay `#51 `_ -- keycloak_quarkus: variable to enable development mode `#45 `_ - -Breaking Changes / Porting Guide --------------------------------- - -- Rename variables from ``infinispan_`` prefix to ``keycloak_infinispan_`` `#42 `_ - -Bugfixes --------- - -- keycloak_quarkus: fix /var/log/keycloak symlink to keycloak log directory `#44 `_ - -v1.0.7 -====== - -Breaking Changes / Porting Guide --------------------------------- - -- keycloak_quarkus: use absolute path for certificate files `#39 `_ - -Bugfixes --------- - -- keycloak_quarkus: use become for tasks that will otherwise fail `#38 `_ - -v1.0.6 -====== - -Bugfixes --------- - -- keycloak_quarkus: add selected java to PATH in systemd unit `#34 `_ -- keycloak_quarkus: set logfile path correctly under keycloak home `#35 `_ - -v1.0.5 -====== - -Minor Changes -------------- - -- Update config options: keycloak and quarkus `#32 `_ - v1.0.4 ====== -Release Summary ---------------- - -Internal release, documentation or test changes only. - v1.0.3 ====== @@ -474,6 +49,7 @@ Release Summary Minor enhancements, bug and documentation fixes. + Major Changes ------------- @@ -491,3 +67,4 @@ Release Summary --------------- This is the first stable release of the ``middleware_automation.keycloak`` collection. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95b60ed..aee36a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,37 +1,3 @@ -## 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 diff --git a/README.md b/README.md index 9e9867d..d341f58 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,14 @@ # Ansible Collection - middleware_automation.keycloak - [![Build Status](https://github.com/ansible-middleware/keycloak/workflows/CI/badge.svg?branch=main)](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.** - - -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). - +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). + ## Ansible version compatibility -This collection has been tested against following Ansible versions: **>=2.16.0**. +This collection has been tested against following Ansible versions: **>=2.9.10**. Plugins and modules within a collection may be tested with only specific Ansible versions. A collection may contain metadata that identifies these versions. @@ -20,15 +16,12 @@ Plugins and modules within a collection may be tested with only specific Ansible ## Installation - ### 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 - - 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 @@ -40,60 +33,93 @@ collections: 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 - + ### Included roles -* `keycloak_quarkus`: role for installing keycloak (>= 19.0.0, quarkus based). -* `keycloak_realm`: role for configuring a realm, user federation(s), clients and users, in an installed service. -* `keycloak`: role for installing legacy keycloak (<= 19.0, wildfly based). +* [`keycloak`](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak/README.md): role for installing the service. +* [`keycloak_realm`](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak_realm/README.md): role for configuring a realm, user federation(s), clients and users, in an installed service. +* [`keycloak_quarkus`](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak_quarkus/README.md): role for installing the quarkus variant of keycloak (>= 17.0.0). - ## Usage ### Install Playbook - -* [`playbooks/keycloak_quarkus.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/keycloak_quarkus.yml) installs keycloak >= 17 based on the defined variables (using most defaults). -* [`playbooks/keycloak.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/keycloak.yml) installs keycloak legacy based on the defined variables (using most defaults). + +* [`playbooks/keycloak.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/keycloak.yml) installs the upstream(Keycloak) based on the defined variables. +* [`playbooks/rhsso.yml`](https://github.com/ansible-middleware/keycloak/blob/main/playbooks/rhsso.yml) installs Red Hat Single Sign-On(RHSSO) based on defined variables. Both playbooks include the `keycloak` role, with different settings, as described in the following sections. For full service configuration details, refer to 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. +### Choosing between upstream project (Keycloak) and Red Hat Single Sign-On (RHSSO) + +The general flag `keycloak_rhsso_enable` controls what to install between upstream (Keycloak, when `False`) or Red Hat Single Sign-On (when `True`). +The default value for the flag if `True` when Red Hat Network credentials are defined, `False` otherwise. + + +#### Install upstream (Keycloak) from keycloak releases + +This is the default approach when RHN credentials are not defined. Keycloak is downloaded from keycloak builds (hosted on github.com) locally, and distributed to target nodes. + + +#### Install RHSSO from the Red Hat Customer Support Portal + +Define the credentials as follows, and the default behaviour is to download a fresh archive of RHSSO on the controller node, then distribute to target nodes. ```yaml -keycloak_offline_install: true +rhn_username: '' +rhn_password: '' +# (keycloak_rhsso_enable defaults to True) ``` - - +#### Install from controller node (local source) + +Making the keycloak zip archive (or the RHSSO zip archive), available to the playbook repository root directory, and setting `keycloak_offline_install` to `True`, allows to skip +the download tasks. The local path for the archive matches the downloaded archive path, so it is also used as a cache when multiple hosts are provisioned in a cluster. + +```yaml +keycloak_offline_install: True +``` + +And depending on `keycloak_rhsso_enable`: + +* `True`: install RHSSO using file rh-sso-x.y.z-server-dist.zip +* `False`: install keycloak using file keycloak-x.y.zip #### 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). +For RHSSO: + +```yaml +keycloak_rhsso_enable: True +keycloak_rhsso_download_url: "https://///rh-sso-x.y.z-server-dist.zip" +``` + +For keycloak: + +```yaml +keycloak_rhsso_enable: False +keycloak_download_url: "https://///keycloak-x.y.zip" +``` ### Example installation command -Execute the following command from the source root directory +Execute the following command from the source root directory ``` ansible-playbook -i -e @rhn-creds.yml playbooks/keycloak.yml -e keycloak_admin_password= -``` +``` - `keycloak_admin_password` Password for the administration console user account. - `ansible_hosts` is the inventory, below is an example inventory for deploying to localhost @@ -103,16 +129,14 @@ ansible-playbook -i -e @rhn-creds.yml playbooks/keycloak.yml -e 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. - ## Configuration ### Config 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). - + ### Example configuration command @@ -130,17 +154,16 @@ ansible-playbook -i playbooks/keycloak_realm.yml -e keycloak_adm [keycloak] localhost ansible_connection=local ``` - + For full configuration details, refer to the [keycloak_realm role README](https://github.com/ansible-middleware/keycloak/blob/main/roles/keycloak_realm/README.md). - - - +## Support +Keycloak collection v1.0.0 is a Beta release and for [Technical Preview](https://access.redhat.com/support/offerings/techpreview). If you have any issues or questions related to collection, please don't hesitate to contact us on Ansible-middleware-core@redhat.com or open an issue on https://github.com/ansible-middleware/keycloak/issues ## License Apache License v2.0 or later - + See [LICENSE](LICENSE) to view the full text. - + diff --git a/bindep.txt b/bindep.txt deleted file mode 100644 index 0014f47..0000000 --- a/bindep.txt +++ /dev/null @@ -1,9 +0,0 @@ -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] - diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 9b09e13..f43e0d7 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -59,618 +59,4 @@ releases: - 31.yaml release_date: '2022-05-09' 1.0.4: - changes: - release_summary: 'Internal release, documentation or test changes only. - - ' release_date: '2022-05-11' - 1.0.5: - changes: - minor_changes: - - 'Update config options: keycloak and quarkus `#32 `_ - - ' - fragments: - - 32.yaml - release_date: '2022-05-25' - 1.0.6: - changes: - bugfixes: - - 'keycloak_quarkus: add selected java to PATH in systemd unit `#34 `_ - - ' - - 'keycloak_quarkus: set logfile path correctly under keycloak home `#35 `_ - - ' - fragments: - - 34.yaml - - 35.yaml - release_date: '2022-06-01' - 1.0.7: - changes: - breaking_changes: - - 'keycloak_quarkus: use absolute path for certificate files `#39 `_ - - ' - bugfixes: - - 'keycloak_quarkus: use become for tasks that will otherwise fail `#38 `_ - - ' - fragments: - - 38.yaml - - 39.yaml - release_date: '2022-07-06' - 1.1.0: - changes: - breaking_changes: - - 'Rename variables from ``infinispan_`` prefix to ``keycloak_infinispan_`` - `#42 `_ - - ' - bugfixes: - - 'keycloak_quarkus: fix /var/log/keycloak symlink to keycloak log directory - `#44 `_ - - ' - minor_changes: - - 'Update keycloak to 18.0.2 - sso to 7.6.1 `#46 `_ - - ' - - 'Variable ``keycloak_no_log`` controls ansible ``no_log`` parameter (for debugging - purposes) `#47 `_ - - ' - - 'Variables to override service start retries and delay `#51 `_ - - ' - - 'keycloak_quarkus: variable to enable development mode `#45 `_ - - ' - fragments: - - 42.yaml - - 44.yaml - - 45.yaml - - 46.yaml - - 47.yaml - - 51.yaml - release_date: '2023-01-09' - 1.1.1: - changes: - bugfixes: - - 'keycloak-quarkus: fix ``cache-config-file`` path in keycloak.conf.j2 template - `#53 `_ - - ' - fragments: - - 53.yaml - release_date: '2023-03-07' - 1.2.0: - changes: - major_changes: - - 'Provide config for multiple modcluster proxies `#60 `_ - - ' - minor_changes: - - 'Allow to configure TCPPING for cluster discovery `#62 `_ - - ' - - 'Drop community.general from dependencies `#61 `_ - - ' - - 'Switch middleware_automation.redhat_csp_download for middleware_automation.common - `#63 `_ - - ' - - 'Switch to middleware_automation.common for rh-sso patching `#64 `_ - - ' - fragments: - - 60.yaml - - 61.yaml - - 62.yaml - - 63.yaml - - 64.yaml - release_date: '2023-03-16' - 1.2.1: - changes: - bugfixes: - - 'Pass attributes to realm clients `#69 `_ - - ' - minor_changes: - - 'Allow to setup keycloak HA cluster without remote cache store `#68 `_ - - ' - fragments: - - 68.yaml - - 69.yaml - release_date: '2023-04-11' - 1.2.4: - changes: - bugfixes: - - 'Fix deprecation warning for ``ipaddr`` `#77 `_ - - ' - - 'Fix undefined facts when offline patching sso `#71 `_ - - ' - minor_changes: - - 'Add ``sqlserver`` to keycloak role jdbc configurations `#78 `_ - - ' - - 'Add configurability for XA transactions `#73 `_ - - ' - fragments: - - 71.yaml - - 73.yaml - - 77.yaml - - 78.yaml - release_date: '2023-05-09' - 1.2.5: - changes: - minor_changes: - - 'Add configuration for database connection pool validation `#85 `_ - - ' - - 'Allow to configure administration endpoint URL `#86 `_ - - ' - - 'Allow to force backend URLs to frontend URLs `#84 `_ - - ' - - 'Introduce systemd unit restart behavior `#81 `_ - - ' - fragments: - - 81.yaml - - 84.yaml - - 85.yaml - - 86.yaml - release_date: '2023-05-26' - 1.2.6: - changes: - bugfixes: - - 'Handle WFLYCTL0117 when background validation millis is 0 `#90 `_ - - ' - minor_changes: - - 'Add profile features enabling/disabling `#87 `_ - - ' - - 'Improve service restart behavior configuration `#88 `_ - - ' - - 'Update default xa_datasource_class value for mariadb jdbc configuration `#89 - `_ - - ' - fragments: - - 87.yaml - - 88.yaml - - 89.yaml - - 90.yaml - release_date: '2023-06-07' - 1.2.7: - changes: - minor_changes: - - 'Allow to override jgroups subnet `#93 `_ - - ' - - 'keycloak-quarkus: update keycloakx to v21.1.1 `#92 `_ - - ' - fragments: - - 92.yaml - - 93.yaml - release_date: '2023-06-19' - 1.2.8: - changes: - bugfixes: - - 'Fix incorrect checks for ``keycloak_jgroups_subnet`` `#98 `_ - - ' - - 'Undefine ``keycloak_db_valid_conn_sql`` default `#91 `_ - - ' - - 'Update bindep.txt package python3-devel to support RHEL9 `#105 `_ - - ' - minor_changes: - - 'keycloak_quarkus: set openjdk 17 as default `#103 `_ - - ' - - 'keycloak_quarkus: update to version 22.0.1 `#107 `_ - - ' - fragments: - - 103.yaml - - 105.yaml - - 107.yaml - - 91.yaml - - 98.yaml - release_date: '2023-08-28' - 1.3.0: - changes: - bugfixes: - - 'keycloak_quarkus: fix validation failure upon port configuration change `#113 - `_ - - ' - major_changes: - - 'Run service as ``keycloak_service_user`` `#106 `_ - - ' - minor_changes: - - 'keycloak_quarkus: Update Keycloak to version 22.0.3 `#112 `_ - - ' - - 'keycloak_quarkus: fix admin console redirect when running locally `#111 `_ - - ' - - 'keycloak_quarkus: skip proxy config if ``keycloak_quarkus_proxy_mode`` is - ``none`` `#109 `_ - - ' - fragments: - - 106.yaml - - 109.yaml - - 111.yaml - - 112.yaml - - 113.yaml - release_date: '2023-09-25' - 2.0.0: - changes: - breaking_changes: - - 'Add support for more http-related configs `#115 `_ - - ' - - 'Update minimum ansible-core version > 2.14 `#119 `_ - - ' - - 'keycloak_quarkus: enable config of key store and trust store `#116 `_ - - ' - minor_changes: - - 'Add new parameter for port offset configuration `#124 `_ - - ' - - 'Update Keycloak to version 22.0.5 `#122 `_ - - ' - fragments: - - 115.yaml - - 116.yaml - - 119.yaml - - 122.yaml - - 124.yaml - release_date: '2023-11-20' - 2.0.1: - changes: - bugfixes: - - 'keycloak_quarkus: template requires lowercase boolean values `#138 `_ - - ' - minor_changes: - - 'keycloak_quarkus: add hostname-strict parameter `#139 `_ - - ' - - 'keycloak_quarkus: update to version 23.0.1 `#133 `_ - - ' - fragments: - - 133.yaml - - 138.yaml - - 139.yaml - release_date: '2023-12-07' - 2.0.2: - changes: - bugfixes: - - 'keycloak_quarkus: Use ``keycloak_quarkus_java_opts`` `#154 `_ - - ' - - 'keycloak_quarkus: allow ports <1024 (e.g. :443) in systemd unit `#150 `_ - - ' - minor_changes: - - 'keycloak_quarkus: Add support for sqlserver jdbc driver `#148 `_ - - ' - - 'keycloak_quarkus: allow configuration of ``hostname-strict-backchannel`` - `#152 `_ - - ' - - 'keycloak_quarkus: systemd restart behavior `#145 `_ - - ' - fragments: - - 145.yaml - - 148.yaml - - 150.yaml - - 152.yaml - - 154.yaml - release_date: '2024-01-17' - 2.1.0: - changes: - breaking_changes: - - 'keycloak_quarkus: renamed infinispan host list configuration `#157 `_ - - ' - bugfixes: - - 'keycloak_quarkus: fix custom JAVA_HOME parameter name `#171 `_ - - ' - major_changes: - - 'Implement infinispan TCPPING discovery protocol `#159 `_ - - ' - minor_changes: - - 'Set enable-recovery when xa transactions are enabled `#167 `_ - - ' - - 'keycloak_quarkus: Allow configuring log rotate options in quarkus configuration - `#161 `_ - - ' - - 'keycloak_quarkus: ``sticky-session`` for infinispan routes `#163 `_ - - ' - fragments: - - 157.yaml - - 159.yaml - - 161.yaml - - 163.yaml - - 167.yaml - - 171.yaml - release_date: '2024-02-28' - 2.1.1: - changes: - bugfixes: - - 'Fix permissions on controller-side downloaded artifacts `#184 `_ - - ' - - 'JVM args moved to ``JAVA_OPTS`` envvar (instead of JAVA_OPTS_APPEND) `#186 - `_ - - ' - - 'Unrelax configuration file permissions `#191 `_ - - ' - - 'Utilize comment filter for ``ansible_managed`` annotations `#176 `_ - - ' - minor_changes: - - 'Add reverse ``proxy_headers`` config, supersedes ``proxy_mode`` `#187 `_ - - ' - - 'Debian/Ubuntu compatibility `#178 `_ - - ' - - 'Use ``keycloak_realm`` as default for sub-entities `#180 `_ - - ' - fragments: - - 176.yaml - - 178.yaml - - 180.yaml - - 184.yaml - - 186.yaml - - 187.yaml - - 191.yaml - release_date: '2024-04-17' - 2.1.2: - changes: - release_summary: 'Internal release, documentation or test changes only. - - ' - release_date: '2024-04-17' - 2.2.0: - changes: - major_changes: - - 'Support java keystore for configuration of sensitive options `#189 `_ - - ' - minor_changes: - - 'Add ``wait_for_port`` and ``wait_for_log`` systemd unit logic `#199 `_ - - ' - - 'Customize jdbc driver downloads, optional authentication `#202 `_ - - ' - - 'Keystore-based vault SPI configuration `#196 `_ - - ' - - 'New ``keycloak_quarkus_hostname_strict_https`` parameter `#195 `_ - - ' - - 'Providers config and custom providers `#201 `_ - - ' - - 'Remove administrator credentials from files once keycloak is bootstrapped - `#197 `_ - - ' - - 'Update keycloak to 24.0 `#194 `_ - - ' - fragments: - - 189.yaml - - 194.yaml - - 195.yaml - - 196.yaml - - 197.yaml - - 199.yaml - - 201.yaml - - 202.yaml - release_date: '2024-05-01' - 2.2.1: - changes: - bugfixes: - - 'JDBC provider: fix clause in argument validation `#204 `_ - - ' - release_summary: Internal release, documentation or test changes only. - fragments: - - 204.yaml - - v2.2.1-devel_summary.yaml - release_date: '2024-05-02' - 2.2.2: - changes: - bugfixes: - - 'Turn off controller privilege escalation `#209 `_ - - ' - minor_changes: - - 'Copying of key material for TLS configuration `#210 `_ - - ' - - 'Validate certs parameter for JDBC driver downloads `#207 `_ - - ' - fragments: - - 207.yaml - - 209.yaml - - 210.yaml - release_date: '2024-05-06' - 2.3.0: - changes: - bugfixes: - - '``kc.sh build`` uses configured jdk `#211 `_ - - ' - major_changes: - - 'Allow for custom providers hosted on maven repositories `#223 `_ - - ' - - 'Restart handler strategy behaviour `#231 `_ - - ' - minor_changes: - - 'Add support for policy files `#225 `_ - - ' - - 'Allow to add extra custom env vars in sysconfig file `#229 `_ - - ' - - 'Download from alternate URL with optional http authentication `#220 `_ - - ' - - 'Update Keycloak to version 24.0.4 `#218 `_ - - ' - - '``proxy-header`` enhancement `#227 `_ - - ' - fragments: - - 211.yaml - - 218.yaml - - 220.yaml - - 223.yaml - - 225.yaml - - 227.yaml - - 229.yaml - - 231.yaml - release_date: '2024-05-20' - 2.4.0: - changes: - major_changes: - - 'Enable by default health check on restart `#234 `_ - - ' - - 'Update minimum ansible-core version > 2.15 `#232 `_ - - ' - fragments: - - 232.yaml - - 234.yaml - release_date: '2024-06-04' - 2.4.1: - changes: - release_summary: Internal release, documentation or test changes only. - fragments: - - v2.4.1-devel_summary.yaml - release_date: '2024-07-02' - 2.4.2: - changes: - bugfixes: - - 'Add wait_for_port number parameter `#237 `_ - - ' - minor_changes: - - 'New parameter ``keycloak_quarkus_download_path`` `#239 `_ - - ' - fragments: - - 237.yaml - - 239.yaml - release_date: '2024-09-26' - 2.4.3: - changes: - minor_changes: - - 'Update keycloak to 24.0.5 `#241 `_ - - ' - fragments: - - 241.yaml - release_date: '2024-10-16' - 3.0.0: - changes: - breaking_changes: - - 'Bump major and ansible-core versions `#266 `_ - - ' - - 'Rename parameters to follow upstream `#270 `_ - - ' - - 'Update for keycloak v26 `#254 `_ - - ' - bugfixes: - - 'Access token lifespan is too short for ansible run `#251 `_ - - ' - - 'Load environment vars during kc rebuild `#274 `_ - - ' - - 'Rebuild config and restart service for local providers `#250 `_ - - ' - - 'Rename and honour parameter ``keycloak_quarkus_http_host`` `#271 `_ - - ' - minor_changes: - - 'Add theme cache invalidation handler `#252 `_ - - ' - - 'keycloak_realm: change url variables to defaults `#268 `_ - - ' - fragments: - - 250.yaml - - 251.yaml - - 252.yaml - - 254.yaml - - 266.yaml - - 268.yaml - - 270.yaml - - 271.yaml - - 274.yaml - modules: - - description: Allows administration of Keycloak realm via Keycloak API - name: keycloak_realm - namespace: '' - release_date: '2025-04-23' - 3.0.1: - changes: - bugfixes: - - 'Trigger rebuild handler on envvars file change `#276 `_ - - ' - minor_changes: - - 'Version update to 26.0.8 / rhbk 26.0.11 `#277 `_ - - ' - fragments: - - 276.yaml - - 277.yaml - release_date: '2025-05-02' diff --git a/changelogs/config.yaml b/changelogs/config.yaml index 3c7fb7e..374ae65 100644 --- a/changelogs/config.yaml +++ b/changelogs/config.yaml @@ -11,22 +11,22 @@ notesdir: fragments prelude_section_name: release_summary prelude_section_title: Release Summary sections: - - - major_changes - - Major Changes - - - minor_changes - - Minor Changes - - - breaking_changes - - Breaking Changes / Porting Guide - - - deprecated_features - - Deprecated Features - - - removed_features - - Removed Features - - - security_fixes - - Security Fixes - - - bugfixes - - Bugfixes - - - known_issues - - Known Issues +- - major_changes + - Major Changes +- - minor_changes + - Minor Changes +- - breaking_changes + - Breaking Changes / Porting Guide +- - deprecated_features + - Deprecated Features +- - removed_features + - Removed Features +- - security_fixes + - Security Fixes +- - bugfixes + - Bugfixes +- - known_issues + - Known Issues title: middleware_automation.keycloak trivial_section_name: trivial use_fqcn: true diff --git a/docs/_gh_include/footer.inc b/docs/_gh_include/footer.inc index 11c1cfe..73bac34 100644 --- a/docs/_gh_include/footer.inc +++ b/docs/_gh_include/footer.inc @@ -7,7 +7,7 @@
-

© Copyright 2024, Red Hat, Inc.

+

© Copyright 2022, Red Hat, Inc.

Built with Sphinx using a theme @@ -18,4 +18,4 @@ - + \ No newline at end of file diff --git a/docs/_gh_include/header.inc b/docs/_gh_include/header.inc index d97c7f1..f9cd101 100644 --- a/docs/_gh_include/header.inc +++ b/docs/_gh_include/header.inc @@ -21,20 +21,6 @@ -
diff --git a/docs/conf.py b/docs/conf.py index c1b24a9..31502f3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,6 @@ extensions = [ 'myst_parser', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', - 'sphinx_antsibull_ext', 'ansible_basic_sphinx_ext', ] @@ -72,7 +71,7 @@ language = None exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.tmp'] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'ansible' +pygments_style = 'sphinx' highlight_language = 'YAML+Jinja' diff --git a/docs/index.rst b/docs/index.rst index 6c46ab1..d9bfa5f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,25 +10,23 @@ Welcome to Keycloak Collection documentation README plugins/index roles/index - Changelog .. toctree:: :maxdepth: 2 :caption: Developer documentation - Developing - Testing - Releasing + testing + developing + releasing .. toctree:: :maxdepth: 2 - :caption: Middleware collections + :caption: General - Keycloak / Red Hat Single Sign-On - Infinispan / Red Hat Data Grid - Wildfly / Red Hat JBoss EAP - Tomcat / Red Hat JWS - ActiveMQ / Red Hat AMQ Broker - Kafka / Red Hat AMQ Streams - Ansible Middleware utilities - JCliff + Changelog + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/docs/requirements.txt b/docs/requirements.txt index 303f3a6..72a7d48 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,5 @@ antsibull>=0.17.0 -antsibull-docs -antsibull-changelog -ansible-core>=2.16.0 -ansible-pygments +ansible-base>=2.10.12 sphinx-rtd-theme git+https://github.com/felixfontein/ansible-basic-sphinx-ext myst-parser diff --git a/docs/testing.md b/docs/testing.md index 8e773ea..7a700c0 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -4,7 +4,24 @@ 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). + +``` +pip install yamllint 'molecule[docker]~=3.5.2' ansible-core flake8 ansible-lint voluptuous +molecule test --all +``` + + +## Integration testing + +Demo repositories which depend on the collection, and aggregate functionality with other middleware_automation collections, are automatically rebuilt +at every collection release to ensure non-breaking changes and consistent behaviour. + +The repository are: + + - [Flange demo](https://github.com/ansible-middleware/flange-demo) + A deployment of Wildfly cluster integrated with keycloak and infinispan. + - [CrossDC keycloak demo](https://github.com/ansible-middleware/cross-dc-rhsso-demo) + A clustered multi-regional installation of keycloak with infinispan remote caches. ## Test playbooks @@ -12,7 +29,15 @@ The test scenarios are available on the source code repository each on his own s 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 +# setup environment +pip install ansible-core +# clone the repository +git clone https://github.com/ansible-middleware/keycloak +cd keycloak +# install collection dependencies +ansible-galaxy collection install -r requirements.yml +# install collection python deps +pip install -r requirements.txt # create inventory for localhost cat << EOF > inventory [keycloak] @@ -21,3 +46,4 @@ EOF # run the playbook ansible-playbook -i inventory playbooks/keycloak.yml ``` + diff --git a/galaxy.yml b/galaxy.yml index 006207e..40279e4 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,13 +1,12 @@ --- namespace: middleware_automation name: keycloak -version: "3.0.2" +version: "1.0.4" readme: README.md authors: - Romain Pelisse - Guido Grazioli - Pavan Kumar Motaparthi - - Helmut Wolf description: Install and configure a keycloak, or Red Hat Single Sign-on, service. license_file: "LICENSE" tags: @@ -21,26 +20,15 @@ tags: - security - infrastructure - authentication - - java - - runtimes - - middleware - - a4mw dependencies: - "middleware_automation.common": ">=1.2.1" - "ansible.posix": ">=1.4.0" + "middleware_automation.redhat_csp_download": ">=1.2.1" + "middleware_automation.wildfly": ">=1.0.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 build_ignore: - - .gitignore + - molecule - .github - - .yamllint - '*.tar.gz' - '*.zip' - - molecule - - changelogs - - docs/_gh_include - - docs/conf.py - - docs/roles.rst.template - - docs/requirements.yml diff --git a/meta/execution-environment.yml b/meta/execution-environment.yml deleted file mode 100644 index dba85a8..0000000 --- a/meta/execution-environment.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -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 diff --git a/meta/runtime.yml b/meta/runtime.yml index 49c7554..9baaad6 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,2 +1,2 @@ --- -requires_ansible: ">=2.16.0" +requires_ansible: ">=2.9.10" \ No newline at end of file diff --git a/molecule/debian/converge.yml b/molecule/debian/converge.yml deleted file mode 100644 index e853b38..0000000 --- a/molecule/debian/converge.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -- name: Converge - hosts: all - vars: - keycloak_quarkus_show_deprecation_warnings: false - keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" - keycloak_quarkus_bootstrap_admin_user: "remembertochangeme" - keycloak_quarkus_hostname: http://instance:8080 - keycloak_quarkus_log: file - keycloak_quarkus_start_dev: true - keycloak_quarkus_proxy_mode: none - roles: - - role: keycloak_quarkus - - role: keycloak_realm - keycloak_url: "{{ keycloak_quarkus_hostname }}" - keycloak_context: '' - keycloak_admin_user: "{{ keycloak_quarkus_bootstrap_admin_user }}" - keycloak_admin_password: "{{ keycloak_quarkus_bootstrap_admin_password }}" - keycloak_client_users: - - username: TestUser - password: password - client_roles: - - client: TestClient - role: TestRoleUser - realm: "{{ keycloak_realm }}" - - username: TestAdmin - password: password - client_roles: - - client: TestClient - role: TestRoleUser - realm: "{{ keycloak_realm }}" - - client: TestClient - role: TestRoleAdmin - realm: "{{ keycloak_realm }}" - keycloak_realm: TestRealm - keycloak_clients: - - name: TestClient - realm: "{{ keycloak_realm }}" - public_client: "{{ keycloak_client_public }}" - web_origins: "{{ keycloak_client_web_origins }}" - users: "{{ keycloak_client_users }}" - client_id: TestClient - attributes: - post.logout.redirect.uris: '/public/logout' diff --git a/molecule/debian/molecule.yml b/molecule/debian/molecule.yml deleted file mode 100644 index 8cccc25..0000000 --- a/molecule/debian/molecule.yml +++ /dev/null @@ -1,48 +0,0 @@ ---- -driver: - name: docker -platforms: - - name: instance - image: ghcr.io/hspaans/molecule-containers:debian-13 - pre_build_image: true - privileged: true - port_bindings: - - "8080/tcp" - - "8443/tcp" - - "8009/tcp" - cgroupns_mode: host - command: "/lib/systemd/systemd" - volumes: - - /sys/fs/cgroup:/sys/fs/cgroup:rw -provisioner: - name: ansible - config_options: - defaults: - interpreter_python: auto_silent - ssh_connection: - pipelining: false - playbooks: - prepare: prepare.yml - converge: converge.yml - verify: verify.yml - inventory: - host_vars: - localhost: - ansible_python_interpreter: /usr/bin/python3 - env: - ANSIBLE_FORCE_COLOR: "true" - ANSIBLE_REMOTE_TMP: /tmp/.ansible/tmp -verifier: - name: ansible -scenario: - test_sequence: - - cleanup - - destroy - - create - - prepare - - converge - - idempotence - - side_effect - - verify - - cleanup - - destroy diff --git a/molecule/debian/prepare.yml b/molecule/debian/prepare.yml deleted file mode 100644 index 7cab507..0000000 --- a/molecule/debian/prepare.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -- name: Prepare - hosts: all - gather_facts: yes - tasks: - - name: Install sudo - ansible.builtin.apt: - name: - - sudo - - openjdk-21-jdk-headless - - iproute2 diff --git a/molecule/debian/roles b/molecule/debian/roles deleted file mode 120000 index b741aa3..0000000 --- a/molecule/debian/roles +++ /dev/null @@ -1 +0,0 @@ -../../roles \ No newline at end of file diff --git a/molecule/debian/verify.yml b/molecule/debian/verify.yml deleted file mode 100644 index 863b820..0000000 --- a/molecule/debian/verify.yml +++ /dev/null @@ -1,40 +0,0 @@ ---- -- name: Verify - hosts: all - vars: - keycloak_quarkus_bootstrap_admin_user: "remembertochangeme" - keycloak_uri: "http://localhost:{{ 8080 + ( keycloak_jboss_port_offset | default(0) ) }}" - keycloak_management_port: "http://localhost:{{ 9990 + ( keycloak_jboss_port_offset | default(0) ) }}" - keycloak_jboss_port_offset: 10 - tasks: - - name: Populate service facts - ansible.builtin.service_facts: - - - name: Check if keycloak service started - ansible.builtin.assert: - that: - - ansible_facts.services["keycloak.service"]["state"] == "running" - - ansible_facts.services["keycloak.service"]["status"] == "enabled" - - - name: Verify openid config - block: - - name: Fetch openID config # noqa blocked_modules command-instead-of-module - ansible.builtin.shell: | - set -o pipefail - curl http://localhost:8080/realms/master/.well-known/openid-configuration -k | jq . - args: - executable: /bin/bash - delegate_to: localhost - register: openid_config - changed_when: False - - name: Verify endpoint URLs - ansible.builtin.assert: - that: - - (openid_config.stdout | from_json)["backchannel_authentication_endpoint"] == 'http://localhost:8080/realms/master/protocol/openid-connect/ext/ciba/auth' - - (openid_config.stdout | from_json)['issuer'] == 'http://localhost:8080/realms/master' - - (openid_config.stdout | from_json)['authorization_endpoint'] == 'http://localhost:8080/realms/master/protocol/openid-connect/auth' - - (openid_config.stdout | from_json)['token_endpoint'] == 'http://localhost:8080/realms/master/protocol/openid-connect/token' - delegate_to: localhost - when: - - hera_home is defined - - hera_home | length == 0 diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index e617b59..7e73d70 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -1,47 +1,41 @@ --- - name: Converge hosts: all - vars: - keycloak_quarkus_show_deprecation_warnings: false - keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" - keycloak_quarkus_bootstrap_admin_user: "remembertochangeme" - keycloak_quarkus_hostname: http://instance:8080 - keycloak_quarkus_log: file - keycloak_quarkus_log_level: debug - keycloak_quarkus_log_target: /tmp/keycloak - keycloak_quarkus_start_dev: true - keycloak_quarkus_proxy_mode: none - keycloak_quarkus_offline_install: true - keycloak_quarkus_download_path: /tmp/keycloak/ - keycloak_quarkus_java_heap_opts: "-Xms640m -Xmx640m " + vars: + keycloak_admin_password: "remembertochangeme" + keycloak_jvm_package: java-11-openjdk-headless roles: - - role: keycloak_quarkus - - role: keycloak_realm - keycloak_url: "{{ keycloak_quarkus_hostname }}" - keycloak_context: '' - keycloak_admin_user: "{{ keycloak_quarkus_bootstrap_admin_user }}" - keycloak_admin_password: "{{ keycloak_quarkus_bootstrap_admin_password }}" - keycloak_client_users: - - username: TestUser - password: password - client_roles: - - client: TestClient - role: TestRoleUser - realm: "{{ keycloak_realm }}" - - username: TestAdmin - password: password - client_roles: - - client: TestClient - role: TestRoleUser - realm: "{{ keycloak_realm }}" - - client: TestClient - role: TestRoleAdmin - realm: "{{ keycloak_realm }}" - keycloak_realm: TestRealm - keycloak_clients: - - name: TestClient - realm: "{{ keycloak_realm }}" - public_client: "{{ keycloak_client_public }}" - web_origins: "{{ keycloak_client_web_origins }}" - users: "{{ keycloak_client_users }}" - client_id: TestClient + - role: keycloak + tasks: + - name: Keycloak Realm Role + ansible.builtin.include_role: + name: keycloak_realm + vars: + keycloak_client_default_roles: + - TestRoleAdmin + - TestRoleUser + keycloak_client_users: + - username: TestUser + password: password + client_roles: + - client: TestClient + role: TestRoleUser + realm: "{{ keycloak_realm }}" + - username: TestAdmin + password: password + client_roles: + - client: TestClient + role: TestRoleUser + realm: "{{ keycloak_realm }}" + - client: TestClient + role: TestRoleAdmin + realm: "{{ keycloak_realm }}" + keycloak_realm: TestRealm + keycloak_clients: + - name: TestClient + roles: "{{ keycloak_client_default_roles }}" + realm: "{{ keycloak_realm }}" + public_client: "{{ keycloak_client_public }}" + web_origins: "{{ keycloak_client_web_origins }}" + users: "{{ keycloak_client_users }}" + client_id: TestClient diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml index 587a3c8..ea8ad61 100644 --- a/molecule/default/molecule.yml +++ b/molecule/default/molecule.yml @@ -1,17 +1,22 @@ --- +dependency: + name: shell + command: ansible-galaxy collection install -r molecule/default/requirements.yml -p $HOME/.ansible/collections --force-with-deps driver: - name: podman + name: docker +lint: | + ansible-lint --version + ansible-lint -v platforms: - name: instance - image: registry.access.redhat.com/ubi9/ubi-init:latest + image: registry.access.redhat.com/ubi8/ubi-init:latest pre_build_image: true privileged: true command: "/usr/sbin/init" port_bindings: - "8080/tcp" - "8443/tcp" - - "8009/tcp" - - "9000/tcp" + - "8009/tcp" provisioner: name: ansible config_options: @@ -28,15 +33,16 @@ provisioner: localhost: ansible_python_interpreter: "{{ ansible_playbook_python }}" env: - ANSIBLE_FORCE_COLOR: "true" - PROXY: "${PROXY}" - NO_PROXY: "${NO_PROXY}" + ANSIBLE_FORCE_COLOR: "true" verifier: name: ansible scenario: test_sequence: + - dependency + - lint - cleanup - destroy + - syntax - create - prepare - converge diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml index 44d4a91..8137cfd 100644 --- a/molecule/default/prepare.yml +++ b/molecule/default/prepare.yml @@ -1,25 +1,14 @@ --- - name: Prepare hosts: all - gather_facts: yes - vars: - sudo_pkg_name: sudo tasks: - - name: "Run preparation common to all scenario" - ansible.builtin.include_tasks: ../prepare.yml + - name: Disable beta repos + ansible.builtin.command: yum config-manager --disable '*beta*' + ignore_errors: yes - - name: Create controller directory for downloads - ansible.builtin.file: # noqa risky-file-permissions delegated, uses controller host user - path: /tmp/keycloak - state: directory - mode: '0750' - delegate_to: localhost - run_once: true - - - name: Download keycloak archive to controller directory - ansible.builtin.get_url: # noqa risky-file-permissions delegated, uses controller host user - url: https://github.com/keycloak/keycloak/releases/download/26.2.4/keycloak-26.2.4.zip - dest: /tmp/keycloak - mode: '0640' - delegate_to: localhost - run_once: true + - name: Install sudo + ansible.builtin.yum: + name: + - sudo + - java-1.8.0-openjdk + state: present diff --git a/molecule/default/requirements.yml b/molecule/default/requirements.yml new file mode 100644 index 0000000..9aa3437 --- /dev/null +++ b/molecule/default/requirements.yml @@ -0,0 +1,10 @@ +--- +collections: + - name: middleware_automation.redhat_csp_download + version: ">=1.2.1" + - name: middleware_automation.wildfly + version: ">=0.0.5" + - name: community.general + - name: community.docker + version: ">=1.9.1" + diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml index ae21396..07acf4d 100644 --- a/molecule/default/verify.yml +++ b/molecule/default/verify.yml @@ -2,9 +2,10 @@ - name: Verify hosts: all vars: - keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" - keycloak_quarkus_bootstrap_admin_user: "remembertochangeme" - keycloak_uri: "http://localhost:8080" + keycloak_admin_password: "remembertochangeme" + keycloak_jvm_package: java-11-openjdk-headless + keycloak_port: http://localhost:8080 + keycloak_management_port: http://localhost:9990 tasks: - name: Populate service facts ansible.builtin.service_facts: @@ -13,13 +14,16 @@ that: - ansible_facts.services["keycloak.service"]["state"] == "running" - ansible_facts.services["keycloak.service"]["status"] == "enabled" + - name: Verify we are running on requested jvm + shell: | + ps -ef | grep /usr/lib/jvm/java-11 | grep -v grep - name: Verify token api call ansible.builtin.uri: - url: "{{ keycloak_uri }}/realms/master/protocol/openid-connect/token" + url: "{{ keycloak_port }}/auth/realms/master/protocol/openid-connect/token" method: POST - body: "client_id=admin-cli&username={{ keycloak_quarkus_bootstrap_admin_user }}&password={{ keycloak_quarkus_bootstrap_admin_user }}&grant_type=password" + body: "client_id=admin-cli&username=admin&password={{ keycloak_admin_password }}&grant_type=password" validate_certs: no register: keycloak_auth_response until: keycloak_auth_response.status == 200 retries: 2 - delay: 2 + delay: 2 \ No newline at end of file diff --git a/molecule/https_revproxy/converge.yml b/molecule/https_revproxy/converge.yml deleted file mode 100644 index 92994fa..0000000 --- a/molecule/https_revproxy/converge.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -- name: Converge - hosts: all - vars: - keycloak_quarkus_show_deprecation_warnings: false - keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" - keycloak_quarkus_bootstrap_admin_user: "remembertochangeme" - keycloak_quarkus_hostname: https://proxy - keycloak_quarkus_log: file - keycloak_quarkus_http_enabled: True - keycloak_quarkus_http_port: 8080 - keycloak_quarkus_proxy_mode: edge - keycloak_quarkus_http_relative_path: / - keycloak_quarkus_health_check_url: http://proxy:8080/realms/master/.well-known/openid-configuration - roles: - - role: keycloak_quarkus diff --git a/molecule/https_revproxy/molecule.yml b/molecule/https_revproxy/molecule.yml deleted file mode 100644 index 7ad8db8..0000000 --- a/molecule/https_revproxy/molecule.yml +++ /dev/null @@ -1,57 +0,0 @@ ---- -driver: - name: docker -platforms: - - name: instance - image: registry.access.redhat.com/ubi9/ubi-init:latest - pre_build_image: true - privileged: true - command: "/usr/sbin/init" - networks: - - name: keycloak - port_bindings: - - "8080/tcp" - published_ports: - - 0.0.0.0:8080:8080/tcp - - name: proxy - image: registry.access.redhat.com/ubi9/ubi-init:latest - pre_build_image: true - privileged: true - command: "/usr/sbin/init" - networks: - - name: keycloak - port_bindings: - - "443/tcp" - published_ports: - - 0.0.0.0:443:443/tcp -provisioner: - name: ansible - config_options: - defaults: - interpreter_python: auto_silent - ssh_connection: - pipelining: false - playbooks: - prepare: prepare.yml - converge: converge.yml - verify: verify.yml - inventory: - host_vars: - localhost: - ansible_python_interpreter: "{{ ansible_playbook_python }}" - env: - ANSIBLE_FORCE_COLOR: "true" -verifier: - name: ansible -scenario: - test_sequence: - - cleanup - - destroy - - create - - prepare - - converge - - idempotence - - side_effect - - verify - - cleanup - - destroy diff --git a/molecule/https_revproxy/prepare.yml b/molecule/https_revproxy/prepare.yml deleted file mode 100644 index 44018be..0000000 --- a/molecule/https_revproxy/prepare.yml +++ /dev/null @@ -1,49 +0,0 @@ ---- -- name: Prepare - hosts: all - tasks: - - name: Install sudo - ansible.builtin.dnf: - name: sudo - state: present - - - name: "Display hera_home if defined." - ansible.builtin.set_fact: - hera_home: "{{ lookup('env', 'HERA_HOME') }}" - -- name: Prepare proxy - hosts: proxy - vars: - nginx_proxy: | - location / { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://instance:8080; - } - roles: - - elan.simple_nginx_reverse_proxy - pre_tasks: - - name: Create certificate request - ansible.builtin.command: openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -subj '/CN=proxy' - delegate_to: localhost - changed_when: false - - name: Make certificate directory - ansible.builtin.file: - path: /etc/nginx/tls - state: directory - mode: 0755 - - name: Copy certificates - ansible.builtin.copy: - src: "{{ item.name }}" - dest: "{{ item.dest }}" - mode: 0444 - become: true - loop: - - { name: 'cert.pem', dest: '/etc/nginx/tls/certificate.crt' } - - { name: 'key.pem', dest: '/etc/nginx/tls/certificate.key' } - - name: Update CA trust - ansible.builtin.command: update-ca-trust - changed_when: false - become: true diff --git a/molecule/https_revproxy/roles b/molecule/https_revproxy/roles deleted file mode 120000 index b741aa3..0000000 --- a/molecule/https_revproxy/roles +++ /dev/null @@ -1 +0,0 @@ -../../roles \ No newline at end of file diff --git a/molecule/https_revproxy/verify.yml b/molecule/https_revproxy/verify.yml deleted file mode 100644 index 4a69cb2..0000000 --- a/molecule/https_revproxy/verify.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -- name: Verify - hosts: instance - tasks: - - name: Populate service facts - ansible.builtin.service_facts: - - - name: Check if keycloak service started - ansible.builtin.assert: - that: - - ansible_facts.services["keycloak.service"]["state"] == "running" - - ansible_facts.services["keycloak.service"]["status"] == "enabled" - - - name: Verify openid config - block: - - name: Fetch openID config # noqa blocked_modules command-instead-of-module - ansible.builtin.uri: - url: http://localhost:8080/realms/master/.well-known/openid-configuration - validate_certs: false - headers: - Host: proxy - register: openid_config - changed_when: False - - name: Verify endpoint URLs - ansible.builtin.assert: - that: - - openid_config.json['issuer'] == 'https://proxy/realms/master' - - openid_config.json['authorization_endpoint'] == 'https://proxy/realms/master/protocol/openid-connect/auth' diff --git a/molecule/overridexml/converge.yml b/molecule/overridexml/converge.yml index 7537684..9304eba 100644 --- a/molecule/overridexml/converge.yml +++ b/molecule/overridexml/converge.yml @@ -1,11 +1,43 @@ --- - name: Converge hosts: all - vars: + vars: keycloak_admin_password: "remembertochangeme" keycloak_config_override_template: custom.xml.j2 keycloak_http_port: 8081 keycloak_management_http_port: 19990 - keycloak_service_runas: True roles: - role: keycloak + tasks: + - name: Keycloak Realm Role + ansible.builtin.include_role: + name: keycloak_realm + vars: + keycloak_client_default_roles: + - TestRoleAdmin + - TestRoleUser + keycloak_client_users: + - username: TestUser + password: password + client_roles: + - client: TestClient + role: TestRoleUser + realm: "{{ keycloak_realm }}" + - username: TestAdmin + password: password + client_roles: + - client: TestClient + role: TestRoleUser + realm: "{{ keycloak_realm }}" + - client: TestClient + role: TestRoleAdmin + realm: "{{ keycloak_realm }}" + keycloak_realm: TestRealm + keycloak_clients: + - name: TestClient + roles: "{{ keycloak_client_default_roles }}" + realm: "{{ keycloak_realm }}" + public_client: "{{ keycloak_client_public }}" + web_origins: "{{ keycloak_client_web_origins }}" + users: "{{ keycloak_client_users }}" + client_id: TestClient diff --git a/molecule/overridexml/molecule.yml b/molecule/overridexml/molecule.yml index 011c08e..ea8ad61 100644 --- a/molecule/overridexml/molecule.yml +++ b/molecule/overridexml/molecule.yml @@ -1,17 +1,22 @@ --- +dependency: + name: shell + command: ansible-galaxy collection install -r molecule/default/requirements.yml -p $HOME/.ansible/collections --force-with-deps driver: name: docker +lint: | + ansible-lint --version + ansible-lint -v platforms: - name: instance - image: registry.access.redhat.com/ubi9/ubi-init:latest + image: registry.access.redhat.com/ubi8/ubi-init:latest pre_build_image: true privileged: true command: "/usr/sbin/init" port_bindings: - "8080/tcp" - "8443/tcp" - - "8009/tcp" - - "9000/tcp" + - "8009/tcp" provisioner: name: ansible config_options: @@ -28,13 +33,16 @@ provisioner: localhost: ansible_python_interpreter: "{{ ansible_playbook_python }}" env: - ANSIBLE_FORCE_COLOR: "true" + ANSIBLE_FORCE_COLOR: "true" verifier: name: ansible scenario: test_sequence: + - dependency + - lint - cleanup - destroy + - syntax - create - prepare - converge diff --git a/molecule/overridexml/prepare.yml b/molecule/overridexml/prepare.yml index 26245be..03433c0 100644 --- a/molecule/overridexml/prepare.yml +++ b/molecule/overridexml/prepare.yml @@ -1,12 +1,12 @@ --- - name: Prepare hosts: all - gather_facts: yes - vars: - sudo_pkg_name: sudo tasks: - - name: "Run preparation common to all scenario" - ansible.builtin.include_tasks: ../prepare.yml - vars: - assets: - - "{{ assets_server }}/sso/7.6.0/rh-sso-7.6.0-server-dist.zip" + - name: Disable beta repos + ansible.builtin.command: yum config-manager --disable '*beta*' + ignore_errors: yes + + - name: Install sudo + ansible.builtin.yum: + name: sudo + state: present diff --git a/molecule/overridexml/requirements.yml b/molecule/overridexml/requirements.yml new file mode 100644 index 0000000..9aa3437 --- /dev/null +++ b/molecule/overridexml/requirements.yml @@ -0,0 +1,10 @@ +--- +collections: + - name: middleware_automation.redhat_csp_download + version: ">=1.2.1" + - name: middleware_automation.wildfly + version: ">=0.0.5" + - name: community.general + - name: community.docker + version: ">=1.9.1" + diff --git a/molecule/overridexml/templates/custom.xml.j2 b/molecule/overridexml/templates/custom.xml.j2 index ec801d3..a59561a 100644 --- a/molecule/overridexml/templates/custom.xml.j2 +++ b/molecule/overridexml/templates/custom.xml.j2 @@ -1,5 +1,5 @@ - + @@ -15,6 +15,7 @@ + @@ -29,6 +30,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -43,8 +69,8 @@ - - + + @@ -179,9 +205,6 @@ - - - @@ -255,13 +278,6 @@ - - - - - - - @@ -481,8 +497,8 @@ default - - + + @@ -497,9 +513,41 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -520,11 +568,10 @@ - - + - + @@ -534,25 +581,20 @@ - - - - + - + - - diff --git a/molecule/overridexml/verify.yml b/molecule/overridexml/verify.yml index b267fa1..ef973cd 100644 --- a/molecule/overridexml/verify.yml +++ b/molecule/overridexml/verify.yml @@ -1,10 +1,6 @@ --- - name: Verify hosts: all - vars: - keycloak_uri: "http://localhost:8081" - keycloak_management_port: "http://localhost:19990" - keycloak_admin_password: "remembertochangeme" tasks: - name: Populate service facts ansible.builtin.service_facts: @@ -13,20 +9,3 @@ that: - ansible_facts.services["keycloak.service"]["state"] == "running" - ansible_facts.services["keycloak.service"]["status"] == "enabled" - - name: Verify we are running on requested jvm # noqa blocked_modules command-instead-of-module - ansible.builtin.shell: | - set -o pipefail - ps -ef | grep '/etc/alternatives/jre_1.8.0/' | grep -v grep - args: - executable: /bin/bash - changed_when: no - - name: Verify token api call - ansible.builtin.uri: - url: "{{ keycloak_uri }}/auth/realms/master/protocol/openid-connect/token" - method: POST - body: "client_id=admin-cli&username=admin&password={{ keycloak_admin_password }}&grant_type=password" - validate_certs: no - register: keycloak_auth_response - until: keycloak_auth_response.status == 200 - retries: 2 - delay: 2 diff --git a/molecule/prepare.yml b/molecule/prepare.yml deleted file mode 100644 index 27486a3..0000000 --- a/molecule/prepare.yml +++ /dev/null @@ -1,58 +0,0 @@ ---- -- name: Display Ansible version - ansible.builtin.debug: - msg: "Ansible version is {{ ansible_version.full }}" - -- name: "Set package name for sudo" - ansible.builtin.set_fact: - sudo_pkg_name: sudo - -- name: "Ensure {{ sudo_pkg_name }} is installed (if user is root)." - ansible.builtin.yum: - name: "{{ sudo_pkg_name }}" - state: present - when: - - ansible_user_id == 'root' - -- name: Gather the package facts - ansible.builtin.package_facts: - manager: auto - -- name: "Check if sudo is installed." - ansible.builtin.assert: - that: - - sudo_pkg_name in ansible_facts.packages - fail_msg: "sudo is not installed on target system" - -- name: "Install iproute" - become: true - ansible.builtin.yum: - name: - - iproute - state: present - -- name: "Retrieve assets server from env" - ansible.builtin.set_fact: - assets_server: "{{ lookup('env', 'MIDDLEWARE_DOWNLOAD_RELEASE_SERVER_URL') }}" - -- name: "Download artefacts only if assets_server is set" - when: - - assets_server is defined - - assets_server | length > 0 - - assets is defined - - assets | length > 0 - block: - - name: "Set offline when assets server from env is defined" - ansible.builtin.set_fact: - sso_offline_install: True - - - name: "Download and deploy zips from {{ assets_server }}" - ansible.builtin.get_url: - url: "{{ asset }}" - dest: "{{ lookup('env', 'PWD') }}" - validate_certs: no - mode: '0644' - delegate_to: localhost - loop: "{{ assets }}" - loop_control: - loop_var: asset diff --git a/molecule/quarkus/converge.yml b/molecule/quarkus/converge.yml index fa2d70f..1f77fe5 100644 --- a/molecule/quarkus/converge.yml +++ b/molecule/quarkus/converge.yml @@ -1,69 +1,14 @@ --- - name: Converge hosts: all - vars: - keycloak_quarkus_show_deprecation_warnings: false - keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" - keycloak_quarkus_bootstrap_admin_user: "remembertochangeme" + vars: + keycloak_quarkus_admin_pass: "remembertochangeme" + keycloak_admin_password: "remembertochangeme" keycloak_realm: TestRealm - keycloak_quarkus_hostname: https://instance:8443 - keycloak_quarkus_log: file - keycloak_quarkus_log_level: debug # needed for the verify step - keycloak_quarkus_https_key_file_enabled: true - keycloak_quarkus_key_file_copy_enabled: true - keycloak_quarkus_key_content: "{{ lookup('file', 'key.pem') }}" - keycloak_quarkus_cert_file_copy_enabled: true - keycloak_quarkus_cert_file_src: cert.pem - keycloak_quarkus_log_target: /tmp/keycloak - keycloak_quarkus_ks_vault_enabled: true - keycloak_quarkus_ks_vault_file: "/opt/keycloak/vault/keystore.p12" - keycloak_quarkus_ks_vault_pass: keystorepassword - keycloak_quarkus_systemd_wait_for_port: true - keycloak_quarkus_systemd_wait_for_timeout: 20 - keycloak_quarkus_systemd_wait_for_delay: 2 - keycloak_quarkus_systemd_wait_for_log: true - keycloak_quarkus_restart_health_check: false # would fail because of self-signed cert - keycloak_quarkus_version: 26.2.4 - keycloak_quarkus_java_heap_opts: "-Xms1024m -Xmx1024m" - keycloak_quarkus_additional_env_vars: - - key: KC_FEATURES_DISABLED - value: impersonation,kerberos - keycloak_quarkus_providers: - - id: http-client - spi: connections - default: true - restart: true - properties: - - key: default-connection-pool-size - value: 10 - - id: spid-saml - url: https://github.com/italia/spid-keycloak-provider/releases/download/24.0.2/spid-provider.jar - - id: spid-saml-w-checksum - url: https://github.com/italia/spid-keycloak-provider/releases/download/24.0.2/spid-provider.jar - checksum: sha256:fbb50e73739d7a6d35b5bff611b1c01668b29adf6f6259624b95e466a305f377 - - id: keycloak-kerberos-federation - maven: - repository_url: https://repo1.maven.org/maven2/ # https://mvnrepository.com/artifact/org.keycloak/keycloak-kerberos-federation/24.0.4 - group_id: org.keycloak - artifact_id: keycloak-kerberos-federation - version: 26.2.4 # optional - # username: myUser # optional - # password: myPAT # optional - # - id: my-static-theme - # local_path: /tmp/my-static-theme.jar - keycloak_quarkus_policies: - - name: "cain-and-abel.txt" - url: "https://github.com/danielmiessler/SecLists/raw/master/Passwords/Software/cain-and-abel.txt" - - name: "john-the-ripper.txt" - url: "https://github.com/danielmiessler/SecLists/raw/master/Passwords/Software/john-the-ripper.txt" - type: password-blacklists roles: - role: keycloak_quarkus - role: keycloak_realm - keycloak_url: http://instance:8080 keycloak_context: '' - keycloak_admin_user: "{{ keycloak_quarkus_bootstrap_admin_user }}" - keycloak_admin_password: "{{ keycloak_quarkus_bootstrap_admin_password }}" keycloak_client_default_roles: - TestRoleAdmin - TestRoleUser diff --git a/molecule/quarkus/molecule.yml b/molecule/quarkus/molecule.yml index 20ca3bc..ea8ad61 100644 --- a/molecule/quarkus/molecule.yml +++ b/molecule/quarkus/molecule.yml @@ -1,19 +1,22 @@ --- +dependency: + name: shell + command: ansible-galaxy collection install -r molecule/default/requirements.yml -p $HOME/.ansible/collections --force-with-deps driver: name: docker +lint: | + ansible-lint --version + ansible-lint -v platforms: - name: instance - image: registry.access.redhat.com/ubi9/ubi-init:latest + image: registry.access.redhat.com/ubi8/ubi-init:latest pre_build_image: true privileged: true command: "/usr/sbin/init" port_bindings: - "8080/tcp" - "8443/tcp" - - "8009/tcp" - - "9000/tcp" - published_ports: - - 0.0.0.0:8443:8443/tcp + - "8009/tcp" provisioner: name: ansible config_options: @@ -30,16 +33,16 @@ provisioner: localhost: ansible_python_interpreter: "{{ ansible_playbook_python }}" env: - ANSIBLE_FORCE_COLOR: "true" - PYTHONHTTPSVERIFY: 0 - PROXY: "${PROXY}" - NO_PROXY: "${NO_PROXY}" + ANSIBLE_FORCE_COLOR: "true" verifier: name: ansible scenario: test_sequence: + - dependency + - lint - cleanup - destroy + - syntax - create - prepare - converge diff --git a/molecule/quarkus/prepare.yml b/molecule/quarkus/prepare.yml index abe2518..03433c0 100644 --- a/molecule/quarkus/prepare.yml +++ b/molecule/quarkus/prepare.yml @@ -2,43 +2,11 @@ - name: Prepare hosts: all tasks: - - name: "Display hera_home if defined." - ansible.builtin.set_fact: - hera_home: "{{ lookup('env', 'HERA_HOME') }}" + - name: Disable beta repos + ansible.builtin.command: yum config-manager --disable '*beta*' + ignore_errors: yes - - name: "Ensure common prepare phase are set." - ansible.builtin.include_tasks: ../prepare.yml - - - name: Create certificate request - ansible.builtin.command: openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -subj '/CN=instance' - delegate_to: localhost - changed_when: false - - - name: Create vault directory - become: true - ansible.builtin.file: - state: directory - path: "/opt/keycloak/vault" - mode: '0755' - - - name: Make sure a jre is available (for keytool to prepare keystore) - delegate_to: localhost - ansible.builtin.package: - name: java-21-openjdk-headless + - name: Install sudo + ansible.builtin.yum: + name: sudo state: present - become: true - failed_when: false - - - name: Create vault keystore - ansible.builtin.command: keytool -importpass -alias TestRealm_testalias -keystore keystore.p12 -storepass keystorepassword - delegate_to: localhost - register: keytool_cmd - changed_when: False - failed_when: not 'already exists' in keytool_cmd.stdout and keytool_cmd.rc != 0 - - - name: Copy certificates and vault - become: true - ansible.builtin.copy: - src: keystore.p12 - dest: /opt/keycloak/vault/keystore.p12 - mode: '0444' diff --git a/molecule/quarkus/requirements.yml b/molecule/quarkus/requirements.yml new file mode 100644 index 0000000..9aa3437 --- /dev/null +++ b/molecule/quarkus/requirements.yml @@ -0,0 +1,10 @@ +--- +collections: + - name: middleware_automation.redhat_csp_download + version: ">=1.2.1" + - name: middleware_automation.wildfly + version: ">=0.0.5" + - name: community.general + - name: community.docker + version: ">=1.9.1" + diff --git a/molecule/quarkus/verify.yml b/molecule/quarkus/verify.yml index 1d9d2c3..ef973cd 100644 --- a/molecule/quarkus/verify.yml +++ b/molecule/quarkus/verify.yml @@ -1,128 +1,11 @@ --- - name: Verify hosts: all - vars: - keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" - keycloak_quarkus_bootstrap_admin_user: "remembertochangeme" tasks: - name: Populate service facts ansible.builtin.service_facts: - - name: Check if keycloak service started ansible.builtin.assert: that: - ansible_facts.services["keycloak.service"]["state"] == "running" - ansible_facts.services["keycloak.service"]["status"] == "enabled" - fail_msg: "Service not running" - - - name: Set internal envvar - ansible.builtin.set_fact: - hera_home: "{{ lookup('env', 'HERA_HOME') }}" - - - name: Verify openid config - when: - - hera_home is defined - - hera_home | length == 0 - block: - - name: Fetch openID config # noqa blocked_modules command-instead-of-module - ansible.builtin.shell: | - set -o pipefail - curl -H 'Host: instance' https://localhost:8443/realms/master/.well-known/openid-configuration -k | jq . - args: - executable: /bin/bash - delegate_to: localhost - register: openid_config - changed_when: False - - name: Verify endpoint URLs - ansible.builtin.assert: - that: - - (openid_config.stdout | from_json)["backchannel_authentication_endpoint"] == 'https://instance:8443/realms/master/protocol/openid-connect/ext/ciba/auth' - - (openid_config.stdout | from_json)['issuer'] == 'https://instance:8443/realms/master' - - (openid_config.stdout | from_json)['authorization_endpoint'] == 'https://instance:8443/realms/master/protocol/openid-connect/auth' - - (openid_config.stdout | from_json)['token_endpoint'] == 'https://instance:8443/realms/master/protocol/openid-connect/token' - delegate_to: localhost - - - name: Check log folder - ansible.builtin.stat: - path: /tmp/keycloak - register: keycloak_log_folder - - - name: Check that keycloak log folder exists and is a link - ansible.builtin.assert: - that: - - keycloak_log_folder.stat.exists - - not keycloak_log_folder.stat.isdir - - keycloak_log_folder.stat.islnk - fail_msg: "Service log symlink not correctly created" - - - name: Check log file - become: true - ansible.builtin.stat: - path: /tmp/keycloak/keycloak.log - register: keycloak_log_file - - - name: Check if keycloak file exists - ansible.builtin.assert: - that: - - keycloak_log_file.stat.exists - - not keycloak_log_file.stat.isdir - - - name: Check default log folder - become: yes - ansible.builtin.stat: - path: /var/log/keycloak - register: keycloak_default_log_folder - failed_when: false - - - name: Check that default keycloak log folder doesn't exist - ansible.builtin.assert: - that: - - not keycloak_default_log_folder.stat.exists - - - name: Verify vault SPI in logfile - become: true - ansible.builtin.shell: | - set -o pipefail - zgrep 'Configured KeystoreVaultProviderFactory with the keystore file' /opt/keycloak/keycloak-*/data/log/keycloak.log*zip - changed_when: false - failed_when: slurped_log.rc != 0 - register: slurped_log - - - name: Verify token api call - ansible.builtin.uri: - url: "https://instance:8443/realms/master/protocol/openid-connect/token" - method: POST - body: "client_id=admin-cli&username={{ keycloak_quarkus_bootstrap_admin_user }}&password={{ keycloak_quarkus_bootstrap_admin_password}}&grant_type=password" - validate_certs: no - register: keycloak_auth_response - until: keycloak_auth_response.status == 200 - retries: 2 - delay: 2 - - - name: "Get Clients" - ansible.builtin.uri: - url: "https://instance:8443/admin/realms/TestRealm/clients" - validate_certs: false - headers: - Authorization: "Bearer {{ keycloak_auth_response.json.access_token }}" - register: keycloak_clients - - - name: Get client uuid - ansible.builtin.set_fact: - keycloak_client_uuid: "{{ ((keycloak_clients.json | selectattr('clientId', '==', 'TestClient')) | first).id }}" - - - name: "Get Client {{ keycloak_client_uuid }}" - ansible.builtin.uri: - url: "https://instance:8443/admin/realms/TestRealm/clients/{{ keycloak_client_uuid }}" - validate_certs: false - headers: - Authorization: "Bearer {{ keycloak_auth_response.json.access_token }}" - register: keycloak_test_client - - - name: "Get Client roles" - ansible.builtin.uri: - url: "https://instance:8443/admin/realms/TestRealm/clients/{{ keycloak_client_uuid }}/roles" - validate_certs: false - headers: - Authorization: "Bearer {{ keycloak_auth_response.json.access_token }}" - register: keycloak_test_client_roles diff --git a/molecule/quarkus_devmode/converge.yml b/molecule/quarkus_devmode/converge.yml deleted file mode 100644 index a596478..0000000 --- a/molecule/quarkus_devmode/converge.yml +++ /dev/null @@ -1,50 +0,0 @@ ---- -- name: Converge - hosts: all - vars: - keycloak_quarkus_show_deprecation_warnings: false - keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" - keycloak_quarkus_bootstrap_admin_user: "remembertochangeme" - keycloak_realm: TestRealm - keycloak_quarkus_log: file - keycloak_quarkus_hostname: 'http://localhost:8080' - keycloak_quarkus_start_dev: True - keycloak_quarkus_proxy_mode: none - keycloak_quarkus_java_home: /opt/openjdk/ - keycloak_quarkus_java_heap_opts: "-Xms640m -Xmx640m" - - roles: - - role: keycloak_quarkus - - role: keycloak_realm - keycloak_url: "{{ keycloak_quarkus_hostname }}" - keycloak_context: '' - keycloak_admin_user: "{{ keycloak_quarkus_bootstrap_admin_user }}" - keycloak_admin_password: "{{ keycloak_quarkus_bootstrap_admin_password }}" - keycloak_client_default_roles: - - TestRoleAdmin - - TestRoleUser - keycloak_client_users: - - username: TestUser - password: password - client_roles: - - client: TestClient - role: TestRoleUser - realm: "{{ keycloak_realm }}" - - username: TestAdmin - password: password - client_roles: - - client: TestClient - role: TestRoleUser - realm: "{{ keycloak_realm }}" - - client: TestClient - role: TestRoleAdmin - realm: "{{ keycloak_realm }}" - keycloak_realm: TestRealm - keycloak_clients: - - name: TestClient - roles: "{{ keycloak_client_default_roles }}" - realm: "{{ keycloak_realm }}" - public_client: "{{ keycloak_client_public }}" - web_origins: "{{ keycloak_client_web_origins }}" - users: "{{ keycloak_client_users }}" - client_id: TestClient diff --git a/molecule/quarkus_devmode/molecule.yml b/molecule/quarkus_devmode/molecule.yml deleted file mode 100644 index 0ae28b3..0000000 --- a/molecule/quarkus_devmode/molecule.yml +++ /dev/null @@ -1,49 +0,0 @@ ---- -driver: - name: podman -platforms: - - name: instance - image: registry.access.redhat.com/ubi9/ubi-init:latest - pre_build_image: true - privileged: true - command: "/usr/sbin/init" - port_bindings: - - "8080/tcp" - - "8009/tcp" - - "9000/tcp" - published_ports: - - 0.0.0.0:8080:8080/tcp - - 0.0.0.0:9000:9000/TCP -provisioner: - name: ansible - config_options: - defaults: - interpreter_python: auto_silent - ssh_connection: - pipelining: false - playbooks: - prepare: prepare.yml - converge: converge.yml - verify: verify.yml - inventory: - host_vars: - localhost: - ansible_python_interpreter: "{{ ansible_playbook_python }}" - env: - ANSIBLE_FORCE_COLOR: "true" - PROXY: "${PROXY}" - NO_PROXY: "${NO_PROXY}" -verifier: - name: ansible -scenario: - test_sequence: - - cleanup - - destroy - - create - - prepare - - converge - - idempotence - - side_effect - - verify - - cleanup - - destroy diff --git a/molecule/quarkus_devmode/prepare.yml b/molecule/quarkus_devmode/prepare.yml deleted file mode 100644 index 9ce721e..0000000 --- a/molecule/quarkus_devmode/prepare.yml +++ /dev/null @@ -1,49 +0,0 @@ ---- -- name: Prepare - hosts: all - tasks: - - name: Install sudo - ansible.builtin.apt: - name: - - sudo - - openjdk-17-jdk-headless - state: present - when: - - ansible_facts.os_family == 'Debian' - - - name: "Ensure common prepare phase are set." - ansible.builtin.include_tasks: ../prepare.yml - - - name: Install JDK17 - become: yes - ansible.builtin.yum: - name: - - java-17-openjdk-headless - state: present - when: - - ansible_facts.os_family == 'RedHat' - - - name: Link default logs directory - become: yes - ansible.builtin.file: - state: link - src: "{{ item }}" - dest: /opt/openjdk - force: true - with_fileglob: - - /usr/lib/jvm/java-17-openjdk* - when: - - ansible_facts.os_family == "Debian" - - - name: Link default logs directory - ansible.builtin.file: - state: link - src: /usr/lib/jvm/jre-17-openjdk - dest: /opt/openjdk - force: true - when: - - ansible_facts.os_family == "RedHat" - - - name: "Display hera_home if defined." - ansible.builtin.set_fact: - hera_home: "{{ lookup('env', 'HERA_HOME') }}" diff --git a/molecule/quarkus_devmode/roles b/molecule/quarkus_devmode/roles deleted file mode 120000 index b741aa3..0000000 --- a/molecule/quarkus_devmode/roles +++ /dev/null @@ -1 +0,0 @@ -../../roles \ No newline at end of file diff --git a/molecule/quarkus_devmode/verify.yml b/molecule/quarkus_devmode/verify.yml deleted file mode 100644 index b808ece..0000000 --- a/molecule/quarkus_devmode/verify.yml +++ /dev/null @@ -1,47 +0,0 @@ ---- -- name: Verify - hosts: all - tasks: - - name: Populate service facts - ansible.builtin.service_facts: - - - name: Check if keycloak service started - ansible.builtin.assert: - that: - - ansible_facts.services["keycloak.service"]["state"] == "running" - - ansible_facts.services["keycloak.service"]["status"] == "enabled" - - - name: Verify we are running on requested JAVA_HOME # noqa blocked_modules command-instead-of-module - ansible.builtin.shell: | - set -o pipefail - ps -ef | grep '/opt/openjdk' | grep -v grep - args: - executable: /bin/bash - changed_when: False - - - name: Set internal envvar - ansible.builtin.set_fact: - hera_home: "{{ lookup('env', 'HERA_HOME') }}" - - - name: Verify openid config - block: - - name: Fetch openID config # noqa blocked_modules command-instead-of-module - ansible.builtin.shell: | - set -o pipefail - curl http://localhost:8080/realms/master/.well-known/openid-configuration -k | jq . - args: - executable: /bin/bash - delegate_to: localhost - register: openid_config - changed_when: False - - name: Verify endpoint URLs - ansible.builtin.assert: - that: - - (openid_config.stdout | from_json)["backchannel_authentication_endpoint"] == 'http://localhost:8080/realms/master/protocol/openid-connect/ext/ciba/auth' - - (openid_config.stdout | from_json)['issuer'] == 'http://localhost:8080/realms/master' - - (openid_config.stdout | from_json)['authorization_endpoint'] == 'http://localhost:8080/realms/master/protocol/openid-connect/auth' - - (openid_config.stdout | from_json)['token_endpoint'] == 'http://localhost:8080/realms/master/protocol/openid-connect/token' - delegate_to: localhost - when: - - hera_home is defined - - hera_home | length == 0 diff --git a/molecule/quarkus_ha/converge.yml b/molecule/quarkus_ha/converge.yml deleted file mode 100644 index fa5314f..0000000 --- a/molecule/quarkus_ha/converge.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -- name: Converge - hosts: keycloak - vars: - keycloak_quarkus_show_deprecation_warnings: false - keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" - keycloak_quarkus_bootstrap_admin_user: "remembertochangeme" - keycloak_quarkus_hostname: "http://{{ inventory_hostname }}:8080" - keycloak_quarkus_log: file - keycloak_quarkus_log_level: info - keycloak_quarkus_https_key_file_enabled: true - keycloak_quarkus_key_file_copy_enabled: true - keycloak_quarkus_key_content: "{{ lookup('file', inventory_hostname + '.key') }}" - keycloak_quarkus_cert_file_copy_enabled: true - keycloak_quarkus_cert_file_src: "{{ inventory_hostname }}.pem" - keycloak_quarkus_ks_vault_enabled: true - keycloak_quarkus_ks_vault_file: "/opt/keycloak/vault/keystore.p12" - keycloak_quarkus_ks_vault_pass: keystorepassword - keycloak_quarkus_systemd_wait_for_port: true - keycloak_quarkus_systemd_wait_for_timeout: 20 - keycloak_quarkus_systemd_wait_for_delay: 2 - keycloak_quarkus_systemd_wait_for_log: true - keycloak_quarkus_ha_enabled: true - keycloak_quarkus_restart_strategy: restart/serial.yml - keycloak_quarkus_db_user: keycloak - keycloak_quarkus_db_pass: mysecretpass - keycloak_quarkus_db_url: jdbc:postgresql://postgres:5432/keycloak - roles: - - role: keycloak_quarkus diff --git a/molecule/quarkus_ha/molecule.yml b/molecule/quarkus_ha/molecule.yml deleted file mode 100644 index ed09971..0000000 --- a/molecule/quarkus_ha/molecule.yml +++ /dev/null @@ -1,82 +0,0 @@ ---- -driver: - name: docker -platforms: - - name: instance1 - image: registry.access.redhat.com/ubi9/ubi-init:latest - pre_build_image: true - privileged: true - command: "/usr/sbin/init" - groups: - - keycloak - networks: - - name: rhbk - port_bindings: - - "8080/tcp" - - "8443/tcp" - - "9000/tcp" - - name: instance2 - image: registry.access.redhat.com/ubi9/ubi-init:latest - pre_build_image: true - privileged: true - command: "/usr/sbin/init" - groups: - - keycloak - networks: - - name: rhbk - port_bindings: - - "8080/tcp" - - "8443/tcp" - - "9000/tcp" - - name: postgres - image: ubuntu/postgres:14-22.04_beta - pre_build_image: true - privileged: true - command: postgres - groups: - - database - networks: - - name: rhbk - port_bindings: - - "5432/tcp" - mounts: - - type: bind - target: /etc/postgresql/postgresql.conf - source: ${PWD}/molecule/quarkus_ha/postgresql/postgresql.conf - env: - POSTGRES_USER: keycloak - POSTGRES_PASSWORD: mysecretpass - POSTGRES_DB: keycloak - POSTGRES_HOST_AUTH_METHOD: trust -provisioner: - name: ansible - config_options: - defaults: - interpreter_python: auto_silent - ssh_connection: - pipelining: false - playbooks: - prepare: prepare.yml - converge: converge.yml - verify: verify.yml - inventory: - host_vars: - localhost: - ansible_python_interpreter: "{{ ansible_playbook_python }}" - env: - ANSIBLE_FORCE_COLOR: "true" - PYTHONHTTPSVERIFY: 0 -verifier: - name: ansible -scenario: - test_sequence: - - cleanup - - destroy - - create - - prepare - - converge - - idempotence - - side_effect - - verify - - cleanup - - destroy diff --git a/molecule/quarkus_ha/postgresql/postgresql.conf b/molecule/quarkus_ha/postgresql/postgresql.conf deleted file mode 100644 index d702576..0000000 --- a/molecule/quarkus_ha/postgresql/postgresql.conf +++ /dev/null @@ -1,750 +0,0 @@ -# ----------------------------- -# PostgreSQL configuration file -# ----------------------------- -# -# This file consists of lines of the form: -# -# name = value -# -# (The "=" is optional.) Whitespace may be used. Comments are introduced with -# "#" anywhere on a line. The complete list of parameter names and allowed -# values can be found in the PostgreSQL documentation. -# -# The commented-out settings shown in this file represent the default values. -# Re-commenting a setting is NOT sufficient to revert it to the default value; -# you need to reload the server. -# -# This file is read on server startup and when the server receives a SIGHUP -# signal. If you edit the file on a running system, you have to SIGHUP the -# server for the changes to take effect, run "pg_ctl reload", or execute -# "SELECT pg_reload_conf()". Some parameters, which are marked below, -# require a server shutdown and restart to take effect. -# -# Any parameter can also be given as a command-line option to the server, e.g., -# "postgres -c log_connections=on". Some parameters can be changed at run time -# with the "SET" SQL command. -# -# Memory units: kB = kilobytes Time units: ms = milliseconds -# MB = megabytes s = seconds -# GB = gigabytes min = minutes -# TB = terabytes h = hours -# d = days - - -#------------------------------------------------------------------------------ -# FILE LOCATIONS -#------------------------------------------------------------------------------ - -# The default values of these variables are driven from the -D command-line -# option or PGDATA environment variable, represented here as ConfigDir. - -#data_directory = 'ConfigDir' # use data in another directory - # (change requires restart) -#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file - # (change requires restart) -#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file - # (change requires restart) - -# If external_pid_file is not explicitly set, no extra PID file is written. -#external_pid_file = '' # write an extra PID file - # (change requires restart) - - -#------------------------------------------------------------------------------ -# CONNECTIONS AND AUTHENTICATION -#------------------------------------------------------------------------------ - -# - Connection Settings - - -listen_addresses = '*' # what IP address(es) to listen on; - # comma-separated list of addresses; - # defaults to 'localhost'; use '*' for all - # (change requires restart) -#port = 5432 # (change requires restart) -#max_connections = 100 # (change requires restart) -#superuser_reserved_connections = 3 # (change requires restart) -#unix_socket_directories = '/tmp' # comma-separated list of directories - # (change requires restart) -#unix_socket_group = '' # (change requires restart) -#unix_socket_permissions = 0777 # begin with 0 to use octal notation - # (change requires restart) -#bonjour = off # advertise server via Bonjour - # (change requires restart) -#bonjour_name = '' # defaults to the computer name - # (change requires restart) - -# - TCP settings - -# see "man 7 tcp" for details - -#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; - # 0 selects the system default -#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; - # 0 selects the system default -#tcp_keepalives_count = 0 # TCP_KEEPCNT; - # 0 selects the system default -#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds; - # 0 selects the system default - -# - Authentication - - -#authentication_timeout = 1min # 1s-600s -#password_encryption = md5 # md5 or scram-sha-256 -#db_user_namespace = off - -# GSSAPI using Kerberos -#krb_server_keyfile = '' -#krb_caseins_users = off - -# - SSL - - -#ssl = off -#ssl_ca_file = '' -#ssl_cert_file = 'server.crt' -#ssl_crl_file = '' -#ssl_key_file = 'server.key' -#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers -#ssl_prefer_server_ciphers = on -#ssl_ecdh_curve = 'prime256v1' -#ssl_min_protocol_version = 'TLSv1' -#ssl_max_protocol_version = '' -#ssl_dh_params_file = '' -#ssl_passphrase_command = '' -#ssl_passphrase_command_supports_reload = off - - -#------------------------------------------------------------------------------ -# RESOURCE USAGE (except WAL) -#------------------------------------------------------------------------------ - -# - Memory - - -#shared_buffers = 32MB # min 128kB - # (change requires restart) -#huge_pages = try # on, off, or try - # (change requires restart) -#temp_buffers = 8MB # min 800kB -#max_prepared_transactions = 0 # zero disables the feature - # (change requires restart) -# Caution: it is not advisable to set max_prepared_transactions nonzero unless -# you actively intend to use prepared transactions. -#work_mem = 4MB # min 64kB -#maintenance_work_mem = 64MB # min 1MB -#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem -#max_stack_depth = 2MB # min 100kB -#shared_memory_type = mmap # the default is the first option - # supported by the operating system: - # mmap - # sysv - # windows - # (change requires restart) -#dynamic_shared_memory_type = posix # the default is the first option - # supported by the operating system: - # posix - # sysv - # windows - # mmap - # (change requires restart) - -# - Disk - - -#temp_file_limit = -1 # limits per-process temp file space - # in kB, or -1 for no limit - -# - Kernel Resources - - -#max_files_per_process = 1000 # min 25 - # (change requires restart) - -# - Cost-Based Vacuum Delay - - -#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables) -#vacuum_cost_page_hit = 1 # 0-10000 credits -#vacuum_cost_page_miss = 10 # 0-10000 credits -#vacuum_cost_page_dirty = 20 # 0-10000 credits -#vacuum_cost_limit = 200 # 1-10000 credits - -# - Background Writer - - -#bgwriter_delay = 200ms # 10-10000ms between rounds -#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables -#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round -#bgwriter_flush_after = 0 # measured in pages, 0 disables - -# - Asynchronous Behavior - - -#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching -#max_worker_processes = 8 # (change requires restart) -#max_parallel_maintenance_workers = 2 # taken from max_parallel_workers -#max_parallel_workers_per_gather = 2 # taken from max_parallel_workers -#parallel_leader_participation = on -#max_parallel_workers = 8 # maximum number of max_worker_processes that - # can be used in parallel operations -#old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate - # (change requires restart) -#backend_flush_after = 0 # measured in pages, 0 disables - - -#------------------------------------------------------------------------------ -# WRITE-AHEAD LOG -#------------------------------------------------------------------------------ - -# - Settings - - -#wal_level = replica # minimal, replica, or logical - # (change requires restart) -#fsync = on # flush data to disk for crash safety - # (turning this off can cause - # unrecoverable data corruption) -#synchronous_commit = on # synchronization level; - # off, local, remote_write, remote_apply, or on -#wal_sync_method = fsync # the default is the first option - # supported by the operating system: - # open_datasync - # fdatasync (default on Linux) - # fsync - # fsync_writethrough - # open_sync -#full_page_writes = on # recover from partial page writes -#wal_compression = off # enable compression of full-page writes -#wal_log_hints = off # also do full page writes of non-critical updates - # (change requires restart) -#wal_init_zero = on # zero-fill new WAL files -#wal_recycle = on # recycle WAL files -#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers - # (change requires restart) -#wal_writer_delay = 200ms # 1-10000 milliseconds -#wal_writer_flush_after = 1MB # measured in pages, 0 disables - -#commit_delay = 0 # range 0-100000, in microseconds -#commit_siblings = 5 # range 1-1000 - -# - Checkpoints - - -#checkpoint_timeout = 5min # range 30s-1d -#max_wal_size = 1GB -#min_wal_size = 80MB -#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0 -#checkpoint_flush_after = 0 # measured in pages, 0 disables -#checkpoint_warning = 30s # 0 disables - -# - Archiving - - -#archive_mode = off # enables archiving; off, on, or always - # (change requires restart) -#archive_command = '' # command to use to archive a logfile segment - # placeholders: %p = path of file to archive - # %f = file name only - # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' -#archive_timeout = 0 # force a logfile segment switch after this - # number of seconds; 0 disables - -# - Archive Recovery - - -# These are only used in recovery mode. - -#restore_command = '' # command to use to restore an archived logfile segment - # placeholders: %p = path of file to restore - # %f = file name only - # e.g. 'cp /mnt/server/archivedir/%f %p' - # (change requires restart) -#archive_cleanup_command = '' # command to execute at every restartpoint -#recovery_end_command = '' # command to execute at completion of recovery - -# - Recovery Target - - -# Set these only when performing a targeted recovery. - -#recovery_target = '' # 'immediate' to end recovery as soon as a - # consistent state is reached - # (change requires restart) -#recovery_target_name = '' # the named restore point to which recovery will proceed - # (change requires restart) -#recovery_target_time = '' # the time stamp up to which recovery will proceed - # (change requires restart) -#recovery_target_xid = '' # the transaction ID up to which recovery will proceed - # (change requires restart) -#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed - # (change requires restart) -#recovery_target_inclusive = on # Specifies whether to stop: - # just after the specified recovery target (on) - # just before the recovery target (off) - # (change requires restart) -#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID - # (change requires restart) -#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown' - # (change requires restart) - - -#------------------------------------------------------------------------------ -# REPLICATION -#------------------------------------------------------------------------------ - -# - Sending Servers - - -# Set these on the master and on any standby that will send replication data. - -#max_wal_senders = 10 # max number of walsender processes - # (change requires restart) -#wal_keep_segments = 0 # in logfile segments; 0 disables -#wal_sender_timeout = 60s # in milliseconds; 0 disables - -#max_replication_slots = 10 # max number of replication slots - # (change requires restart) -#track_commit_timestamp = off # collect timestamp of transaction commit - # (change requires restart) - -# - Master Server - - -# These settings are ignored on a standby server. - -#synchronous_standby_names = '' # standby servers that provide sync rep - # method to choose sync standbys, number of sync standbys, - # and comma-separated list of application_name - # from standby(s); '*' = all -#vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed - -# - Standby Servers - - -# These settings are ignored on a master server. - -#primary_conninfo = '' # connection string to sending server - # (change requires restart) -#primary_slot_name = '' # replication slot on sending server - # (change requires restart) -#promote_trigger_file = '' # file name whose presence ends recovery -#hot_standby = on # "off" disallows queries during recovery - # (change requires restart) -#max_standby_archive_delay = 30s # max delay before canceling queries - # when reading WAL from archive; - # -1 allows indefinite delay -#max_standby_streaming_delay = 30s # max delay before canceling queries - # when reading streaming WAL; - # -1 allows indefinite delay -#wal_receiver_status_interval = 10s # send replies at least this often - # 0 disables -#hot_standby_feedback = off # send info from standby to prevent - # query conflicts -#wal_receiver_timeout = 60s # time that receiver waits for - # communication from master - # in milliseconds; 0 disables -#wal_retrieve_retry_interval = 5s # time to wait before retrying to - # retrieve WAL after a failed attempt -#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery - -# - Subscribers - - -# These settings are ignored on a publisher. - -#max_logical_replication_workers = 4 # taken from max_worker_processes - # (change requires restart) -#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers - - -#------------------------------------------------------------------------------ -# QUERY TUNING -#------------------------------------------------------------------------------ - -# - Planner Method Configuration - - -#enable_bitmapscan = on -#enable_hashagg = on -#enable_hashjoin = on -#enable_indexscan = on -#enable_indexonlyscan = on -#enable_material = on -#enable_mergejoin = on -#enable_nestloop = on -#enable_parallel_append = on -#enable_seqscan = on -#enable_sort = on -#enable_tidscan = on -#enable_partitionwise_join = off -#enable_partitionwise_aggregate = off -#enable_parallel_hash = on -#enable_partition_pruning = on - -# - Planner Cost Constants - - -#seq_page_cost = 1.0 # measured on an arbitrary scale -#random_page_cost = 4.0 # same scale as above -#cpu_tuple_cost = 0.01 # same scale as above -#cpu_index_tuple_cost = 0.005 # same scale as above -#cpu_operator_cost = 0.0025 # same scale as above -#parallel_tuple_cost = 0.1 # same scale as above -#parallel_setup_cost = 1000.0 # same scale as above - -#jit_above_cost = 100000 # perform JIT compilation if available - # and query more expensive than this; - # -1 disables -#jit_inline_above_cost = 500000 # inline small functions if query is - # more expensive than this; -1 disables -#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if - # query is more expensive than this; - # -1 disables - -#min_parallel_table_scan_size = 8MB -#min_parallel_index_scan_size = 512kB -#effective_cache_size = 4GB - -# - Genetic Query Optimizer - - -#geqo = on -#geqo_threshold = 12 -#geqo_effort = 5 # range 1-10 -#geqo_pool_size = 0 # selects default based on effort -#geqo_generations = 0 # selects default based on effort -#geqo_selection_bias = 2.0 # range 1.5-2.0 -#geqo_seed = 0.0 # range 0.0-1.0 - -# - Other Planner Options - - -#default_statistics_target = 100 # range 1-10000 -#constraint_exclusion = partition # on, off, or partition -#cursor_tuple_fraction = 0.1 # range 0.0-1.0 -#from_collapse_limit = 8 -#join_collapse_limit = 8 # 1 disables collapsing of explicit - # JOIN clauses -#force_parallel_mode = off -#jit = on # allow JIT compilation -#plan_cache_mode = auto # auto, force_generic_plan or - # force_custom_plan - - -#------------------------------------------------------------------------------ -# REPORTING AND LOGGING -#------------------------------------------------------------------------------ - -# - Where to Log - - -#log_destination = 'stderr' # Valid values are combinations of - # stderr, csvlog, syslog, and eventlog, - # depending on platform. csvlog - # requires logging_collector to be on. - -# This is used when logging to stderr: -#logging_collector = off # Enable capturing of stderr and csvlog - # into log files. Required to be on for - # csvlogs. - # (change requires restart) - -# These are only used if logging_collector is on: -#log_directory = 'log' # directory where log files are written, - # can be absolute or relative to PGDATA -#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, - # can include strftime() escapes -#log_file_mode = 0600 # creation mode for log files, - # begin with 0 to use octal notation -#log_truncate_on_rotation = off # If on, an existing log file with the - # same name as the new log file will be - # truncated rather than appended to. - # But such truncation only occurs on - # time-driven rotation, not on restarts - # or size-driven rotation. Default is - # off, meaning append to existing files - # in all cases. -#log_rotation_age = 1d # Automatic rotation of logfiles will - # happen after that time. 0 disables. -#log_rotation_size = 10MB # Automatic rotation of logfiles will - # happen after that much log output. - # 0 disables. - -# These are relevant when logging to syslog: -#syslog_facility = 'LOCAL0' -#syslog_ident = 'postgres' -#syslog_sequence_numbers = on -#syslog_split_messages = on - -# This is only relevant when logging to eventlog (win32): -# (change requires restart) -#event_source = 'PostgreSQL' - -# - When to Log - - -#log_min_messages = warning # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # info - # notice - # warning - # error - # log - # fatal - # panic - -#log_min_error_statement = error # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # info - # notice - # warning - # error - # log - # fatal - # panic (effectively off) - -#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements - # and their durations, > 0 logs only - # statements running at least this number - # of milliseconds - -#log_transaction_sample_rate = 0.0 # Fraction of transactions whose statements - # are logged regardless of their duration. 1.0 logs all - # statements from all transactions, 0.0 never logs. - -# - What to Log - - -#debug_print_parse = off -#debug_print_rewritten = off -#debug_print_plan = off -#debug_pretty_print = on -#log_checkpoints = off -#log_connections = off -#log_disconnections = off -#log_duration = off -#log_error_verbosity = default # terse, default, or verbose messages -#log_hostname = off -#log_line_prefix = '%m [%p] ' # special values: - # %a = application name - # %u = user name - # %d = database name - # %r = remote host and port - # %h = remote host - # %p = process ID - # %t = timestamp without milliseconds - # %m = timestamp with milliseconds - # %n = timestamp with milliseconds (as a Unix epoch) - # %i = command tag - # %e = SQL state - # %c = session ID - # %l = session line number - # %s = session start timestamp - # %v = virtual transaction ID - # %x = transaction ID (0 if none) - # %q = stop here in non-session - # processes - # %% = '%' - # e.g. '<%u%%%d> ' -#log_lock_waits = off # log lock waits >= deadlock_timeout -#log_statement = 'none' # none, ddl, mod, all -#log_replication_commands = off -#log_temp_files = -1 # log temporary files equal or larger - # than the specified size in kilobytes; - # -1 disables, 0 logs all temp files -#log_timezone = 'GMT' - -#------------------------------------------------------------------------------ -# PROCESS TITLE -#------------------------------------------------------------------------------ - -#cluster_name = '' # added to process titles if nonempty - # (change requires restart) -#update_process_title = on - - -#------------------------------------------------------------------------------ -# STATISTICS -#------------------------------------------------------------------------------ - -# - Query and Index Statistics Collector - - -#track_activities = on -#track_counts = on -#track_io_timing = off -#track_functions = none # none, pl, all -#track_activity_query_size = 1024 # (change requires restart) -#stats_temp_directory = 'pg_stat_tmp' - - -# - Monitoring - - -#log_parser_stats = off -#log_planner_stats = off -#log_executor_stats = off -#log_statement_stats = off - - -#------------------------------------------------------------------------------ -# AUTOVACUUM -#------------------------------------------------------------------------------ - -#autovacuum = on # Enable autovacuum subprocess? 'on' - # requires track_counts to also be on. -#log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and - # their durations, > 0 logs only - # actions running at least this number - # of milliseconds. -#autovacuum_max_workers = 3 # max number of autovacuum subprocesses - # (change requires restart) -#autovacuum_naptime = 1min # time between autovacuum runs -#autovacuum_vacuum_threshold = 50 # min number of row updates before - # vacuum -#autovacuum_analyze_threshold = 50 # min number of row updates before - # analyze -#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum -#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze -#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum - # (change requires restart) -#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age - # before forced vacuum - # (change requires restart) -#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for - # autovacuum, in milliseconds; - # -1 means use vacuum_cost_delay -#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for - # autovacuum, -1 means use - # vacuum_cost_limit - - -#------------------------------------------------------------------------------ -# CLIENT CONNECTION DEFAULTS -#------------------------------------------------------------------------------ - -# - Statement Behavior - - -#client_min_messages = notice # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # log - # notice - # warning - # error -#search_path = '"$user", public' # schema names -#row_security = on -#default_tablespace = '' # a tablespace name, '' uses the default -#temp_tablespaces = '' # a list of tablespace names, '' uses - # only default tablespace -#default_table_access_method = 'heap' -#check_function_bodies = on -#default_transaction_isolation = 'read committed' -#default_transaction_read_only = off -#default_transaction_deferrable = off -#session_replication_role = 'origin' -#statement_timeout = 0 # in milliseconds, 0 is disabled -#lock_timeout = 0 # in milliseconds, 0 is disabled -#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled -#vacuum_freeze_min_age = 50000000 -#vacuum_freeze_table_age = 150000000 -#vacuum_multixact_freeze_min_age = 5000000 -#vacuum_multixact_freeze_table_age = 150000000 -#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples - # before index cleanup, 0 always performs - # index cleanup -#bytea_output = 'hex' # hex, escape -#xmlbinary = 'base64' -#xmloption = 'content' -#gin_fuzzy_search_limit = 0 -#gin_pending_list_limit = 4MB - -# - Locale and Formatting - - -#datestyle = 'iso, mdy' -#intervalstyle = 'postgres' -#timezone = 'GMT' -#timezone_abbreviations = 'Default' # Select the set of available time zone - # abbreviations. Currently, there are - # Default - # Australia (historical usage) - # India - # You can create your own file in - # share/timezonesets/. -#extra_float_digits = 1 # min -15, max 3; any value >0 actually - # selects precise output mode -#client_encoding = sql_ascii # actually, defaults to database - # encoding - -# These settings are initialized by initdb, but they can be changed. -#lc_messages = 'C' # locale for system error message - # strings -#lc_monetary = 'C' # locale for monetary formatting -#lc_numeric = 'C' # locale for number formatting -#lc_time = 'C' # locale for time formatting - -# default configuration for text search -#default_text_search_config = 'pg_catalog.simple' - -# - Shared Library Preloading - - -#shared_preload_libraries = '' # (change requires restart) -#local_preload_libraries = '' -#session_preload_libraries = '' -#jit_provider = 'llvmjit' # JIT library to use - -# - Other Defaults - - -#dynamic_library_path = '$libdir' - - -#------------------------------------------------------------------------------ -# LOCK MANAGEMENT -#------------------------------------------------------------------------------ - -#deadlock_timeout = 1s -#max_locks_per_transaction = 64 # min 10 - # (change requires restart) -#max_pred_locks_per_transaction = 64 # min 10 - # (change requires restart) -#max_pred_locks_per_relation = -2 # negative values mean - # (max_pred_locks_per_transaction - # / -max_pred_locks_per_relation) - 1 -#max_pred_locks_per_page = 2 # min 0 - - -#------------------------------------------------------------------------------ -# VERSION AND PLATFORM COMPATIBILITY -#------------------------------------------------------------------------------ - -# - Previous PostgreSQL Versions - - -#array_nulls = on -#backslash_quote = safe_encoding # on, off, or safe_encoding -#escape_string_warning = on -#lo_compat_privileges = off -#operator_precedence_warning = off -#quote_all_identifiers = off -#standard_conforming_strings = on -#synchronize_seqscans = on - -# - Other Platforms and Clients - - -#transform_null_equals = off - - -#------------------------------------------------------------------------------ -# ERROR HANDLING -#------------------------------------------------------------------------------ - -#exit_on_error = off # terminate session on any error? -#restart_after_crash = on # reinitialize after backend crash? -#data_sync_retry = off # retry or panic on failure to fsync - # data? - # (change requires restart) - - -#------------------------------------------------------------------------------ -# CONFIG FILE INCLUDES -#------------------------------------------------------------------------------ - -# These options allow settings to be loaded from files other than the -# default postgresql.conf. Note that these are directives, not variable -# assignments, so they can usefully be given more than once. - -#include_dir = '...' # include files ending in '.conf' from - # a directory, e.g., 'conf.d' -#include_if_exists = '...' # include file only if it exists -#include = '...' # include file - - -#------------------------------------------------------------------------------ -# CUSTOMIZED OPTIONS -#------------------------------------------------------------------------------ - -# Add settings for extensions here diff --git a/molecule/quarkus_ha/prepare.yml b/molecule/quarkus_ha/prepare.yml deleted file mode 100644 index dff1821..0000000 --- a/molecule/quarkus_ha/prepare.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -- name: Prepare - hosts: keycloak - tasks: - - name: "Display hera_home if defined." - ansible.builtin.set_fact: - hera_home: "{{ lookup('env', 'HERA_HOME') }}" - - - name: "Ensure common prepare phase are set." - ansible.builtin.include_tasks: ../prepare.yml - - - name: Create certificate request - ansible.builtin.command: "openssl req -x509 -newkey rsa:4096 -keyout {{ inventory_hostname }}.key -out {{ inventory_hostname }}.pem -sha256 -days 365 -nodes -subj '/CN={{ inventory_hostname }}'" - delegate_to: localhost - changed_when: False - - - name: Create vault directory - become: true - ansible.builtin.file: - state: directory - path: "/opt/keycloak/vault" - mode: 0755 - - - name: Make sure a jre is available (for keytool to prepare keystore) - delegate_to: localhost - ansible.builtin.package: - name: "{{ 'java-17-openjdk-headless' if hera_home | length > 0 else 'openjdk-17-jdk-headless' }}" - state: present - become: true - failed_when: false - - - name: Create vault keystore - ansible.builtin.command: keytool -importpass -alias TestRealm_testalias -keystore keystore.p12 -storepass keystorepassword - delegate_to: localhost - register: keytool_cmd - changed_when: False - failed_when: not 'already exists' in keytool_cmd.stdout and keytool_cmd.rc != 0 - - - name: Copy certificates and vault - become: true - ansible.builtin.copy: - src: keystore.p12 - dest: /opt/keycloak/vault/keystore.p12 - mode: 0444 diff --git a/molecule/quarkus_ha/roles b/molecule/quarkus_ha/roles deleted file mode 120000 index b741aa3..0000000 --- a/molecule/quarkus_ha/roles +++ /dev/null @@ -1 +0,0 @@ -../../roles \ No newline at end of file diff --git a/molecule/quarkus_ha/verify.yml b/molecule/quarkus_ha/verify.yml deleted file mode 100644 index c1a2fb9..0000000 --- a/molecule/quarkus_ha/verify.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -- name: Verify - hosts: keycloak - tasks: - - name: Populate service facts - ansible.builtin.service_facts: - - - name: Check if keycloak service started - ansible.builtin.assert: - that: - - ansible_facts.services["keycloak.service"]["state"] == "running" - - ansible_facts.services["keycloak.service"]["status"] == "enabled" - fail_msg: "Service not running" - - - name: Set internal envvar - ansible.builtin.set_fact: - hera_home: "{{ lookup('env', 'HERA_HOME') }}" - - - name: Check log file - become: true - ansible.builtin.stat: - path: /var/log/keycloak/keycloak.log - register: keycloak_log_file - - - name: Check if keycloak file exists - ansible.builtin.assert: - that: - - keycloak_log_file.stat.exists - - not keycloak_log_file.stat.isdir diff --git a/molecule/quarkus_ha_remote/converge.yml b/molecule/quarkus_ha_remote/converge.yml deleted file mode 100644 index e62ae23..0000000 --- a/molecule/quarkus_ha_remote/converge.yml +++ /dev/null @@ -1,57 +0,0 @@ ---- -- name: Converge - hosts: infinispan - roles: - - role: middleware_automation.infinispan.infinispan - infinispan_service_name: infinispan - infinispan_supervisor_password: remembertochangeme - infinispan_keycloak_caches: true - infinispan_keycloak_persistence: False - infinispan_jdbc_engine: postgres - infinispan_jdbc_url: jdbc:postgresql://postgres:5432/keycloak - infinispan_jdbc_driver_version: 9.4.1212 - infinispan_jdbc_user: keycloak - infinispan_jdbc_pass: mysecretpass - infinispan_bind_address: "{{ ansible_default_ipv4.address }}" - infinispan_users: - - { name: 'testuser', password: 'test', roles: 'observer' } - -- name: Converge - hosts: keycloak - vars: - keycloak_quarkus_show_deprecation_warnings: false - keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" - keycloak_quarkus_bootstrap_admin_user: "remembertochangeme" - keycloak_quarkus_hostname: "http://{{ inventory_hostname }}:8080" - keycloak_quarkus_log: file - keycloak_quarkus_log_level: info - keycloak_quarkus_https_key_file_enabled: true - keycloak_quarkus_key_file_copy_enabled: true - keycloak_quarkus_key_content: "{{ lookup('file', inventory_hostname + '.key') }}" - keycloak_quarkus_cert_file_copy_enabled: true - keycloak_quarkus_cert_file_src: "{{ inventory_hostname }}.pem" - keycloak_quarkus_ks_vault_enabled: true - keycloak_quarkus_ks_vault_file: "/opt/keycloak/vault/keystore.p12" - keycloak_quarkus_ks_vault_pass: keystorepassword - keycloak_quarkus_systemd_wait_for_port: true - keycloak_quarkus_systemd_wait_for_timeout: 20 - keycloak_quarkus_systemd_wait_for_delay: 2 - keycloak_quarkus_systemd_wait_for_log: true - keycloak_quarkus_ha_enabled: true - keycloak_quarkus_restart_strategy: restart/serial.yml - keycloak_quarkus_db_user: keycloak - keycloak_quarkus_db_pass: mysecretpass - keycloak_quarkus_db_url: jdbc:postgresql://postgres:5432/keycloak - keycloak_quarkus_cache_remote: true - keycloak_quarkus_cache_remote_username: supervisor - keycloak_quarkus_cache_remote_password: remembertochangeme - keycloak_quarkus_cache_remote_host: "infinispan1" - keycloak_quarkus_cache_remote_port: 11222 - keycloak_quarkus_cache_remote_tls_enabled: false - keycloak_quarkus_additional_env_vars: - - key: KC_FEATURES - value: clusterless - - key: KC_FEATURES_DISABLED - value: persistent-user-sessions - roles: - - role: keycloak_quarkus diff --git a/molecule/quarkus_ha_remote/molecule.yml b/molecule/quarkus_ha_remote/molecule.yml deleted file mode 100644 index 23d8db6..0000000 --- a/molecule/quarkus_ha_remote/molecule.yml +++ /dev/null @@ -1,80 +0,0 @@ ---- -driver: - name: docker -platforms: - - name: keycloak1 - image: registry.access.redhat.com/ubi9/ubi-init:latest - pre_build_image: true - privileged: true - command: "/usr/sbin/init" - groups: - - keycloak - networks: - - name: rhbk - port_bindings: - - "8080/tcp" - - "8443/tcp" - - "9000/tcp" - - name: infinispan1 - image: registry.access.redhat.com/ubi9/ubi-init:latest - pre_build_image: true - privileged: true - command: "/usr/sbin/init" - groups: - - infinispan - networks: - - name: rhbk - port_bindings: - - "11222/tcp" - - name: postgres - image: ubuntu/postgres:14-22.04_beta - pre_build_image: true - privileged: true - command: postgres - groups: - - database - networks: - - name: rhbk - port_bindings: - - "5432/tcp" - mounts: - - type: bind - target: /etc/postgresql/postgresql.conf - source: ${PWD}/molecule/quarkus_ha/postgresql/postgresql.conf - env: - POSTGRES_USER: keycloak - POSTGRES_PASSWORD: mysecretpass - POSTGRES_DB: keycloak - POSTGRES_HOST_AUTH_METHOD: trust -provisioner: - name: ansible - config_options: - defaults: - interpreter_python: auto_silent - ssh_connection: - pipelining: false - playbooks: - prepare: prepare.yml - converge: converge.yml - verify: verify.yml - inventory: - host_vars: - localhost: - ansible_python_interpreter: "{{ ansible_playbook_python }}" - env: - ANSIBLE_FORCE_COLOR: "true" - PYTHONHTTPSVERIFY: 0 -verifier: - name: ansible -scenario: - test_sequence: - - cleanup - - destroy - - create - - prepare - - converge - - idempotence - - side_effect - - verify - - cleanup - - destroy diff --git a/molecule/quarkus_ha_remote/postgresql/postgresql.conf b/molecule/quarkus_ha_remote/postgresql/postgresql.conf deleted file mode 100644 index d702576..0000000 --- a/molecule/quarkus_ha_remote/postgresql/postgresql.conf +++ /dev/null @@ -1,750 +0,0 @@ -# ----------------------------- -# PostgreSQL configuration file -# ----------------------------- -# -# This file consists of lines of the form: -# -# name = value -# -# (The "=" is optional.) Whitespace may be used. Comments are introduced with -# "#" anywhere on a line. The complete list of parameter names and allowed -# values can be found in the PostgreSQL documentation. -# -# The commented-out settings shown in this file represent the default values. -# Re-commenting a setting is NOT sufficient to revert it to the default value; -# you need to reload the server. -# -# This file is read on server startup and when the server receives a SIGHUP -# signal. If you edit the file on a running system, you have to SIGHUP the -# server for the changes to take effect, run "pg_ctl reload", or execute -# "SELECT pg_reload_conf()". Some parameters, which are marked below, -# require a server shutdown and restart to take effect. -# -# Any parameter can also be given as a command-line option to the server, e.g., -# "postgres -c log_connections=on". Some parameters can be changed at run time -# with the "SET" SQL command. -# -# Memory units: kB = kilobytes Time units: ms = milliseconds -# MB = megabytes s = seconds -# GB = gigabytes min = minutes -# TB = terabytes h = hours -# d = days - - -#------------------------------------------------------------------------------ -# FILE LOCATIONS -#------------------------------------------------------------------------------ - -# The default values of these variables are driven from the -D command-line -# option or PGDATA environment variable, represented here as ConfigDir. - -#data_directory = 'ConfigDir' # use data in another directory - # (change requires restart) -#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file - # (change requires restart) -#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file - # (change requires restart) - -# If external_pid_file is not explicitly set, no extra PID file is written. -#external_pid_file = '' # write an extra PID file - # (change requires restart) - - -#------------------------------------------------------------------------------ -# CONNECTIONS AND AUTHENTICATION -#------------------------------------------------------------------------------ - -# - Connection Settings - - -listen_addresses = '*' # what IP address(es) to listen on; - # comma-separated list of addresses; - # defaults to 'localhost'; use '*' for all - # (change requires restart) -#port = 5432 # (change requires restart) -#max_connections = 100 # (change requires restart) -#superuser_reserved_connections = 3 # (change requires restart) -#unix_socket_directories = '/tmp' # comma-separated list of directories - # (change requires restart) -#unix_socket_group = '' # (change requires restart) -#unix_socket_permissions = 0777 # begin with 0 to use octal notation - # (change requires restart) -#bonjour = off # advertise server via Bonjour - # (change requires restart) -#bonjour_name = '' # defaults to the computer name - # (change requires restart) - -# - TCP settings - -# see "man 7 tcp" for details - -#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; - # 0 selects the system default -#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; - # 0 selects the system default -#tcp_keepalives_count = 0 # TCP_KEEPCNT; - # 0 selects the system default -#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds; - # 0 selects the system default - -# - Authentication - - -#authentication_timeout = 1min # 1s-600s -#password_encryption = md5 # md5 or scram-sha-256 -#db_user_namespace = off - -# GSSAPI using Kerberos -#krb_server_keyfile = '' -#krb_caseins_users = off - -# - SSL - - -#ssl = off -#ssl_ca_file = '' -#ssl_cert_file = 'server.crt' -#ssl_crl_file = '' -#ssl_key_file = 'server.key' -#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers -#ssl_prefer_server_ciphers = on -#ssl_ecdh_curve = 'prime256v1' -#ssl_min_protocol_version = 'TLSv1' -#ssl_max_protocol_version = '' -#ssl_dh_params_file = '' -#ssl_passphrase_command = '' -#ssl_passphrase_command_supports_reload = off - - -#------------------------------------------------------------------------------ -# RESOURCE USAGE (except WAL) -#------------------------------------------------------------------------------ - -# - Memory - - -#shared_buffers = 32MB # min 128kB - # (change requires restart) -#huge_pages = try # on, off, or try - # (change requires restart) -#temp_buffers = 8MB # min 800kB -#max_prepared_transactions = 0 # zero disables the feature - # (change requires restart) -# Caution: it is not advisable to set max_prepared_transactions nonzero unless -# you actively intend to use prepared transactions. -#work_mem = 4MB # min 64kB -#maintenance_work_mem = 64MB # min 1MB -#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem -#max_stack_depth = 2MB # min 100kB -#shared_memory_type = mmap # the default is the first option - # supported by the operating system: - # mmap - # sysv - # windows - # (change requires restart) -#dynamic_shared_memory_type = posix # the default is the first option - # supported by the operating system: - # posix - # sysv - # windows - # mmap - # (change requires restart) - -# - Disk - - -#temp_file_limit = -1 # limits per-process temp file space - # in kB, or -1 for no limit - -# - Kernel Resources - - -#max_files_per_process = 1000 # min 25 - # (change requires restart) - -# - Cost-Based Vacuum Delay - - -#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables) -#vacuum_cost_page_hit = 1 # 0-10000 credits -#vacuum_cost_page_miss = 10 # 0-10000 credits -#vacuum_cost_page_dirty = 20 # 0-10000 credits -#vacuum_cost_limit = 200 # 1-10000 credits - -# - Background Writer - - -#bgwriter_delay = 200ms # 10-10000ms between rounds -#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables -#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round -#bgwriter_flush_after = 0 # measured in pages, 0 disables - -# - Asynchronous Behavior - - -#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching -#max_worker_processes = 8 # (change requires restart) -#max_parallel_maintenance_workers = 2 # taken from max_parallel_workers -#max_parallel_workers_per_gather = 2 # taken from max_parallel_workers -#parallel_leader_participation = on -#max_parallel_workers = 8 # maximum number of max_worker_processes that - # can be used in parallel operations -#old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate - # (change requires restart) -#backend_flush_after = 0 # measured in pages, 0 disables - - -#------------------------------------------------------------------------------ -# WRITE-AHEAD LOG -#------------------------------------------------------------------------------ - -# - Settings - - -#wal_level = replica # minimal, replica, or logical - # (change requires restart) -#fsync = on # flush data to disk for crash safety - # (turning this off can cause - # unrecoverable data corruption) -#synchronous_commit = on # synchronization level; - # off, local, remote_write, remote_apply, or on -#wal_sync_method = fsync # the default is the first option - # supported by the operating system: - # open_datasync - # fdatasync (default on Linux) - # fsync - # fsync_writethrough - # open_sync -#full_page_writes = on # recover from partial page writes -#wal_compression = off # enable compression of full-page writes -#wal_log_hints = off # also do full page writes of non-critical updates - # (change requires restart) -#wal_init_zero = on # zero-fill new WAL files -#wal_recycle = on # recycle WAL files -#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers - # (change requires restart) -#wal_writer_delay = 200ms # 1-10000 milliseconds -#wal_writer_flush_after = 1MB # measured in pages, 0 disables - -#commit_delay = 0 # range 0-100000, in microseconds -#commit_siblings = 5 # range 1-1000 - -# - Checkpoints - - -#checkpoint_timeout = 5min # range 30s-1d -#max_wal_size = 1GB -#min_wal_size = 80MB -#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0 -#checkpoint_flush_after = 0 # measured in pages, 0 disables -#checkpoint_warning = 30s # 0 disables - -# - Archiving - - -#archive_mode = off # enables archiving; off, on, or always - # (change requires restart) -#archive_command = '' # command to use to archive a logfile segment - # placeholders: %p = path of file to archive - # %f = file name only - # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' -#archive_timeout = 0 # force a logfile segment switch after this - # number of seconds; 0 disables - -# - Archive Recovery - - -# These are only used in recovery mode. - -#restore_command = '' # command to use to restore an archived logfile segment - # placeholders: %p = path of file to restore - # %f = file name only - # e.g. 'cp /mnt/server/archivedir/%f %p' - # (change requires restart) -#archive_cleanup_command = '' # command to execute at every restartpoint -#recovery_end_command = '' # command to execute at completion of recovery - -# - Recovery Target - - -# Set these only when performing a targeted recovery. - -#recovery_target = '' # 'immediate' to end recovery as soon as a - # consistent state is reached - # (change requires restart) -#recovery_target_name = '' # the named restore point to which recovery will proceed - # (change requires restart) -#recovery_target_time = '' # the time stamp up to which recovery will proceed - # (change requires restart) -#recovery_target_xid = '' # the transaction ID up to which recovery will proceed - # (change requires restart) -#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed - # (change requires restart) -#recovery_target_inclusive = on # Specifies whether to stop: - # just after the specified recovery target (on) - # just before the recovery target (off) - # (change requires restart) -#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID - # (change requires restart) -#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown' - # (change requires restart) - - -#------------------------------------------------------------------------------ -# REPLICATION -#------------------------------------------------------------------------------ - -# - Sending Servers - - -# Set these on the master and on any standby that will send replication data. - -#max_wal_senders = 10 # max number of walsender processes - # (change requires restart) -#wal_keep_segments = 0 # in logfile segments; 0 disables -#wal_sender_timeout = 60s # in milliseconds; 0 disables - -#max_replication_slots = 10 # max number of replication slots - # (change requires restart) -#track_commit_timestamp = off # collect timestamp of transaction commit - # (change requires restart) - -# - Master Server - - -# These settings are ignored on a standby server. - -#synchronous_standby_names = '' # standby servers that provide sync rep - # method to choose sync standbys, number of sync standbys, - # and comma-separated list of application_name - # from standby(s); '*' = all -#vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed - -# - Standby Servers - - -# These settings are ignored on a master server. - -#primary_conninfo = '' # connection string to sending server - # (change requires restart) -#primary_slot_name = '' # replication slot on sending server - # (change requires restart) -#promote_trigger_file = '' # file name whose presence ends recovery -#hot_standby = on # "off" disallows queries during recovery - # (change requires restart) -#max_standby_archive_delay = 30s # max delay before canceling queries - # when reading WAL from archive; - # -1 allows indefinite delay -#max_standby_streaming_delay = 30s # max delay before canceling queries - # when reading streaming WAL; - # -1 allows indefinite delay -#wal_receiver_status_interval = 10s # send replies at least this often - # 0 disables -#hot_standby_feedback = off # send info from standby to prevent - # query conflicts -#wal_receiver_timeout = 60s # time that receiver waits for - # communication from master - # in milliseconds; 0 disables -#wal_retrieve_retry_interval = 5s # time to wait before retrying to - # retrieve WAL after a failed attempt -#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery - -# - Subscribers - - -# These settings are ignored on a publisher. - -#max_logical_replication_workers = 4 # taken from max_worker_processes - # (change requires restart) -#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers - - -#------------------------------------------------------------------------------ -# QUERY TUNING -#------------------------------------------------------------------------------ - -# - Planner Method Configuration - - -#enable_bitmapscan = on -#enable_hashagg = on -#enable_hashjoin = on -#enable_indexscan = on -#enable_indexonlyscan = on -#enable_material = on -#enable_mergejoin = on -#enable_nestloop = on -#enable_parallel_append = on -#enable_seqscan = on -#enable_sort = on -#enable_tidscan = on -#enable_partitionwise_join = off -#enable_partitionwise_aggregate = off -#enable_parallel_hash = on -#enable_partition_pruning = on - -# - Planner Cost Constants - - -#seq_page_cost = 1.0 # measured on an arbitrary scale -#random_page_cost = 4.0 # same scale as above -#cpu_tuple_cost = 0.01 # same scale as above -#cpu_index_tuple_cost = 0.005 # same scale as above -#cpu_operator_cost = 0.0025 # same scale as above -#parallel_tuple_cost = 0.1 # same scale as above -#parallel_setup_cost = 1000.0 # same scale as above - -#jit_above_cost = 100000 # perform JIT compilation if available - # and query more expensive than this; - # -1 disables -#jit_inline_above_cost = 500000 # inline small functions if query is - # more expensive than this; -1 disables -#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if - # query is more expensive than this; - # -1 disables - -#min_parallel_table_scan_size = 8MB -#min_parallel_index_scan_size = 512kB -#effective_cache_size = 4GB - -# - Genetic Query Optimizer - - -#geqo = on -#geqo_threshold = 12 -#geqo_effort = 5 # range 1-10 -#geqo_pool_size = 0 # selects default based on effort -#geqo_generations = 0 # selects default based on effort -#geqo_selection_bias = 2.0 # range 1.5-2.0 -#geqo_seed = 0.0 # range 0.0-1.0 - -# - Other Planner Options - - -#default_statistics_target = 100 # range 1-10000 -#constraint_exclusion = partition # on, off, or partition -#cursor_tuple_fraction = 0.1 # range 0.0-1.0 -#from_collapse_limit = 8 -#join_collapse_limit = 8 # 1 disables collapsing of explicit - # JOIN clauses -#force_parallel_mode = off -#jit = on # allow JIT compilation -#plan_cache_mode = auto # auto, force_generic_plan or - # force_custom_plan - - -#------------------------------------------------------------------------------ -# REPORTING AND LOGGING -#------------------------------------------------------------------------------ - -# - Where to Log - - -#log_destination = 'stderr' # Valid values are combinations of - # stderr, csvlog, syslog, and eventlog, - # depending on platform. csvlog - # requires logging_collector to be on. - -# This is used when logging to stderr: -#logging_collector = off # Enable capturing of stderr and csvlog - # into log files. Required to be on for - # csvlogs. - # (change requires restart) - -# These are only used if logging_collector is on: -#log_directory = 'log' # directory where log files are written, - # can be absolute or relative to PGDATA -#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, - # can include strftime() escapes -#log_file_mode = 0600 # creation mode for log files, - # begin with 0 to use octal notation -#log_truncate_on_rotation = off # If on, an existing log file with the - # same name as the new log file will be - # truncated rather than appended to. - # But such truncation only occurs on - # time-driven rotation, not on restarts - # or size-driven rotation. Default is - # off, meaning append to existing files - # in all cases. -#log_rotation_age = 1d # Automatic rotation of logfiles will - # happen after that time. 0 disables. -#log_rotation_size = 10MB # Automatic rotation of logfiles will - # happen after that much log output. - # 0 disables. - -# These are relevant when logging to syslog: -#syslog_facility = 'LOCAL0' -#syslog_ident = 'postgres' -#syslog_sequence_numbers = on -#syslog_split_messages = on - -# This is only relevant when logging to eventlog (win32): -# (change requires restart) -#event_source = 'PostgreSQL' - -# - When to Log - - -#log_min_messages = warning # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # info - # notice - # warning - # error - # log - # fatal - # panic - -#log_min_error_statement = error # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # info - # notice - # warning - # error - # log - # fatal - # panic (effectively off) - -#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements - # and their durations, > 0 logs only - # statements running at least this number - # of milliseconds - -#log_transaction_sample_rate = 0.0 # Fraction of transactions whose statements - # are logged regardless of their duration. 1.0 logs all - # statements from all transactions, 0.0 never logs. - -# - What to Log - - -#debug_print_parse = off -#debug_print_rewritten = off -#debug_print_plan = off -#debug_pretty_print = on -#log_checkpoints = off -#log_connections = off -#log_disconnections = off -#log_duration = off -#log_error_verbosity = default # terse, default, or verbose messages -#log_hostname = off -#log_line_prefix = '%m [%p] ' # special values: - # %a = application name - # %u = user name - # %d = database name - # %r = remote host and port - # %h = remote host - # %p = process ID - # %t = timestamp without milliseconds - # %m = timestamp with milliseconds - # %n = timestamp with milliseconds (as a Unix epoch) - # %i = command tag - # %e = SQL state - # %c = session ID - # %l = session line number - # %s = session start timestamp - # %v = virtual transaction ID - # %x = transaction ID (0 if none) - # %q = stop here in non-session - # processes - # %% = '%' - # e.g. '<%u%%%d> ' -#log_lock_waits = off # log lock waits >= deadlock_timeout -#log_statement = 'none' # none, ddl, mod, all -#log_replication_commands = off -#log_temp_files = -1 # log temporary files equal or larger - # than the specified size in kilobytes; - # -1 disables, 0 logs all temp files -#log_timezone = 'GMT' - -#------------------------------------------------------------------------------ -# PROCESS TITLE -#------------------------------------------------------------------------------ - -#cluster_name = '' # added to process titles if nonempty - # (change requires restart) -#update_process_title = on - - -#------------------------------------------------------------------------------ -# STATISTICS -#------------------------------------------------------------------------------ - -# - Query and Index Statistics Collector - - -#track_activities = on -#track_counts = on -#track_io_timing = off -#track_functions = none # none, pl, all -#track_activity_query_size = 1024 # (change requires restart) -#stats_temp_directory = 'pg_stat_tmp' - - -# - Monitoring - - -#log_parser_stats = off -#log_planner_stats = off -#log_executor_stats = off -#log_statement_stats = off - - -#------------------------------------------------------------------------------ -# AUTOVACUUM -#------------------------------------------------------------------------------ - -#autovacuum = on # Enable autovacuum subprocess? 'on' - # requires track_counts to also be on. -#log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and - # their durations, > 0 logs only - # actions running at least this number - # of milliseconds. -#autovacuum_max_workers = 3 # max number of autovacuum subprocesses - # (change requires restart) -#autovacuum_naptime = 1min # time between autovacuum runs -#autovacuum_vacuum_threshold = 50 # min number of row updates before - # vacuum -#autovacuum_analyze_threshold = 50 # min number of row updates before - # analyze -#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum -#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze -#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum - # (change requires restart) -#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age - # before forced vacuum - # (change requires restart) -#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for - # autovacuum, in milliseconds; - # -1 means use vacuum_cost_delay -#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for - # autovacuum, -1 means use - # vacuum_cost_limit - - -#------------------------------------------------------------------------------ -# CLIENT CONNECTION DEFAULTS -#------------------------------------------------------------------------------ - -# - Statement Behavior - - -#client_min_messages = notice # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # log - # notice - # warning - # error -#search_path = '"$user", public' # schema names -#row_security = on -#default_tablespace = '' # a tablespace name, '' uses the default -#temp_tablespaces = '' # a list of tablespace names, '' uses - # only default tablespace -#default_table_access_method = 'heap' -#check_function_bodies = on -#default_transaction_isolation = 'read committed' -#default_transaction_read_only = off -#default_transaction_deferrable = off -#session_replication_role = 'origin' -#statement_timeout = 0 # in milliseconds, 0 is disabled -#lock_timeout = 0 # in milliseconds, 0 is disabled -#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled -#vacuum_freeze_min_age = 50000000 -#vacuum_freeze_table_age = 150000000 -#vacuum_multixact_freeze_min_age = 5000000 -#vacuum_multixact_freeze_table_age = 150000000 -#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples - # before index cleanup, 0 always performs - # index cleanup -#bytea_output = 'hex' # hex, escape -#xmlbinary = 'base64' -#xmloption = 'content' -#gin_fuzzy_search_limit = 0 -#gin_pending_list_limit = 4MB - -# - Locale and Formatting - - -#datestyle = 'iso, mdy' -#intervalstyle = 'postgres' -#timezone = 'GMT' -#timezone_abbreviations = 'Default' # Select the set of available time zone - # abbreviations. Currently, there are - # Default - # Australia (historical usage) - # India - # You can create your own file in - # share/timezonesets/. -#extra_float_digits = 1 # min -15, max 3; any value >0 actually - # selects precise output mode -#client_encoding = sql_ascii # actually, defaults to database - # encoding - -# These settings are initialized by initdb, but they can be changed. -#lc_messages = 'C' # locale for system error message - # strings -#lc_monetary = 'C' # locale for monetary formatting -#lc_numeric = 'C' # locale for number formatting -#lc_time = 'C' # locale for time formatting - -# default configuration for text search -#default_text_search_config = 'pg_catalog.simple' - -# - Shared Library Preloading - - -#shared_preload_libraries = '' # (change requires restart) -#local_preload_libraries = '' -#session_preload_libraries = '' -#jit_provider = 'llvmjit' # JIT library to use - -# - Other Defaults - - -#dynamic_library_path = '$libdir' - - -#------------------------------------------------------------------------------ -# LOCK MANAGEMENT -#------------------------------------------------------------------------------ - -#deadlock_timeout = 1s -#max_locks_per_transaction = 64 # min 10 - # (change requires restart) -#max_pred_locks_per_transaction = 64 # min 10 - # (change requires restart) -#max_pred_locks_per_relation = -2 # negative values mean - # (max_pred_locks_per_transaction - # / -max_pred_locks_per_relation) - 1 -#max_pred_locks_per_page = 2 # min 0 - - -#------------------------------------------------------------------------------ -# VERSION AND PLATFORM COMPATIBILITY -#------------------------------------------------------------------------------ - -# - Previous PostgreSQL Versions - - -#array_nulls = on -#backslash_quote = safe_encoding # on, off, or safe_encoding -#escape_string_warning = on -#lo_compat_privileges = off -#operator_precedence_warning = off -#quote_all_identifiers = off -#standard_conforming_strings = on -#synchronize_seqscans = on - -# - Other Platforms and Clients - - -#transform_null_equals = off - - -#------------------------------------------------------------------------------ -# ERROR HANDLING -#------------------------------------------------------------------------------ - -#exit_on_error = off # terminate session on any error? -#restart_after_crash = on # reinitialize after backend crash? -#data_sync_retry = off # retry or panic on failure to fsync - # data? - # (change requires restart) - - -#------------------------------------------------------------------------------ -# CONFIG FILE INCLUDES -#------------------------------------------------------------------------------ - -# These options allow settings to be loaded from files other than the -# default postgresql.conf. Note that these are directives, not variable -# assignments, so they can usefully be given more than once. - -#include_dir = '...' # include files ending in '.conf' from - # a directory, e.g., 'conf.d' -#include_if_exists = '...' # include file only if it exists -#include = '...' # include file - - -#------------------------------------------------------------------------------ -# CUSTOMIZED OPTIONS -#------------------------------------------------------------------------------ - -# Add settings for extensions here diff --git a/molecule/quarkus_ha_remote/prepare.yml b/molecule/quarkus_ha_remote/prepare.yml deleted file mode 100644 index 16ae9b9..0000000 --- a/molecule/quarkus_ha_remote/prepare.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -- name: Prepare - hosts: 'keycloak:infinispan' - tasks: - - name: "Display hera_home if defined." - ansible.builtin.set_fact: - hera_home: "{{ lookup('env', 'HERA_HOME') }}" - - - name: "Ensure common prepare phase are set." - ansible.builtin.include_tasks: ../prepare.yml - - - name: Create certificate request - ansible.builtin.command: "openssl req -x509 -newkey rsa:4096 -keyout {{ inventory_hostname }}.key -out {{ inventory_hostname }}.pem -sha256 -days 365 -nodes -subj '/CN={{ inventory_hostname }}'" - delegate_to: localhost - changed_when: False - - - name: Create vault directory - become: true - ansible.builtin.file: - state: directory - path: "/opt/keycloak/vault" - mode: 0755 - - - name: Make sure a jre is available (for keytool to prepare keystore) - delegate_to: localhost - ansible.builtin.package: - name: "{{ 'java-17-openjdk-headless' if hera_home | length > 0 else 'openjdk-17-jdk-headless' }}" - state: present - become: true - failed_when: false - - - name: Create vault keystore - ansible.builtin.command: keytool -importpass -alias TestRealm_testalias -keystore keystore.p12 -storepass keystorepassword - delegate_to: localhost - register: keytool_cmd - changed_when: False - failed_when: not 'already exists' in keytool_cmd.stdout and keytool_cmd.rc != 0 - - - name: Copy certificates and vault - become: true - ansible.builtin.copy: - src: keystore.p12 - dest: /opt/keycloak/vault/keystore.p12 - mode: 0444 diff --git a/molecule/quarkus_ha_remote/roles b/molecule/quarkus_ha_remote/roles deleted file mode 120000 index b741aa3..0000000 --- a/molecule/quarkus_ha_remote/roles +++ /dev/null @@ -1 +0,0 @@ -../../roles \ No newline at end of file diff --git a/molecule/quarkus_ha_remote/verify.yml b/molecule/quarkus_ha_remote/verify.yml deleted file mode 100644 index c1a2fb9..0000000 --- a/molecule/quarkus_ha_remote/verify.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -- name: Verify - hosts: keycloak - tasks: - - name: Populate service facts - ansible.builtin.service_facts: - - - name: Check if keycloak service started - ansible.builtin.assert: - that: - - ansible_facts.services["keycloak.service"]["state"] == "running" - - ansible_facts.services["keycloak.service"]["status"] == "enabled" - fail_msg: "Service not running" - - - name: Set internal envvar - ansible.builtin.set_fact: - hera_home: "{{ lookup('env', 'HERA_HOME') }}" - - - name: Check log file - become: true - ansible.builtin.stat: - path: /var/log/keycloak/keycloak.log - register: keycloak_log_file - - - name: Check if keycloak file exists - ansible.builtin.assert: - that: - - keycloak_log_file.stat.exists - - not keycloak_log_file.stat.isdir diff --git a/molecule/quarkus_upgrade/converge.yml b/molecule/quarkus_upgrade/converge.yml deleted file mode 100644 index 0f67169..0000000 --- a/molecule/quarkus_upgrade/converge.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- -- name: Converge - hosts: all - vars_files: - - vars.yml - vars: - keycloak_quarkus_show_deprecation_warnings: false - keycloak_quarkus_additional_env_vars: - - key: KC_FEATURES_DISABLED - value: ciba,device-flow,impersonation,kerberos,docker - keycloak_quarkus_version: 26.0.7 - roles: - - role: keycloak_quarkus diff --git a/molecule/quarkus_upgrade/molecule.yml b/molecule/quarkus_upgrade/molecule.yml deleted file mode 100644 index b121079..0000000 --- a/molecule/quarkus_upgrade/molecule.yml +++ /dev/null @@ -1,49 +0,0 @@ ---- -dependency: - name: galaxy - options: - requirements-file: molecule/requirements.yml -driver: - name: podman -platforms: - - name: instance - image: registry.access.redhat.com/ubi9/ubi-init:latest - command: "/usr/sbin/init" - pre_build_image: true - privileged: true - port_bindings: - - 8080:8080 - - "9000/tcp" - published_ports: - - 0.0.0.0:8080:8080/TCP - - 0.0.0.0:9000:9000/TCP -provisioner: - name: ansible - playbooks: - prepare: prepare.yml - converge: converge.yml - verify: verify.yml - inventory: - host_vars: - localhost: - ansible_python_interpreter: "{{ ansible_playbook_python }}" - env: - ANSIBLE_FORCE_COLOR: "true" - PROXY: "${PROXY}" - NO_PROXY: "${NO_PROXY}" -verifier: - name: ansible -scenario: - test_sequence: - - dependency - - cleanup - - destroy - - syntax - - create - - prepare - - converge - - idempotence - - side_effect - - verify - - cleanup - - destroy diff --git a/molecule/quarkus_upgrade/prepare.yml b/molecule/quarkus_upgrade/prepare.yml deleted file mode 100644 index 1be16d6..0000000 --- a/molecule/quarkus_upgrade/prepare.yml +++ /dev/null @@ -1,56 +0,0 @@ ---- -- name: Prepare - hosts: all - vars_files: - - vars.yml - vars: - sudo_pkg_name: sudo - keycloak_quarkus_version: 26.0.4 - keycloak_quarkus_additional_env_vars: - - key: KC_FEATURES_DISABLED - value: impersonation,kerberos - pre_tasks: - - name: Install sudo - ansible.builtin.apt: - name: - - sudo - - openjdk-17-jdk-headless - state: present - when: - - ansible_facts.os_family == 'Debian' - - - name: "Ensure common prepare phase are set." - ansible.builtin.include_tasks: ../prepare.yml - - - name: Display Ansible version - ansible.builtin.debug: - msg: "Ansible version is {{ ansible_version.full }}" - - - name: "Ensure {{ sudo_pkg_name }} is installed (if user is root)." - ansible.builtin.dnf: - name: "{{ sudo_pkg_name }}" - when: - - ansible_user_id == 'root' - - - name: Gather the package facts - ansible.builtin.package_facts: - manager: auto - - - name: "Check if {{ sudo_pkg_name }} is installed." - ansible.builtin.assert: - that: - - sudo_pkg_name in ansible_facts.packages - - - name: Create certificate request - ansible.builtin.command: openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -subj '/CN=instance' - delegate_to: localhost - changed_when: false - roles: - - role: keycloak_quarkus - - post_tasks: - - name: "Delete custom fact" - ansible.builtin.file: - path: /etc/ansible/facts.d/keycloak.fact - state: absent - become: true diff --git a/molecule/quarkus_upgrade/roles b/molecule/quarkus_upgrade/roles deleted file mode 120000 index b741aa3..0000000 --- a/molecule/quarkus_upgrade/roles +++ /dev/null @@ -1 +0,0 @@ -../../roles \ No newline at end of file diff --git a/molecule/quarkus_upgrade/vars.yml b/molecule/quarkus_upgrade/vars.yml deleted file mode 100644 index 1567ae4..0000000 --- a/molecule/quarkus_upgrade/vars.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- -keycloak_quarkus_offline_install: false -keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" -keycloak_quarkus_realm: TestRealm -keycloak_quarkus_hostname: http://instance:8080 -keycloak_quarkus_log: file -keycloak_quarkus_https_key_file_enabled: true -keycloak_quarkus_log_target: /tmp/keycloak -keycloak_quarkus_hostname_strict: false -keycloak_quarkus_cert_file_copy_enabled: true -keycloak_quarkus_key_file_copy_enabled: true -keycloak_quarkus_key_content: "{{ lookup('file', 'key.pem') }}" -keycloak_quarkus_cert_file_src: cert.pem diff --git a/molecule/quarkus_upgrade/verify.yml b/molecule/quarkus_upgrade/verify.yml deleted file mode 100644 index 1c4a0ba..0000000 --- a/molecule/quarkus_upgrade/verify.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -- name: Verify - hosts: instance - vars: - keycloak_quarkus_bootstrap_admin_password: "remembertochangeme" - keycloak_quarkus_port: http://localhost:8080 - tasks: - - name: Populate service facts - ansible.builtin.service_facts: - - - name: Check if keycloak service started - ansible.builtin.assert: - that: - - ansible_facts.services["keycloak.service"]["state"] == "running" - - ansible_facts.services["keycloak.service"]["status"] == "enabled" - - - name: Verify we are running on requested jvm - ansible.builtin.shell: | - set -eo pipefail - ps -ef | grep 'etc/alternatives/.*21' | grep -v grep - changed_when: false - - - name: Verify token api call - ansible.builtin.uri: - url: "{{ keycloak_quarkus_port }}/realms/master/protocol/openid-connect/token" - method: POST - body: "client_id=admin-cli&username=admin&password={{ keycloak_quarkus_bootstrap_admin_password }}&grant_type=password" - validate_certs: no - register: keycloak_auth_response - until: keycloak_auth_response.status == 200 - retries: 2 - delay: 2 diff --git a/molecule/requirements.yml b/molecule/requirements.yml deleted file mode 100644 index 125a922..0000000 --- a/molecule/requirements.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -collections: - - name: middleware_automation.common - - name: middleware_automation.jbcs - - name: middleware_automation.infinispan - - name: community.general - - name: ansible.posix - - name: community.docker - version: ">=3.8.0" - -roles: - - name: elan.simple_nginx_reverse_proxy diff --git a/playbooks/keycloak.yml b/playbooks/keycloak.yml index 6c7ca4a..2b222a5 100644 --- a/playbooks/keycloak.yml +++ b/playbooks/keycloak.yml @@ -3,5 +3,7 @@ hosts: all vars: keycloak_admin_password: "remembertochangeme" + collections: + - middleware_automation.keycloak roles: - - middleware_automation.keycloak.keycloak + - keycloak diff --git a/playbooks/keycloak_federation.yml b/playbooks/keycloak_federation.yml deleted file mode 100644 index 49cb6c0..0000000 --- a/playbooks/keycloak_federation.yml +++ /dev/null @@ -1,68 +0,0 @@ ---- -- name: Playbook for Keycloak Hosts - hosts: all - tasks: - - name: Keycloak Realm Role - ansible.builtin.include_role: - name: keycloak_realm - vars: - keycloak_admin_password: "remembertochangeme" - keycloak_realm: TestRealm - keycloak_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_clients: - - name: TestClient1 - client_id: TestClient1 - roles: - - TestClient1Admin - - TestClient1User - realm: "{{ keycloak_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 }}" diff --git a/playbooks/keycloak_quarkus.yml b/playbooks/keycloak_quarkus.yml deleted file mode 100644 index b8aedf2..0000000 --- a/playbooks/keycloak_quarkus.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -- 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 diff --git a/playbooks/keycloak_quarkus_dev.yml b/playbooks/keycloak_quarkus_dev.yml deleted file mode 100644 index c8bb54e..0000000 --- a/playbooks/keycloak_quarkus_dev.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -- 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 diff --git a/playbooks/keycloak_realm.yml b/playbooks/keycloak_realm.yml index 99b2ef8..baeb2f2 100644 --- a/playbooks/keycloak_realm.yml +++ b/playbooks/keycloak_realm.yml @@ -1,26 +1,67 @@ --- - name: Playbook for Keycloak Hosts hosts: all - vars: - keycloak_admin_password: "remembertochangeme" - keycloak_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: TestRealm + tasks: + - name: Keycloak Realm Role + ansible.builtin.include_role: + name: middleware_automation.keycloak.keycloak_realm + vars: + keycloak_admin_password: "remembertochangeme" + keycloak_realm: TestRealm + keycloak_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_clients: + - name: TestClient1 + roles: + - TestClient1Admin + - TestClient1User + realm: "{{ keycloak_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 }}" diff --git a/playbooks/rhsso.yml b/playbooks/rhsso.yml index ea61f66..ba30a74 100644 --- a/playbooks/rhsso.yml +++ b/playbooks/rhsso.yml @@ -1,8 +1,12 @@ --- -- name: Playbook for Red Hat SSO Hosts - hosts: sso +- name: Playbook for Keycloak Hosts + hosts: keycloak vars: keycloak_admin_password: "remembertochangeme" - sso_enable: true + keycloak_rhsso_enable: True + collections: + - middleware_automation.redhat_csp_download + - middleware_automation.keycloak roles: + - middleware_automation.redhat_csp_download.redhat_csp_download - middleware_automation.keycloak.keycloak diff --git a/plugins/doc_fragments/attributes.py b/plugins/doc_fragments/attributes.py deleted file mode 100644 index 9b8488e..0000000 --- a/plugins/doc_fragments/attributes.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- 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) -__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. -''' diff --git a/plugins/doc_fragments/keycloak.py b/plugins/doc_fragments/keycloak.py deleted file mode 100644 index 5d79fad..0000000 --- a/plugins/doc_fragments/keycloak.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2017, Eike Frost -# 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) -__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 - version_added: 3.0.0 - - 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 - version_added: 4.5.0 - http_agent: - description: - - Configures the HTTP User-Agent header. - type: str - default: Ansible - version_added: 5.4.0 -''' diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py deleted file mode 100644 index 128b0fe..0000000 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ /dev/null @@ -1,3191 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, Eike Frost -# BSD 2-Clause license (see LICENSES/BSD-2-Clause.txt) -# SPDX-License-Identifier: BSD-2-Clause - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -import json -import traceback -import copy - -from ansible.module_utils.urls import open_url -from ansible.module_utils.six.moves.urllib.parse import urlencode, quote -from ansible.module_utils.six.moves.urllib.error import HTTPError -from ansible.module_utils.common.text.converters import to_native, to_text - -URL_REALM_INFO = "{url}/realms/{realm}" -URL_REALMS = "{url}/admin/realms" -URL_REALM = "{url}/admin/realms/{realm}" -URL_REALM_KEYS_METADATA = "{url}/admin/realms/{realm}/keys" - -URL_TOKEN = "{url}/realms/{realm}/protocol/openid-connect/token" -URL_CLIENT = "{url}/admin/realms/{realm}/clients/{id}" -URL_CLIENTS = "{url}/admin/realms/{realm}/clients" - -URL_CLIENT_ROLES = "{url}/admin/realms/{realm}/clients/{id}/roles" -URL_CLIENT_ROLE = "{url}/admin/realms/{realm}/clients/{id}/roles/{name}" -URL_CLIENT_ROLE_COMPOSITES = "{url}/admin/realms/{realm}/clients/{id}/roles/{name}/composites" - -URL_CLIENT_ROLE_SCOPE_CLIENTS = "{url}/admin/realms/{realm}/clients/{id}/scope-mappings/clients/{scopeid}" -URL_CLIENT_ROLE_SCOPE_REALM = "{url}/admin/realms/{realm}/clients/{id}/scope-mappings/realm" - -URL_REALM_ROLES = "{url}/admin/realms/{realm}/roles" -URL_REALM_ROLE = "{url}/admin/realms/{realm}/roles/{name}" -URL_REALM_ROLEMAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/realm" -URL_REALM_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/users/{id}/role-mappings/realm/available" -URL_REALM_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/users/{id}/role-mappings/realm/composite" -URL_REALM_ROLE_COMPOSITES = "{url}/admin/realms/{realm}/roles/{name}/composites" - -URL_ROLES_BY_ID = "{url}/admin/realms/{realm}/roles-by-id/{id}" -URL_ROLES_BY_ID_COMPOSITES_CLIENTS = "{url}/admin/realms/{realm}/roles-by-id/{id}/composites/clients/{cid}" -URL_ROLES_BY_ID_COMPOSITES = "{url}/admin/realms/{realm}/roles-by-id/{id}/composites" - -URL_CLIENTTEMPLATE = "{url}/admin/realms/{realm}/client-templates/{id}" -URL_CLIENTTEMPLATES = "{url}/admin/realms/{realm}/client-templates" -URL_GROUPS = "{url}/admin/realms/{realm}/groups" -URL_GROUP = "{url}/admin/realms/{realm}/groups/{groupid}" -URL_GROUP_CHILDREN = "{url}/admin/realms/{realm}/groups/{groupid}/children" - -URL_CLIENTSCOPES = "{url}/admin/realms/{realm}/client-scopes" -URL_CLIENTSCOPE = "{url}/admin/realms/{realm}/client-scopes/{id}" -URL_CLIENTSCOPE_PROTOCOLMAPPERS = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models" -URL_CLIENTSCOPE_PROTOCOLMAPPER = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models/{mapper_id}" - -URL_DEFAULT_CLIENTSCOPES = "{url}/admin/realms/{realm}/default-default-client-scopes" -URL_DEFAULT_CLIENTSCOPE = "{url}/admin/realms/{realm}/default-default-client-scopes/{id}" -URL_OPTIONAL_CLIENTSCOPES = "{url}/admin/realms/{realm}/default-optional-client-scopes" -URL_OPTIONAL_CLIENTSCOPE = "{url}/admin/realms/{realm}/default-optional-client-scopes/{id}" - -URL_CLIENT_DEFAULT_CLIENTSCOPES = "{url}/admin/realms/{realm}/clients/{cid}/default-client-scopes" -URL_CLIENT_DEFAULT_CLIENTSCOPE = "{url}/admin/realms/{realm}/clients/{cid}/default-client-scopes/{id}" -URL_CLIENT_OPTIONAL_CLIENTSCOPES = "{url}/admin/realms/{realm}/clients/{cid}/optional-client-scopes" -URL_CLIENT_OPTIONAL_CLIENTSCOPE = "{url}/admin/realms/{realm}/clients/{cid}/optional-client-scopes/{id}" - -URL_CLIENT_GROUP_ROLEMAPPINGS = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}" -URL_CLIENT_GROUP_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/available" -URL_CLIENT_GROUP_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/composite" - -URL_USERS = "{url}/admin/realms/{realm}/users" -URL_USER = "{url}/admin/realms/{realm}/users/{id}" -URL_USER_ROLE_MAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings" -URL_USER_REALM_ROLE_MAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/realm" -URL_USER_CLIENTS_ROLE_MAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients" -URL_USER_CLIENT_ROLE_MAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client_id}" -URL_USER_GROUPS = "{url}/admin/realms/{realm}/users/{id}/groups" -URL_USER_GROUP = "{url}/admin/realms/{realm}/users/{id}/groups/{group_id}" - -URL_CLIENT_SERVICE_ACCOUNT_USER = "{url}/admin/realms/{realm}/clients/{id}/service-account-user" -URL_CLIENT_USER_ROLEMAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client}" -URL_CLIENT_USER_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client}/available" -URL_CLIENT_USER_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client}/composite" - -URL_REALM_GROUP_ROLEMAPPINGS = "{url}/admin/realms/{realm}/groups/{group}/role-mappings/realm" - -URL_CLIENTSECRET = "{url}/admin/realms/{realm}/clients/{id}/client-secret" - -URL_AUTHENTICATION_FLOWS = "{url}/admin/realms/{realm}/authentication/flows" -URL_AUTHENTICATION_FLOW = "{url}/admin/realms/{realm}/authentication/flows/{id}" -URL_AUTHENTICATION_FLOW_COPY = "{url}/admin/realms/{realm}/authentication/flows/{copyfrom}/copy" -URL_AUTHENTICATION_FLOW_EXECUTIONS = "{url}/admin/realms/{realm}/authentication/flows/{flowalias}/executions" -URL_AUTHENTICATION_FLOW_EXECUTIONS_EXECUTION = "{url}/admin/realms/{realm}/authentication/flows/{flowalias}/executions/execution" -URL_AUTHENTICATION_FLOW_EXECUTIONS_FLOW = "{url}/admin/realms/{realm}/authentication/flows/{flowalias}/executions/flow" -URL_AUTHENTICATION_EXECUTION_CONFIG = "{url}/admin/realms/{realm}/authentication/executions/{id}/config" -URL_AUTHENTICATION_EXECUTION_RAISE_PRIORITY = "{url}/admin/realms/{realm}/authentication/executions/{id}/raise-priority" -URL_AUTHENTICATION_EXECUTION_LOWER_PRIORITY = "{url}/admin/realms/{realm}/authentication/executions/{id}/lower-priority" -URL_AUTHENTICATION_CONFIG = "{url}/admin/realms/{realm}/authentication/config/{id}" -URL_AUTHENTICATION_REGISTER_REQUIRED_ACTION = "{url}/admin/realms/{realm}/authentication/register-required-action" -URL_AUTHENTICATION_REQUIRED_ACTIONS = "{url}/admin/realms/{realm}/authentication/required-actions" -URL_AUTHENTICATION_REQUIRED_ACTIONS_ALIAS = "{url}/admin/realms/{realm}/authentication/required-actions/{alias}" - -URL_IDENTITY_PROVIDERS = "{url}/admin/realms/{realm}/identity-provider/instances" -URL_IDENTITY_PROVIDER = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}" -URL_IDENTITY_PROVIDER_MAPPERS = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}/mappers" -URL_IDENTITY_PROVIDER_MAPPER = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}/mappers/{id}" - -URL_COMPONENTS = "{url}/admin/realms/{realm}/components" -URL_COMPONENT = "{url}/admin/realms/{realm}/components/{id}" - -URL_AUTHZ_AUTHORIZATION_SCOPE = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/scope/{id}" -URL_AUTHZ_AUTHORIZATION_SCOPES = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/scope" - -# This URL is used for: -# - Querying client authorization permissions -# - Removing client authorization permissions -URL_AUTHZ_POLICIES = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/policy" -URL_AUTHZ_POLICY = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/policy/{id}" - -URL_AUTHZ_PERMISSION = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/permission/{permission_type}/{id}" -URL_AUTHZ_PERMISSIONS = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/permission/{permission_type}" - -URL_AUTHZ_RESOURCES = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/resource" - -URL_AUTHZ_CUSTOM_POLICY = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/policy/{policy_type}" -URL_AUTHZ_CUSTOM_POLICIES = "{url}/admin/realms/{realm}/clients/{client_id}/authz/resource-server/policy" - - -def keycloak_argument_spec(): - """ - Returns argument_spec of options common to keycloak_*-modules - - :return: argument_spec dict - """ - return dict( - auth_keycloak_url=dict(type='str', aliases=['url'], required=True, no_log=False), - auth_client_id=dict(type='str', default='admin-cli'), - auth_realm=dict(type='str'), - auth_client_secret=dict(type='str', default=None, no_log=True), - auth_username=dict(type='str', aliases=['username']), - auth_password=dict(type='str', aliases=['password'], no_log=True), - validate_certs=dict(type='bool', default=True), - connection_timeout=dict(type='int', default=10), - token=dict(type='str', no_log=True), - http_agent=dict(type='str', default='Ansible'), - ) - - -def camel(words): - return words.split('_')[0] + ''.join(x.capitalize() or '_' for x in words.split('_')[1:]) - - -class KeycloakError(Exception): - pass - - -def get_token(module_params): - """ Obtains connection header with token for the authentication, - token already given or obtained from credentials - :param module_params: parameters of the module - :return: connection header - """ - token = module_params.get('token') - base_url = module_params.get('auth_keycloak_url') - http_agent = module_params.get('http_agent') - - if not base_url.lower().startswith(('http', 'https')): - raise KeycloakError("auth_url '%s' should either start with 'http' or 'https'." % base_url) - - if token is None: - base_url = module_params.get('auth_keycloak_url') - validate_certs = module_params.get('validate_certs') - auth_realm = module_params.get('auth_realm') - client_id = module_params.get('auth_client_id') - auth_username = module_params.get('auth_username') - auth_password = module_params.get('auth_password') - client_secret = module_params.get('auth_client_secret') - connection_timeout = module_params.get('connection_timeout') - auth_url = URL_TOKEN.format(url=base_url, realm=auth_realm) - temp_payload = { - 'grant_type': 'password', - 'client_id': client_id, - 'client_secret': client_secret, - 'username': auth_username, - 'password': auth_password, - } - # Remove empty items, for instance missing client_secret - payload = {k: v for k, v in temp_payload.items() if v is not None} - try: - r = json.loads(to_native(open_url(auth_url, method='POST', - validate_certs=validate_certs, http_agent=http_agent, timeout=connection_timeout, - data=urlencode(payload)).read())) - except ValueError as e: - raise KeycloakError( - 'API returned invalid JSON when trying to obtain access token from %s: %s' - % (auth_url, str(e))) - except Exception as e: - raise KeycloakError('Could not obtain access token from %s: %s' - % (auth_url, str(e))) - - try: - token = r['access_token'] - except KeyError: - raise KeycloakError( - 'Could not obtain access token from %s' % auth_url) - return { - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/json' - } - - -def is_struct_included(struct1, struct2, exclude=None): - """ - This function compare if the first parameter structure is included in the second. - The function use every elements of struct1 and validates they are present in the struct2 structure. - The two structure does not need to be equals for that function to return true. - Each elements are compared recursively. - :param struct1: - type: - dict for the initial call, can be dict, list, bool, int or str for recursive calls - description: - reference structure - :param struct2: - type: - dict for the initial call, can be dict, list, bool, int or str for recursive calls - description: - structure to compare with first parameter. - :param exclude: - type: - list - description: - Key to exclude from the comparison. - default: None - :return: - type: - bool - description: - Return True if all element of dict 1 are present in dict 2, return false otherwise. - """ - if isinstance(struct1, list) and isinstance(struct2, list): - if not struct1 and not struct2: - return True - for item1 in struct1: - if isinstance(item1, (list, dict)): - for item2 in struct2: - if is_struct_included(item1, item2, exclude): - break - else: - return False - else: - if item1 not in struct2: - return False - return True - elif isinstance(struct1, dict) and isinstance(struct2, dict): - if not struct1 and not struct2: - return True - try: - for key in struct1: - if not (exclude and key in exclude): - if not is_struct_included(struct1[key], struct2[key], exclude): - return False - except KeyError: - return False - return True - elif isinstance(struct1, bool) and isinstance(struct2, bool): - return struct1 == struct2 - else: - return to_text(struct1, 'utf-8') == to_text(struct2, 'utf-8') - - -class KeycloakAPI(object): - """ Keycloak API access; Keycloak uses OAuth 2.0 to protect its API, an access token for which - is obtained through OpenID connect - """ - def __init__(self, module, connection_header): - self.module = module - self.baseurl = self.module.params.get('auth_keycloak_url') - self.validate_certs = self.module.params.get('validate_certs') - self.connection_timeout = self.module.params.get('connection_timeout') - self.restheaders = connection_header - self.http_agent = self.module.params.get('http_agent') - - def get_realm_info_by_id(self, realm='master'): - """ Obtain realm public info by id - - :param realm: realm id - :return: dict of real, representation or None if none matching exist - """ - realm_info_url = URL_REALM_INFO.format(url=self.baseurl, realm=realm) - - try: - return json.loads(to_native(open_url(realm_info_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not obtain realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - except Exception as e: - self.module.fail_json(msg='Could not obtain realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - - def get_realm_keys_metadata_by_id(self, realm='master'): - """Obtain realm public info by id - - :param realm: realm id - - :return: None, or a 'KeysMetadataRepresentation' - (https://www.keycloak.org/docs-api/latest/rest-api/index.html#KeysMetadataRepresentation) - -- a dict containing the keys 'active' and 'keys', the former containing a mapping - from algorithms to key-ids, the latter containing a list of dicts with key - information. - """ - realm_keys_metadata_url = URL_REALM_KEYS_METADATA.format(url=self.baseurl, realm=realm) - - try: - return json.loads(to_native(open_url(realm_keys_metadata_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not obtain realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - except Exception as e: - self.module.fail_json(msg='Could not obtain realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - - def get_realm_by_id(self, realm='master'): - """ Obtain realm representation by id - - :param realm: realm id - :return: dict of real, representation or None if none matching exist - """ - realm_url = URL_REALM.format(url=self.baseurl, realm=realm) - - try: - return json.loads(to_native(open_url(realm_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not obtain realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - except Exception as e: - self.module.fail_json(msg='Could not obtain realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - - def update_realm(self, realmrep, realm="master"): - """ Update an existing realm - :param realmrep: corresponding (partial/full) realm representation with updates - :param realm: realm to be updated in Keycloak - :return: HTTPResponse object on success - """ - realm_url = URL_REALM.format(url=self.baseurl, realm=realm) - - try: - return open_url(realm_url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(realmrep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not update realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - - def create_realm(self, realmrep): - """ Create a realm in keycloak - :param realmrep: Realm representation of realm to be created. - :return: HTTPResponse object on success - """ - realm_url = URL_REALMS.format(url=self.baseurl) - - try: - return open_url(realm_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(realmrep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create realm %s: %s' % (realmrep['id'], str(e)), - exception=traceback.format_exc()) - - def delete_realm(self, realm="master"): - """ Delete a realm from Keycloak - - :param realm: realm to be deleted - :return: HTTPResponse object on success - """ - realm_url = URL_REALM.format(url=self.baseurl, realm=realm) - - try: - return open_url(realm_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not delete realm %s: %s' % (realm, str(e)), - exception=traceback.format_exc()) - - def get_clients(self, realm='master', filter=None): - """ Obtains client representations for clients in a realm - - :param realm: realm to be queried - :param filter: if defined, only the client with clientId specified in the filter is returned - :return: list of dicts of client representations - """ - clientlist_url = URL_CLIENTS.format(url=self.baseurl, realm=realm) - if filter is not None: - clientlist_url += '?clientId=%s' % filter - - try: - return json.loads(to_native(open_url(clientlist_url, http_agent=self.http_agent, method='GET', headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of clients for realm %s: %s' - % (realm, str(e))) - except Exception as e: - self.fail_open_url(e, msg='Could not obtain list of clients for realm %s: %s' - % (realm, str(e))) - - def get_client_by_clientid(self, client_id, realm='master'): - """ Get client representation by clientId - :param client_id: The clientId to be queried - :param realm: realm from which to obtain the client representation - :return: dict with a client representation or None if none matching exist - """ - r = self.get_clients(realm=realm, filter=client_id) - if len(r) > 0: - return r[0] - else: - return None - - def get_client_by_id(self, id, realm='master'): - """ Obtain client representation by id - - :param id: id (not clientId) of client to be queried - :param realm: client from this realm - :return: dict of client representation or None if none matching exist - """ - client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id) - - try: - return json.loads(to_native(open_url(client_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not obtain client %s for realm %s: %s' - % (id, realm, str(e))) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain client %s for realm %s: %s' - % (id, realm, str(e))) - except Exception as e: - self.module.fail_json(msg='Could not obtain client %s for realm %s: %s' - % (id, realm, str(e))) - - def get_client_id(self, client_id, realm='master'): - """ Obtain id of client by client_id - - :param client_id: client_id of client to be queried - :param realm: client template from this realm - :return: id of client (usually a UUID) - """ - result = self.get_client_by_clientid(client_id, realm) - if isinstance(result, dict) and 'id' in result: - return result['id'] - else: - return None - - def update_client(self, id, clientrep, realm="master"): - """ Update an existing client - :param id: id (not clientId) of client to be updated in Keycloak - :param clientrep: corresponding (partial/full) client representation with updates - :param realm: realm the client is in - :return: HTTPResponse object on success - """ - client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id) - - try: - return open_url(client_url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(clientrep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not update client %s in realm %s: %s' - % (id, realm, str(e))) - - def create_client(self, clientrep, realm="master"): - """ Create a client in keycloak - :param clientrep: Client representation of client to be created. Must at least contain field clientId. - :param realm: realm for client to be created. - :return: HTTPResponse object on success - """ - client_url = URL_CLIENTS.format(url=self.baseurl, realm=realm) - - try: - return open_url(client_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(clientrep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create client %s in realm %s: %s' - % (clientrep['clientId'], realm, str(e))) - - def delete_client(self, id, realm="master"): - """ Delete a client from Keycloak - - :param id: id (not clientId) of client to be deleted - :param realm: realm of client to be deleted - :return: HTTPResponse object on success - """ - client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id) - - try: - return open_url(client_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not delete client %s in realm %s: %s' - % (id, realm, str(e))) - - def get_client_roles_by_id(self, cid, realm="master"): - """ Fetch the roles of the a client on the Keycloak server. - - :param cid: ID of the client from which to obtain the rolemappings. - :param realm: Realm from which to obtain the rolemappings. - :return: The rollemappings of specified group and client of the realm (default "master"). - """ - client_roles_url = URL_CLIENT_ROLES.format(url=self.baseurl, realm=realm, id=cid) - try: - return json.loads(to_native(open_url(client_roles_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch rolemappings for client %s in realm %s: %s" - % (cid, realm, str(e))) - - def get_client_role_id_by_name(self, cid, name, realm="master"): - """ Get the role ID of a client. - - :param cid: ID of the client from which to obtain the rolemappings. - :param name: Name of the role. - :param realm: Realm from which to obtain the rolemappings. - :return: The ID of the role, None if not found. - """ - rolemappings = self.get_client_roles_by_id(cid, realm=realm) - for role in rolemappings: - if name == role['name']: - return role['id'] - return None - - def get_client_group_rolemapping_by_id(self, gid, cid, rid, realm='master'): - """ Obtain client representation by id - - :param gid: ID of the group from which to obtain the rolemappings. - :param cid: ID of the client from which to obtain the rolemappings. - :param rid: ID of the role. - :param realm: client from this realm - :return: dict of rolemapping representation or None if none matching exist - """ - rolemappings_url = URL_CLIENT_GROUP_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid) - try: - rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - for role in rolemappings: - if rid == role['id']: - return role - except Exception as e: - self.fail_open_url(e, msg="Could not fetch rolemappings for client %s in group %s, realm %s: %s" - % (cid, gid, realm, str(e))) - return None - - def get_client_group_available_rolemappings(self, gid, cid, realm="master"): - """ Fetch the available role of a client in a specified group on the Keycloak server. - - :param gid: ID of the group from which to obtain the rolemappings. - :param cid: ID of the client from which to obtain the rolemappings. - :param realm: Realm from which to obtain the rolemappings. - :return: The rollemappings of specified group and client of the realm (default "master"). - """ - available_rolemappings_url = URL_CLIENT_GROUP_ROLEMAPPINGS_AVAILABLE.format(url=self.baseurl, realm=realm, id=gid, client=cid) - try: - return json.loads(to_native(open_url(available_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s" - % (cid, gid, realm, str(e))) - - def get_client_group_composite_rolemappings(self, gid, cid, realm="master"): - """ Fetch the composite role of a client in a specified group on the Keycloak server. - - :param gid: ID of the group from which to obtain the rolemappings. - :param cid: ID of the client from which to obtain the rolemappings. - :param realm: Realm from which to obtain the rolemappings. - :return: The rollemappings of specified group and client of the realm (default "master"). - """ - composite_rolemappings_url = URL_CLIENT_GROUP_ROLEMAPPINGS_COMPOSITE.format(url=self.baseurl, realm=realm, id=gid, client=cid) - try: - return json.loads(to_native(open_url(composite_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s" - % (cid, gid, realm, str(e))) - - def get_role_by_id(self, rid, realm="master"): - """ Fetch a role by its id on the Keycloak server. - - :param rid: ID of the role. - :param realm: Realm from which to obtain the rolemappings. - :return: The role. - """ - client_roles_url = URL_ROLES_BY_ID.format(url=self.baseurl, realm=realm, id=rid) - try: - return json.loads(to_native(open_url(client_roles_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch role for id %s in realm %s: %s" - % (rid, realm, str(e))) - - def get_client_roles_by_id_composite_rolemappings(self, rid, cid, realm="master"): - """ Fetch a role by its id on the Keycloak server. - - :param rid: ID of the composite role. - :param cid: ID of the client from which to obtain the rolemappings. - :param realm: Realm from which to obtain the rolemappings. - :return: The role. - """ - client_roles_url = URL_ROLES_BY_ID_COMPOSITES_CLIENTS.format(url=self.baseurl, realm=realm, id=rid, cid=cid) - try: - return json.loads(to_native(open_url(client_roles_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch role for id %s and cid %s in realm %s: %s" - % (rid, cid, realm, str(e))) - - def add_client_roles_by_id_composite_rolemapping(self, rid, roles_rep, realm="master"): - """ Assign roles to composite role - - :param rid: ID of the composite role. - :param roles_rep: Representation of the roles to assign. - :param realm: Realm from which to obtain the rolemappings. - :return: None. - """ - available_rolemappings_url = URL_ROLES_BY_ID_COMPOSITES.format(url=self.baseurl, realm=realm, id=rid) - try: - open_url(available_rolemappings_url, method="POST", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(roles_rep), - validate_certs=self.validate_certs, timeout=self.connection_timeout) - except Exception as e: - self.fail_open_url(e, msg="Could not assign roles to composite role %s and realm %s: %s" - % (rid, realm, str(e))) - - def add_group_realm_rolemapping(self, gid, role_rep, realm="master"): - """ Add the specified realm role to specified group on the Keycloak server. - - :param gid: ID of the group to add the role mapping. - :param role_rep: Representation of the role to assign. - :param realm: Realm from which to obtain the rolemappings. - :return: None. - """ - url = URL_REALM_GROUP_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, group=gid) - try: - open_url(url, method="POST", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep), - validate_certs=self.validate_certs, timeout=self.connection_timeout) - except Exception as e: - self.fail_open_url(e, msg="Could add realm role mappings for group %s, realm %s: %s" - % (gid, realm, str(e))) - - def delete_group_realm_rolemapping(self, gid, role_rep, realm="master"): - """ Delete the specified realm role from the specified group on the Keycloak server. - - :param gid: ID of the group from which to obtain the rolemappings. - :param role_rep: Representation of the role to assign. - :param realm: Realm from which to obtain the rolemappings. - :return: None. - """ - url = URL_REALM_GROUP_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, group=gid) - try: - open_url(url, method="DELETE", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep), - validate_certs=self.validate_certs, timeout=self.connection_timeout) - except Exception as e: - self.fail_open_url(e, msg="Could not delete realm role mappings for group %s, realm %s: %s" - % (gid, realm, str(e))) - - def add_group_rolemapping(self, gid, cid, role_rep, realm="master"): - """ Fetch the composite role of a client in a specified group on the Keycloak server. - - :param gid: ID of the group from which to obtain the rolemappings. - :param cid: ID of the client from which to obtain the rolemappings. - :param role_rep: Representation of the role to assign. - :param realm: Realm from which to obtain the rolemappings. - :return: None. - """ - available_rolemappings_url = URL_CLIENT_GROUP_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid) - try: - open_url(available_rolemappings_url, method="POST", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep), - validate_certs=self.validate_certs, timeout=self.connection_timeout) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s" - % (cid, gid, realm, str(e))) - - def delete_group_rolemapping(self, gid, cid, role_rep, realm="master"): - """ Delete the rolemapping of a client in a specified group on the Keycloak server. - - :param gid: ID of the group from which to obtain the rolemappings. - :param cid: ID of the client from which to obtain the rolemappings. - :param role_rep: Representation of the role to assign. - :param realm: Realm from which to obtain the rolemappings. - :return: None. - """ - available_rolemappings_url = URL_CLIENT_GROUP_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid) - try: - open_url(available_rolemappings_url, method="DELETE", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep), - validate_certs=self.validate_certs, timeout=self.connection_timeout) - except Exception as e: - self.fail_open_url(e, msg="Could not delete available rolemappings for client %s in group %s, realm %s: %s" - % (cid, gid, realm, str(e))) - - def get_client_user_rolemapping_by_id(self, uid, cid, rid, realm='master'): - """ Obtain client representation by id - - :param uid: ID of the user from which to obtain the rolemappings. - :param cid: ID of the client from which to obtain the rolemappings. - :param rid: ID of the role. - :param realm: client from this realm - :return: dict of rolemapping representation or None if none matching exist - """ - rolemappings_url = URL_CLIENT_USER_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid, client=cid) - try: - rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - for role in rolemappings: - if rid == role['id']: - return role - except Exception as e: - self.fail_open_url(e, msg="Could not fetch rolemappings for client %s and user %s, realm %s: %s" - % (cid, uid, realm, str(e))) - return None - - def get_client_user_available_rolemappings(self, uid, cid, realm="master"): - """ Fetch the available role of a client for a specified user on the Keycloak server. - - :param uid: ID of the user from which to obtain the rolemappings. - :param cid: ID of the client from which to obtain the rolemappings. - :param realm: Realm from which to obtain the rolemappings. - :return: The effective rollemappings of specified client and user of the realm (default "master"). - """ - available_rolemappings_url = URL_CLIENT_USER_ROLEMAPPINGS_AVAILABLE.format(url=self.baseurl, realm=realm, id=uid, client=cid) - try: - return json.loads(to_native(open_url(available_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch effective rolemappings for client %s and user %s, realm %s: %s" - % (cid, uid, realm, str(e))) - - def get_client_user_composite_rolemappings(self, uid, cid, realm="master"): - """ Fetch the composite role of a client for a specified user on the Keycloak server. - - :param uid: ID of the user from which to obtain the rolemappings. - :param cid: ID of the client from which to obtain the rolemappings. - :param realm: Realm from which to obtain the rolemappings. - :return: The rollemappings of specified group and client of the realm (default "master"). - """ - composite_rolemappings_url = URL_CLIENT_USER_ROLEMAPPINGS_COMPOSITE.format(url=self.baseurl, realm=realm, id=uid, client=cid) - try: - return json.loads(to_native(open_url(composite_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch available rolemappings for user %s of realm %s: %s" - % (uid, realm, str(e))) - - def get_realm_user_rolemapping_by_id(self, uid, rid, realm='master'): - """ Obtain role representation by id - - :param uid: ID of the user from which to obtain the rolemappings. - :param rid: ID of the role. - :param realm: client from this realm - :return: dict of rolemapping representation or None if none matching exist - """ - rolemappings_url = URL_REALM_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid) - try: - rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - for role in rolemappings: - if rid == role['id']: - return role - except Exception as e: - self.fail_open_url(e, msg="Could not fetch rolemappings for user %s, realm %s: %s" - % (uid, realm, str(e))) - return None - - def get_realm_user_available_rolemappings(self, uid, realm="master"): - """ Fetch the available role of a realm for a specified user on the Keycloak server. - - :param uid: ID of the user from which to obtain the rolemappings. - :param realm: Realm from which to obtain the rolemappings. - :return: The rollemappings of specified group and client of the realm (default "master"). - """ - available_rolemappings_url = URL_REALM_ROLEMAPPINGS_AVAILABLE.format(url=self.baseurl, realm=realm, id=uid) - try: - return json.loads(to_native(open_url(available_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch available rolemappings for user %s of realm %s: %s" - % (uid, realm, str(e))) - - def get_realm_user_composite_rolemappings(self, uid, realm="master"): - """ Fetch the composite role of a realm for a specified user on the Keycloak server. - - :param uid: ID of the user from which to obtain the rolemappings. - :param realm: Realm from which to obtain the rolemappings. - :return: The effective rollemappings of specified client and user of the realm (default "master"). - """ - composite_rolemappings_url = URL_REALM_ROLEMAPPINGS_COMPOSITE.format(url=self.baseurl, realm=realm, id=uid) - try: - return json.loads(to_native(open_url(composite_rolemappings_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch effective rolemappings for user %s, realm %s: %s" - % (uid, realm, str(e))) - - def get_user_by_username(self, username, realm="master"): - """ Fetch a keycloak user within a realm based on its username. - - If the user does not exist, None is returned. - :param username: Username of the user to fetch. - :param realm: Realm in which the user resides; default 'master' - """ - users_url = URL_USERS.format(url=self.baseurl, realm=realm) - users_url += '?username=%s&exact=true' % username - try: - userrep = None - users = json.loads(to_native(open_url(users_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - for user in users: - if user['username'] == username: - userrep = user - break - return userrep - - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain the user for realm %s and username %s: %s' - % (realm, username, str(e))) - except Exception as e: - self.fail_open_url(e, msg='Could not obtain the user for realm %s and username %s: %s' - % (realm, username, str(e))) - - def get_service_account_user_by_client_id(self, client_id, realm="master"): - """ Fetch a keycloak service account user within a realm based on its client_id. - - If the user does not exist, None is returned. - :param client_id: clientId of the service account user to fetch. - :param realm: Realm in which the user resides; default 'master' - """ - cid = self.get_client_id(client_id, realm=realm) - - service_account_user_url = URL_CLIENT_SERVICE_ACCOUNT_USER.format(url=self.baseurl, realm=realm, id=cid) - try: - return json.loads(to_native(open_url(service_account_user_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain the service-account-user for realm %s and client_id %s: %s' - % (realm, client_id, str(e))) - except Exception as e: - self.fail_open_url(e, msg='Could not obtain the service-account-user for realm %s and client_id %s: %s' - % (realm, client_id, str(e))) - - def add_user_rolemapping(self, uid, cid, role_rep, realm="master"): - """ Assign a realm or client role to a specified user on the Keycloak server. - - :param uid: ID of the user roles are assigned to. - :param cid: ID of the client from which to obtain the rolemappings. If empty, roles are from the realm - :param role_rep: Representation of the role to assign. - :param realm: Realm from which to obtain the rolemappings. - :return: None. - """ - if cid is None: - user_realm_rolemappings_url = URL_REALM_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid) - try: - open_url(user_realm_rolemappings_url, method="POST", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep), - validate_certs=self.validate_certs, timeout=self.connection_timeout) - except Exception as e: - self.fail_open_url(e, msg="Could not map roles to userId %s for realm %s and roles %s: %s" - % (uid, realm, json.dumps(role_rep), str(e))) - else: - user_client_rolemappings_url = URL_CLIENT_USER_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid, client=cid) - try: - open_url(user_client_rolemappings_url, method="POST", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep), - validate_certs=self.validate_certs, timeout=self.connection_timeout) - except Exception as e: - self.fail_open_url(e, msg="Could not map roles to userId %s for client %s, realm %s and roles %s: %s" - % (cid, uid, realm, json.dumps(role_rep), str(e))) - - def delete_user_rolemapping(self, uid, cid, role_rep, realm="master"): - """ Delete the rolemapping of a client in a specified user on the Keycloak server. - - :param uid: ID of the user from which to remove the rolemappings. - :param cid: ID of the client from which to remove the rolemappings. - :param role_rep: Representation of the role to remove from rolemappings. - :param realm: Realm from which to remove the rolemappings. - :return: None. - """ - if cid is None: - user_realm_rolemappings_url = URL_REALM_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid) - try: - open_url(user_realm_rolemappings_url, method="DELETE", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep), - validate_certs=self.validate_certs, timeout=self.connection_timeout) - except Exception as e: - self.fail_open_url(e, msg="Could not remove roles %s from userId %s, realm %s: %s" - % (json.dumps(role_rep), uid, realm, str(e))) - else: - user_client_rolemappings_url = URL_CLIENT_USER_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=uid, client=cid) - try: - open_url(user_client_rolemappings_url, method="DELETE", http_agent=self.http_agent, headers=self.restheaders, data=json.dumps(role_rep), - validate_certs=self.validate_certs, timeout=self.connection_timeout) - except Exception as e: - self.fail_open_url(e, msg="Could not remove roles %s for client %s from userId %s, realm %s: %s" - % (json.dumps(role_rep), cid, uid, realm, str(e))) - - def get_client_templates(self, realm='master'): - """ Obtains client template representations for client templates in a realm - - :param realm: realm to be queried - :return: list of dicts of client representations - """ - url = URL_CLIENTTEMPLATES.format(url=self.baseurl, realm=realm) - - try: - return json.loads(to_native(open_url(url, method='GET', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of client templates for realm %s: %s' - % (realm, str(e))) - except Exception as e: - self.fail_open_url(e, msg='Could not obtain list of client templates for realm %s: %s' - % (realm, str(e))) - - def get_client_template_by_id(self, id, realm='master'): - """ Obtain client template representation by id - - :param id: id (not name) of client template to be queried - :param realm: client template from this realm - :return: dict of client template representation or None if none matching exist - """ - url = URL_CLIENTTEMPLATE.format(url=self.baseurl, id=id, realm=realm) - - try: - return json.loads(to_native(open_url(url, method='GET', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain client templates %s for realm %s: %s' - % (id, realm, str(e))) - except Exception as e: - self.fail_open_url(e, msg='Could not obtain client template %s for realm %s: %s' - % (id, realm, str(e))) - - def get_client_template_by_name(self, name, realm='master'): - """ Obtain client template representation by name - - :param name: name of client template to be queried - :param realm: client template from this realm - :return: dict of client template representation or None if none matching exist - """ - result = self.get_client_templates(realm) - if isinstance(result, list): - result = [x for x in result if x['name'] == name] - if len(result) > 0: - return result[0] - return None - - def get_client_template_id(self, name, realm='master'): - """ Obtain client template id by name - - :param name: name of client template to be queried - :param realm: client template from this realm - :return: client template id (usually a UUID) - """ - result = self.get_client_template_by_name(name, realm) - if isinstance(result, dict) and 'id' in result: - return result['id'] - else: - return None - - def update_client_template(self, id, clienttrep, realm="master"): - """ Update an existing client template - :param id: id (not name) of client template to be updated in Keycloak - :param clienttrep: corresponding (partial/full) client template representation with updates - :param realm: realm the client template is in - :return: HTTPResponse object on success - """ - url = URL_CLIENTTEMPLATE.format(url=self.baseurl, realm=realm, id=id) - - try: - return open_url(url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(clienttrep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not update client template %s in realm %s: %s' - % (id, realm, str(e))) - - def create_client_template(self, clienttrep, realm="master"): - """ Create a client in keycloak - :param clienttrep: Client template representation of client template to be created. Must at least contain field name - :param realm: realm for client template to be created in - :return: HTTPResponse object on success - """ - url = URL_CLIENTTEMPLATES.format(url=self.baseurl, realm=realm) - - try: - return open_url(url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(clienttrep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create client template %s in realm %s: %s' - % (clienttrep['clientId'], realm, str(e))) - - def delete_client_template(self, id, realm="master"): - """ Delete a client template from Keycloak - - :param id: id (not name) of client to be deleted - :param realm: realm of client template to be deleted - :return: HTTPResponse object on success - """ - url = URL_CLIENTTEMPLATE.format(url=self.baseurl, realm=realm, id=id) - - try: - return open_url(url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not delete client template %s in realm %s: %s' - % (id, realm, str(e))) - - def get_clientscopes(self, realm="master"): - """ Fetch the name and ID of all clientscopes on the Keycloak server. - - To fetch the full data of the group, make a subsequent call to - get_clientscope_by_clientscopeid, passing in the ID of the group you wish to return. - - :param realm: Realm in which the clientscope resides; default 'master'. - :return The clientscopes of this realm (default "master") - """ - clientscopes_url = URL_CLIENTSCOPES.format(url=self.baseurl, realm=realm) - try: - return json.loads(to_native(open_url(clientscopes_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch list of clientscopes in realm %s: %s" - % (realm, str(e))) - - def get_clientscope_by_clientscopeid(self, cid, realm="master"): - """ Fetch a keycloak clientscope from the provided realm using the clientscope's unique ID. - - If the clientscope does not exist, None is returned. - - gid is a UUID provided by the Keycloak API - :param cid: UUID of the clientscope to be returned - :param realm: Realm in which the clientscope resides; default 'master'. - """ - clientscope_url = URL_CLIENTSCOPE.format(url=self.baseurl, realm=realm, id=cid) - try: - return json.loads(to_native(open_url(clientscope_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg="Could not fetch clientscope %s in realm %s: %s" - % (cid, realm, str(e))) - except Exception as e: - self.module.fail_json(msg="Could not clientscope group %s in realm %s: %s" - % (cid, realm, str(e))) - - def get_clientscope_by_name(self, name, realm="master"): - """ Fetch a keycloak clientscope within a realm based on its name. - - The Keycloak API does not allow filtering of the clientscopes resource by name. - As a result, this method first retrieves the entire list of clientscopes - name and ID - - then performs a second query to fetch the group. - - If the clientscope does not exist, None is returned. - :param name: Name of the clientscope to fetch. - :param realm: Realm in which the clientscope resides; default 'master' - """ - try: - all_clientscopes = self.get_clientscopes(realm=realm) - - for clientscope in all_clientscopes: - if clientscope['name'] == name: - return self.get_clientscope_by_clientscopeid(clientscope['id'], realm=realm) - - return None - - except Exception as e: - self.module.fail_json(msg="Could not fetch clientscope %s in realm %s: %s" - % (name, realm, str(e))) - - def create_clientscope(self, clientscoperep, realm="master"): - """ Create a Keycloak clientscope. - - :param clientscoperep: a ClientScopeRepresentation of the clientscope to be created. Must contain at minimum the field name. - :return: HTTPResponse object on success - """ - clientscopes_url = URL_CLIENTSCOPES.format(url=self.baseurl, realm=realm) - try: - return open_url(clientscopes_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(clientscoperep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg="Could not create clientscope %s in realm %s: %s" - % (clientscoperep['name'], realm, str(e))) - - def update_clientscope(self, clientscoperep, realm="master"): - """ Update an existing clientscope. - - :param grouprep: A GroupRepresentation of the updated group. - :return HTTPResponse object on success - """ - clientscope_url = URL_CLIENTSCOPE.format(url=self.baseurl, realm=realm, id=clientscoperep['id']) - - try: - return open_url(clientscope_url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(clientscoperep), validate_certs=self.validate_certs) - - except Exception as e: - self.fail_open_url(e, msg='Could not update clientscope %s in realm %s: %s' - % (clientscoperep['name'], realm, str(e))) - - def delete_clientscope(self, name=None, cid=None, realm="master"): - """ Delete a clientscope. One of name or cid must be provided. - - Providing the clientscope ID is preferred as it avoids a second lookup to - convert a clientscope name to an ID. - - :param name: The name of the clientscope. A lookup will be performed to retrieve the clientscope ID. - :param cid: The ID of the clientscope (preferred to name). - :param realm: The realm in which this group resides, default "master". - """ - - if cid is None and name is None: - # prefer an exception since this is almost certainly a programming error in the module itself. - raise Exception("Unable to delete group - one of group ID or name must be provided.") - - # only lookup the name if cid isn't provided. - # in the case that both are provided, prefer the ID, since it's one - # less lookup. - if cid is None and name is not None: - for clientscope in self.get_clientscopes(realm=realm): - if clientscope['name'] == name: - cid = clientscope['id'] - break - - # if the group doesn't exist - no problem, nothing to delete. - if cid is None: - return None - - # should have a good cid by here. - clientscope_url = URL_CLIENTSCOPE.format(realm=realm, id=cid, url=self.baseurl) - try: - return open_url(clientscope_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - - except Exception as e: - self.fail_open_url(e, msg="Unable to delete clientscope %s: %s" % (cid, str(e))) - - def get_clientscope_protocolmappers(self, cid, realm="master"): - """ Fetch the name and ID of all clientscopes on the Keycloak server. - - To fetch the full data of the group, make a subsequent call to - get_clientscope_by_clientscopeid, passing in the ID of the group you wish to return. - - :param cid: id of clientscope (not name). - :param realm: Realm in which the clientscope resides; default 'master'. - :return The protocolmappers of this realm (default "master") - """ - protocolmappers_url = URL_CLIENTSCOPE_PROTOCOLMAPPERS.format(id=cid, url=self.baseurl, realm=realm) - try: - return json.loads(to_native(open_url(protocolmappers_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch list of protocolmappers in realm %s: %s" - % (realm, str(e))) - - def get_clientscope_protocolmapper_by_protocolmapperid(self, pid, cid, realm="master"): - """ Fetch a keycloak clientscope from the provided realm using the clientscope's unique ID. - - If the clientscope does not exist, None is returned. - - gid is a UUID provided by the Keycloak API - - :param cid: UUID of the protocolmapper to be returned - :param cid: UUID of the clientscope to be returned - :param realm: Realm in which the clientscope resides; default 'master'. - """ - protocolmapper_url = URL_CLIENTSCOPE_PROTOCOLMAPPER.format(url=self.baseurl, realm=realm, id=cid, mapper_id=pid) - try: - return json.loads(to_native(open_url(protocolmapper_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg="Could not fetch protocolmapper %s in realm %s: %s" - % (pid, realm, str(e))) - except Exception as e: - self.module.fail_json(msg="Could not fetch protocolmapper %s in realm %s: %s" - % (cid, realm, str(e))) - - def get_clientscope_protocolmapper_by_name(self, cid, name, realm="master"): - """ Fetch a keycloak clientscope within a realm based on its name. - - The Keycloak API does not allow filtering of the clientscopes resource by name. - As a result, this method first retrieves the entire list of clientscopes - name and ID - - then performs a second query to fetch the group. - - If the clientscope does not exist, None is returned. - :param cid: Id of the clientscope (not name). - :param name: Name of the protocolmapper to fetch. - :param realm: Realm in which the clientscope resides; default 'master' - """ - try: - all_protocolmappers = self.get_clientscope_protocolmappers(cid, realm=realm) - - for protocolmapper in all_protocolmappers: - if protocolmapper['name'] == name: - return self.get_clientscope_protocolmapper_by_protocolmapperid(protocolmapper['id'], cid, realm=realm) - - return None - - except Exception as e: - self.module.fail_json(msg="Could not fetch protocolmapper %s in realm %s: %s" - % (name, realm, str(e))) - - def create_clientscope_protocolmapper(self, cid, mapper_rep, realm="master"): - """ Create a Keycloak clientscope protocolmapper. - - :param cid: Id of the clientscope. - :param mapper_rep: a ProtocolMapperRepresentation of the protocolmapper to be created. Must contain at minimum the field name. - :return: HTTPResponse object on success - """ - protocolmappers_url = URL_CLIENTSCOPE_PROTOCOLMAPPERS.format(url=self.baseurl, id=cid, realm=realm) - try: - return open_url(protocolmappers_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(mapper_rep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg="Could not create protocolmapper %s in realm %s: %s" - % (mapper_rep['name'], realm, str(e))) - - def update_clientscope_protocolmappers(self, cid, mapper_rep, realm="master"): - """ Update an existing clientscope. - - :param cid: Id of the clientscope. - :param mapper_rep: A ProtocolMapperRepresentation of the updated protocolmapper. - :return HTTPResponse object on success - """ - protocolmapper_url = URL_CLIENTSCOPE_PROTOCOLMAPPER.format(url=self.baseurl, realm=realm, id=cid, mapper_id=mapper_rep['id']) - - try: - return open_url(protocolmapper_url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(mapper_rep), validate_certs=self.validate_certs) - - except Exception as e: - self.fail_open_url(e, msg='Could not update protocolmappers for clientscope %s in realm %s: %s' - % (mapper_rep, realm, str(e))) - - def get_default_clientscopes(self, realm, client_id=None): - """Fetch the name and ID of all clientscopes on the Keycloak server. - - To fetch the full data of the client scope, make a subsequent call to - get_clientscope_by_clientscopeid, passing in the ID of the client scope you wish to return. - - :param realm: Realm in which the clientscope resides. - :param client_id: The client in which the clientscope resides. - :return The default clientscopes of this realm or client - """ - url = URL_DEFAULT_CLIENTSCOPES if client_id is None else URL_CLIENT_DEFAULT_CLIENTSCOPES - return self._get_clientscopes_of_type(realm, url, 'default', client_id) - - def get_optional_clientscopes(self, realm, client_id=None): - """Fetch the name and ID of all clientscopes on the Keycloak server. - - To fetch the full data of the client scope, make a subsequent call to - get_clientscope_by_clientscopeid, passing in the ID of the client scope you wish to return. - - :param realm: Realm in which the clientscope resides. - :param client_id: The client in which the clientscope resides. - :return The optional clientscopes of this realm or client - """ - url = URL_OPTIONAL_CLIENTSCOPES if client_id is None else URL_CLIENT_OPTIONAL_CLIENTSCOPES - return self._get_clientscopes_of_type(realm, url, 'optional', client_id) - - def _get_clientscopes_of_type(self, realm, url_template, scope_type, client_id=None): - """Fetch the name and ID of all clientscopes on the Keycloak server. - - To fetch the full data of the client scope, make a subsequent call to - get_clientscope_by_clientscopeid, passing in the ID of the client scope you wish to return. - - :param realm: Realm in which the clientscope resides. - :param url_template the template for the right type - :param scope_type this can be either optional or default - :param client_id: The client in which the clientscope resides. - :return The clientscopes of the specified type of this realm - """ - if client_id is None: - clientscopes_url = url_template.format(url=self.baseurl, realm=realm) - try: - return json.loads(to_native(open_url(clientscopes_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch list of %s clientscopes in realm %s: %s" % (scope_type, realm, str(e))) - else: - cid = self.get_client_id(client_id=client_id, realm=realm) - clientscopes_url = url_template.format(url=self.baseurl, realm=realm, cid=cid) - try: - return json.loads(to_native(open_url(clientscopes_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch list of %s clientscopes in client %s: %s" % (scope_type, client_id, clientscopes_url)) - - def _decide_url_type_clientscope(self, client_id=None, scope_type="default"): - """Decides which url to use. - :param scope_type this can be either optional or default - :param client_id: The client in which the clientscope resides. - """ - if client_id is None: - if scope_type == "default": - return URL_DEFAULT_CLIENTSCOPE - if scope_type == "optional": - return URL_OPTIONAL_CLIENTSCOPE - else: - if scope_type == "default": - return URL_CLIENT_DEFAULT_CLIENTSCOPE - if scope_type == "optional": - return URL_CLIENT_OPTIONAL_CLIENTSCOPE - - def add_default_clientscope(self, id, realm="master", client_id=None): - """Add a client scope as default either on realm or client level. - - :param id: Client scope Id. - :param realm: Realm in which the clientscope resides. - :param client_id: The client in which the clientscope resides. - """ - self._action_type_clientscope(id, client_id, "default", realm, 'add') - - def add_optional_clientscope(self, id, realm="master", client_id=None): - """Add a client scope as optional either on realm or client level. - - :param id: Client scope Id. - :param realm: Realm in which the clientscope resides. - :param client_id: The client in which the clientscope resides. - """ - self._action_type_clientscope(id, client_id, "optional", realm, 'add') - - def delete_default_clientscope(self, id, realm="master", client_id=None): - """Remove a client scope as default either on realm or client level. - - :param id: Client scope Id. - :param realm: Realm in which the clientscope resides. - :param client_id: The client in which the clientscope resides. - """ - self._action_type_clientscope(id, client_id, "default", realm, 'delete') - - def delete_optional_clientscope(self, id, realm="master", client_id=None): - """Remove a client scope as optional either on realm or client level. - - :param id: Client scope Id. - :param realm: Realm in which the clientscope resides. - :param client_id: The client in which the clientscope resides. - """ - self._action_type_clientscope(id, client_id, "optional", realm, 'delete') - - def _action_type_clientscope(self, id=None, client_id=None, scope_type="default", realm="master", action='add'): - """ Delete or add a clientscope of type. - :param name: The name of the clientscope. A lookup will be performed to retrieve the clientscope ID. - :param client_id: The ID of the clientscope (preferred to name). - :param scope_type 'default' or 'optional' - :param realm: The realm in which this group resides, default "master". - """ - cid = None if client_id is None else self.get_client_id(client_id=client_id, realm=realm) - # should have a good cid by here. - clientscope_type_url = self._decide_url_type_clientscope(client_id, scope_type).format(realm=realm, id=id, cid=cid, url=self.baseurl) - try: - method = 'PUT' if action == "add" else 'DELETE' - return open_url(clientscope_type_url, method=method, http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - - except Exception as e: - place = 'realm' if client_id is None else 'client ' + client_id - self.fail_open_url(e, msg="Unable to %s %s clientscope %s @ %s : %s" % (action, scope_type, id, place, str(e))) - - def create_clientsecret(self, id, realm="master"): - """ Generate a new client secret by id - - :param id: id (not clientId) of client to be queried - :param realm: client from this realm - :return: dict of credential representation - """ - clientsecret_url = URL_CLIENTSECRET.format(url=self.baseurl, realm=realm, id=id) - - try: - return json.loads(to_native(open_url(clientsecret_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not obtain clientsecret of client %s for realm %s: %s' - % (id, realm, str(e))) - except Exception as e: - self.module.fail_json(msg='Could not obtain clientsecret of client %s for realm %s: %s' - % (id, realm, str(e))) - - def get_clientsecret(self, id, realm="master"): - """ Obtain client secret by id - - :param id: id (not clientId) of client to be queried - :param realm: client from this realm - :return: dict of credential representation - """ - clientsecret_url = URL_CLIENTSECRET.format(url=self.baseurl, realm=realm, id=id) - - try: - return json.loads(to_native(open_url(clientsecret_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not obtain clientsecret of client %s for realm %s: %s' - % (id, realm, str(e))) - except Exception as e: - self.module.fail_json(msg='Could not obtain clientsecret of client %s for realm %s: %s' - % (id, realm, str(e))) - - def get_groups(self, realm="master"): - """ Fetch the name and ID of all groups on the Keycloak server. - - To fetch the full data of the group, make a subsequent call to - get_group_by_groupid, passing in the ID of the group you wish to return. - - :param realm: Return the groups of this realm (default "master"). - """ - groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm) - try: - return json.loads(to_native(open_url(groups_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg="Could not fetch list of groups in realm %s: %s" - % (realm, str(e))) - - def get_group_by_groupid(self, gid, realm="master"): - """ Fetch a keycloak group from the provided realm using the group's unique ID. - - If the group does not exist, None is returned. - - gid is a UUID provided by the Keycloak API - :param gid: UUID of the group to be returned - :param realm: Realm in which the group resides; default 'master'. - """ - groups_url = URL_GROUP.format(url=self.baseurl, realm=realm, groupid=gid) - try: - return json.loads(to_native(open_url(groups_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg="Could not fetch group %s in realm %s: %s" - % (gid, realm, str(e))) - except Exception as e: - self.module.fail_json(msg="Could not fetch group %s in realm %s: %s" - % (gid, realm, str(e))) - - def get_group_by_name(self, name, realm="master", parents=None): - """ Fetch a keycloak group within a realm based on its name. - - The Keycloak API does not allow filtering of the Groups resource by name. - As a result, this method first retrieves the entire list of groups - name and ID - - then performs a second query to fetch the group. - - If the group does not exist, None is returned. - :param name: Name of the group to fetch. - :param realm: Realm in which the group resides; default 'master' - :param parents: Optional list of parents when group to look for is a subgroup - """ - groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm) - try: - if parents: - parent = self.get_subgroup_direct_parent(parents, realm) - - if not parent: - return None - - all_groups = parent['subGroups'] - else: - all_groups = self.get_groups(realm=realm) - - for group in all_groups: - if group['name'] == name: - return self.get_group_by_groupid(group['id'], realm=realm) - - return None - - except Exception as e: - self.module.fail_json(msg="Could not fetch group %s in realm %s: %s" - % (name, realm, str(e))) - - def _get_normed_group_parent(self, parent): - """ Converts parent dict information into a more easy to use form. - - :param parent: parent describing dict - """ - if parent['id']: - return (parent['id'], True) - - return (parent['name'], False) - - def get_subgroup_by_chain(self, name_chain, realm="master"): - """ Access a subgroup API object by walking down a given name/id chain. - - Groups can be given either as by name or by ID, the first element - must either be a toplvl group or given as ID, all parents must exist. - - If the group cannot be found, None is returned. - :param name_chain: Topdown ordered list of subgroup parent (ids or names) + its own name at the end - :param realm: Realm in which the group resides; default 'master' - """ - cp = name_chain[0] - - # for 1st parent in chain we must query the server - cp, is_id = self._get_normed_group_parent(cp) - - if is_id: - tmp = self.get_group_by_groupid(cp, realm=realm) - else: - # given as name, assume toplvl group - tmp = self.get_group_by_name(cp, realm=realm) - - if not tmp: - return None - - for p in name_chain[1:]: - for sg in tmp['subGroups']: - pv, is_id = self._get_normed_group_parent(p) - - if is_id: - cmpkey = "id" - else: - cmpkey = "name" - - if pv == sg[cmpkey]: - tmp = sg - break - - if not tmp: - return None - - return tmp - - def get_subgroup_direct_parent(self, parents, realm="master", children_to_resolve=None): - """ Get keycloak direct parent group API object for a given chain of parents. - - To successfully work the API for subgroups we actually don't need - to "walk the whole tree" for nested groups but only need to know - the ID for the direct predecessor of current subgroup. This - method will guarantee us this information getting there with - as minimal work as possible. - - Note that given parent list can and might be incomplete at the - upper levels as long as it starts with an ID instead of a name - - If the group does not exist, None is returned. - :param parents: Topdown ordered list of subgroup parents - :param realm: Realm in which the group resides; default 'master' - """ - if children_to_resolve is None: - # start recursion by reversing parents (in optimal cases - # we dont need to walk the whole tree upwarts) - parents = list(reversed(parents)) - children_to_resolve = [] - - if not parents: - # walk complete parents list to the top, all names, no id's, - # try to resolve it assuming list is complete and 1st - # element is a toplvl group - return self.get_subgroup_by_chain(list(reversed(children_to_resolve)), realm=realm) - - cp = parents[0] - unused, is_id = self._get_normed_group_parent(cp) - - if is_id: - # current parent is given as ID, we can stop walking - # upwards searching for an entry point - return self.get_subgroup_by_chain([cp] + list(reversed(children_to_resolve)), realm=realm) - else: - # current parent is given as name, it must be resolved - # later, try next parent (recurse) - children_to_resolve.append(cp) - return self.get_subgroup_direct_parent( - parents[1:], - realm=realm, children_to_resolve=children_to_resolve - ) - - def create_group(self, grouprep, realm="master"): - """ Create a Keycloak group. - - :param grouprep: a GroupRepresentation of the group to be created. Must contain at minimum the field name. - :return: HTTPResponse object on success - """ - groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm) - try: - return open_url(groups_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(grouprep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg="Could not create group %s in realm %s: %s" - % (grouprep['name'], realm, str(e))) - - def create_subgroup(self, parents, grouprep, realm="master"): - """ Create a Keycloak subgroup. - - :param parents: list of one or more parent groups - :param grouprep: a GroupRepresentation of the group to be created. Must contain at minimum the field name. - :return: HTTPResponse object on success - """ - parent_id = "---UNDETERMINED---" - try: - parent_id = self.get_subgroup_direct_parent(parents, realm) - - if not parent_id: - raise Exception( - "Could not determine subgroup parent ID for given" - " parent chain {0}. Assure that all parents exist" - " already and the list is complete and properly" - " ordered, starts with an ID or starts at the" - " top level".format(parents) - ) - - parent_id = parent_id["id"] - url = URL_GROUP_CHILDREN.format(url=self.baseurl, realm=realm, groupid=parent_id) - return open_url(url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(grouprep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg="Could not create subgroup %s for parent group %s in realm %s: %s" - % (grouprep['name'], parent_id, realm, str(e))) - - def update_group(self, grouprep, realm="master"): - """ Update an existing group. - - :param grouprep: A GroupRepresentation of the updated group. - :return HTTPResponse object on success - """ - group_url = URL_GROUP.format(url=self.baseurl, realm=realm, groupid=grouprep['id']) - - try: - return open_url(group_url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(grouprep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not update group %s in realm %s: %s' - % (grouprep['name'], realm, str(e))) - - def delete_group(self, name=None, groupid=None, realm="master"): - """ Delete a group. One of name or groupid must be provided. - - Providing the group ID is preferred as it avoids a second lookup to - convert a group name to an ID. - - :param name: The name of the group. A lookup will be performed to retrieve the group ID. - :param groupid: The ID of the group (preferred to name). - :param realm: The realm in which this group resides, default "master". - """ - - if groupid is None and name is None: - # prefer an exception since this is almost certainly a programming error in the module itself. - raise Exception("Unable to delete group - one of group ID or name must be provided.") - - # only lookup the name if groupid isn't provided. - # in the case that both are provided, prefer the ID, since it's one - # less lookup. - if groupid is None and name is not None: - for group in self.get_groups(realm=realm): - if group['name'] == name: - groupid = group['id'] - break - - # if the group doesn't exist - no problem, nothing to delete. - if groupid is None: - return None - - # should have a good groupid by here. - group_url = URL_GROUP.format(realm=realm, groupid=groupid, url=self.baseurl) - try: - return open_url(group_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg="Unable to delete group %s: %s" % (groupid, str(e))) - - def get_realm_roles(self, realm='master'): - """ Obtains role representations for roles in a realm - - :param realm: realm to be queried - :return: list of dicts of role representations - """ - rolelist_url = URL_REALM_ROLES.format(url=self.baseurl, realm=realm) - try: - return json.loads(to_native(open_url(rolelist_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of roles for realm %s: %s' - % (realm, str(e))) - except Exception as e: - self.fail_open_url(e, msg='Could not obtain list of roles for realm %s: %s' - % (realm, str(e))) - - def get_realm_role(self, name, realm='master'): - """ Fetch a keycloak role from the provided realm using the role's name. - - If the role does not exist, None is returned. - :param name: Name of the role to fetch. - :param realm: Realm in which the role resides; default 'master'. - """ - role_url = URL_REALM_ROLE.format(url=self.baseurl, realm=realm, name=quote(name, safe='')) - try: - return json.loads(to_native(open_url(role_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not fetch role %s in realm %s: %s' - % (name, realm, str(e))) - except Exception as e: - self.module.fail_json(msg='Could not fetch role %s in realm %s: %s' - % (name, realm, str(e))) - - def create_realm_role(self, rolerep, realm='master'): - """ Create a Keycloak realm role. - - :param rolerep: a RoleRepresentation of the role to be created. Must contain at minimum the field name. - :return: HTTPResponse object on success - """ - roles_url = URL_REALM_ROLES.format(url=self.baseurl, realm=realm) - try: - if "composites" in rolerep: - keycloak_compatible_composites = self.convert_role_composites(rolerep["composites"]) - rolerep["composites"] = keycloak_compatible_composites - return open_url(roles_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(rolerep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create role %s in realm %s: %s' - % (rolerep['name'], realm, str(e))) - - def update_realm_role(self, rolerep, realm='master'): - """ Update an existing realm role. - - :param rolerep: A RoleRepresentation of the updated role. - :return HTTPResponse object on success - """ - role_url = URL_REALM_ROLE.format(url=self.baseurl, realm=realm, name=quote(rolerep['name']), safe='') - try: - composites = None - if "composites" in rolerep: - composites = copy.deepcopy(rolerep["composites"]) - del rolerep["composites"] - role_response = open_url(role_url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(rolerep), validate_certs=self.validate_certs) - if composites is not None: - self.update_role_composites(rolerep=rolerep, composites=composites, realm=realm) - return role_response - except Exception as e: - self.fail_open_url(e, msg='Could not update role %s in realm %s: %s' - % (rolerep['name'], realm, str(e))) - - def get_role_composites(self, rolerep, clientid=None, realm='master'): - composite_url = '' - try: - if clientid is not None: - client = self.get_client_by_clientid(client_id=clientid, realm=realm) - cid = client['id'] - composite_url = URL_CLIENT_ROLE_COMPOSITES.format(url=self.baseurl, realm=realm, id=cid, name=quote(rolerep["name"], safe='')) - else: - composite_url = URL_REALM_ROLE_COMPOSITES.format(url=self.baseurl, realm=realm, name=quote(rolerep["name"], safe='')) - # Get existing composites - return json.loads(to_native(open_url( - composite_url, - method='GET', - http_agent=self.http_agent, - headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg='Could not get role %s composites in realm %s: %s' - % (rolerep['name'], realm, str(e))) - - def create_role_composites(self, rolerep, composites, clientid=None, realm='master'): - composite_url = '' - try: - if clientid is not None: - client = self.get_client_by_clientid(client_id=clientid, realm=realm) - cid = client['id'] - composite_url = URL_CLIENT_ROLE_COMPOSITES.format(url=self.baseurl, realm=realm, id=cid, name=quote(rolerep["name"], safe='')) - else: - composite_url = URL_REALM_ROLE_COMPOSITES.format(url=self.baseurl, realm=realm, name=quote(rolerep["name"], safe='')) - # Get existing composites - # create new composites - return open_url(composite_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(composites), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create role %s composites in realm %s: %s' - % (rolerep['name'], realm, str(e))) - - def delete_role_composites(self, rolerep, composites, clientid=None, realm='master'): - composite_url = '' - try: - if clientid is not None: - client = self.get_client_by_clientid(client_id=clientid, realm=realm) - cid = client['id'] - composite_url = URL_CLIENT_ROLE_COMPOSITES.format(url=self.baseurl, realm=realm, id=cid, name=quote(rolerep["name"], safe='')) - else: - composite_url = URL_REALM_ROLE_COMPOSITES.format(url=self.baseurl, realm=realm, name=quote(rolerep["name"], safe='')) - # Get existing composites - # create new composites - return open_url(composite_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(composites), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create role %s composites in realm %s: %s' - % (rolerep['name'], realm, str(e))) - - def update_role_composites(self, rolerep, composites, clientid=None, realm='master'): - # Get existing composites - existing_composites = self.get_role_composites(rolerep=rolerep, clientid=clientid, realm=realm) - composites_to_be_created = [] - composites_to_be_deleted = [] - for composite in composites: - composite_found = False - existing_composite_client = None - for existing_composite in existing_composites: - if existing_composite["clientRole"]: - existing_composite_client = self.get_client_by_id(existing_composite["containerId"], realm=realm) - if ("client_id" in composite - and composite['client_id'] is not None - and existing_composite_client["clientId"] == composite["client_id"] - and composite["name"] == existing_composite["name"]): - composite_found = True - break - else: - if (("client_id" not in composite or composite['client_id'] is None) - and composite["name"] == existing_composite["name"]): - composite_found = True - break - if (not composite_found and ('state' not in composite or composite['state'] == 'present')): - if "client_id" in composite and composite['client_id'] is not None: - client_roles = self.get_client_roles(clientid=composite['client_id'], realm=realm) - for client_role in client_roles: - if client_role['name'] == composite['name']: - composites_to_be_created.append(client_role) - break - else: - realm_role = self.get_realm_role(name=composite["name"], realm=realm) - composites_to_be_created.append(realm_role) - elif composite_found and 'state' in composite and composite['state'] == 'absent': - if "client_id" in composite and composite['client_id'] is not None: - client_roles = self.get_client_roles(clientid=composite['client_id'], realm=realm) - for client_role in client_roles: - if client_role['name'] == composite['name']: - composites_to_be_deleted.append(client_role) - break - else: - realm_role = self.get_realm_role(name=composite["name"], realm=realm) - composites_to_be_deleted.append(realm_role) - - if len(composites_to_be_created) > 0: - # create new composites - self.create_role_composites(rolerep=rolerep, composites=composites_to_be_created, clientid=clientid, realm=realm) - if len(composites_to_be_deleted) > 0: - # delete new composites - self.delete_role_composites(rolerep=rolerep, composites=composites_to_be_deleted, clientid=clientid, realm=realm) - - def delete_realm_role(self, name, realm='master'): - """ Delete a realm role. - - :param name: The name of the role. - :param realm: The realm in which this role resides, default "master". - """ - role_url = URL_REALM_ROLE.format(url=self.baseurl, realm=realm, name=quote(name, safe='')) - try: - return open_url(role_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Unable to delete role %s in realm %s: %s' - % (name, realm, str(e))) - - def get_client_roles(self, clientid, realm='master'): - """ Obtains role representations for client roles in a specific client - - :param clientid: Client id to be queried - :param realm: Realm to be queried - :return: List of dicts of role representations - """ - cid = self.get_client_id(clientid, realm=realm) - if cid is None: - self.module.fail_json(msg='Could not find client %s in realm %s' - % (clientid, realm)) - rolelist_url = URL_CLIENT_ROLES.format(url=self.baseurl, realm=realm, id=cid) - try: - return json.loads(to_native(open_url(rolelist_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of roles for client %s in realm %s: %s' - % (clientid, realm, str(e))) - except Exception as e: - self.fail_open_url(e, msg='Could not obtain list of roles for client %s in realm %s: %s' - % (clientid, realm, str(e))) - - def get_client_role(self, name, clientid, realm='master'): - """ Fetch a keycloak client role from the provided realm using the role's name. - - :param name: Name of the role to fetch. - :param clientid: Client id for the client role - :param realm: Realm in which the role resides - :return: Dict of role representation - If the role does not exist, None is returned. - """ - cid = self.get_client_id(clientid, realm=realm) - if cid is None: - self.module.fail_json(msg='Could not find client %s in realm %s' - % (clientid, realm)) - role_url = URL_CLIENT_ROLE.format(url=self.baseurl, realm=realm, id=cid, name=quote(name, safe='')) - try: - return json.loads(to_native(open_url(role_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not fetch role %s in client %s of realm %s: %s' - % (name, clientid, realm, str(e))) - except Exception as e: - self.module.fail_json(msg='Could not fetch role %s for client %s in realm %s: %s' - % (name, clientid, realm, str(e))) - - def create_client_role(self, rolerep, clientid, realm='master'): - """ Create a Keycloak client role. - - :param rolerep: a RoleRepresentation of the role to be created. Must contain at minimum the field name. - :param clientid: Client id for the client role - :param realm: Realm in which the role resides - :return: HTTPResponse object on success - """ - cid = self.get_client_id(clientid, realm=realm) - if cid is None: - self.module.fail_json(msg='Could not find client %s in realm %s' - % (clientid, realm)) - roles_url = URL_CLIENT_ROLES.format(url=self.baseurl, realm=realm, id=cid) - try: - if "composites" in rolerep: - keycloak_compatible_composites = self.convert_role_composites(rolerep["composites"]) - rolerep["composites"] = keycloak_compatible_composites - return open_url(roles_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(rolerep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create role %s for client %s in realm %s: %s' - % (rolerep['name'], clientid, realm, str(e))) - - def convert_role_composites(self, composites): - keycloak_compatible_composites = { - 'client': {}, - 'realm': [] - } - for composite in composites: - if 'state' not in composite or composite['state'] == 'present': - if "client_id" in composite and composite["client_id"] is not None: - if composite["client_id"] not in keycloak_compatible_composites["client"]: - keycloak_compatible_composites["client"][composite["client_id"]] = [] - keycloak_compatible_composites["client"][composite["client_id"]].append(composite["name"]) - else: - keycloak_compatible_composites["realm"].append(composite["name"]) - return keycloak_compatible_composites - - def update_client_role(self, rolerep, clientid, realm="master"): - """ Update an existing client role. - - :param rolerep: A RoleRepresentation of the updated role. - :param clientid: Client id for the client role - :param realm: Realm in which the role resides - :return HTTPResponse object on success - """ - cid = self.get_client_id(clientid, realm=realm) - if cid is None: - self.module.fail_json(msg='Could not find client %s in realm %s' - % (clientid, realm)) - role_url = URL_CLIENT_ROLE.format(url=self.baseurl, realm=realm, id=cid, name=quote(rolerep['name'], safe='')) - try: - composites = None - if "composites" in rolerep: - composites = copy.deepcopy(rolerep["composites"]) - del rolerep['composites'] - update_role_response = open_url(role_url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(rolerep), validate_certs=self.validate_certs) - if composites is not None: - self.update_role_composites(rolerep=rolerep, clientid=clientid, composites=composites, realm=realm) - return update_role_response - except Exception as e: - self.fail_open_url(e, msg='Could not update role %s for client %s in realm %s: %s' - % (rolerep['name'], clientid, realm, str(e))) - - def delete_client_role(self, name, clientid, realm="master"): - """ Delete a role. One of name or roleid must be provided. - - :param name: The name of the role. - :param clientid: Client id for the client role - :param realm: Realm in which the role resides - """ - cid = self.get_client_id(clientid, realm=realm) - if cid is None: - self.module.fail_json(msg='Could not find client %s in realm %s' - % (clientid, realm)) - role_url = URL_CLIENT_ROLE.format(url=self.baseurl, realm=realm, id=cid, name=quote(name, safe='')) - try: - return open_url(role_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Unable to delete role %s for client %s in realm %s: %s' - % (name, clientid, realm, str(e))) - - def get_authentication_flow_by_alias(self, alias, realm='master'): - """ - Get an authentication flow by it's alias - :param alias: Alias of the authentication flow to get. - :param realm: Realm. - :return: Authentication flow representation. - """ - try: - authentication_flow = {} - # Check if the authentication flow exists on the Keycloak serveraders - authentications = json.load(open_url(URL_AUTHENTICATION_FLOWS.format(url=self.baseurl, realm=realm), method='GET', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, validate_certs=self.validate_certs)) - for authentication in authentications: - if authentication["alias"] == alias: - authentication_flow = authentication - break - return authentication_flow - except Exception as e: - self.fail_open_url(e, msg="Unable get authentication flow %s: %s" % (alias, str(e))) - - def delete_authentication_flow_by_id(self, id, realm='master'): - """ - Delete an authentication flow from Keycloak - :param id: id of authentication flow to be deleted - :param realm: realm of client to be deleted - :return: HTTPResponse object on success - """ - flow_url = URL_AUTHENTICATION_FLOW.format(url=self.baseurl, realm=realm, id=id) - - try: - return open_url(flow_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not delete authentication flow %s in realm %s: %s' - % (id, realm, str(e))) - - def copy_auth_flow(self, config, realm='master'): - """ - Create a new authentication flow from a copy of another. - :param config: Representation of the authentication flow to create. - :param realm: Realm. - :return: Representation of the new authentication flow. - """ - try: - new_name = dict( - newName=config["alias"] - ) - open_url( - URL_AUTHENTICATION_FLOW_COPY.format( - url=self.baseurl, - realm=realm, - copyfrom=quote(config["copyFrom"], safe='')), - method='POST', - http_agent=self.http_agent, headers=self.restheaders, - data=json.dumps(new_name), - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - flow_list = json.load( - open_url( - URL_AUTHENTICATION_FLOWS.format(url=self.baseurl, - realm=realm), - method='GET', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs)) - for flow in flow_list: - if flow["alias"] == config["alias"]: - return flow - return None - except Exception as e: - self.fail_open_url(e, msg='Could not copy authentication flow %s in realm %s: %s' - % (config["alias"], realm, str(e))) - - def create_empty_auth_flow(self, config, realm='master'): - """ - Create a new empty authentication flow. - :param config: Representation of the authentication flow to create. - :param realm: Realm. - :return: Representation of the new authentication flow. - """ - try: - new_flow = dict( - alias=config["alias"], - providerId=config["providerId"], - description=config["description"], - topLevel=True - ) - open_url( - URL_AUTHENTICATION_FLOWS.format( - url=self.baseurl, - realm=realm), - method='POST', - http_agent=self.http_agent, headers=self.restheaders, - data=json.dumps(new_flow), - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - flow_list = json.load( - open_url( - URL_AUTHENTICATION_FLOWS.format( - url=self.baseurl, - realm=realm), - method='GET', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs)) - for flow in flow_list: - if flow["alias"] == config["alias"]: - return flow - return None - except Exception as e: - self.fail_open_url(e, msg='Could not create empty authentication flow %s in realm %s: %s' - % (config["alias"], realm, str(e))) - - def update_authentication_executions(self, flowAlias, updatedExec, realm='master'): - """ Update authentication executions - - :param flowAlias: name of the parent flow - :param updatedExec: JSON containing updated execution - :return: HTTPResponse object on success - """ - try: - open_url( - URL_AUTHENTICATION_FLOW_EXECUTIONS.format( - url=self.baseurl, - realm=realm, - flowalias=quote(flowAlias, safe='')), - method='PUT', - http_agent=self.http_agent, headers=self.restheaders, - data=json.dumps(updatedExec), - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except HTTPError as e: - self.fail_open_url(e, msg="Unable to update execution '%s': %s: %s %s" - % (flowAlias, repr(e), ";".join([e.url, e.msg, str(e.code), str(e.hdrs)]), str(updatedExec))) - except Exception as e: - self.module.fail_json(msg="Unable to update executions %s: %s" % (updatedExec, str(e))) - - def add_authenticationConfig_to_execution(self, executionId, authenticationConfig, realm='master'): - """ Add autenticatorConfig to the execution - - :param executionId: id of execution - :param authenticationConfig: config to add to the execution - :return: HTTPResponse object on success - """ - try: - open_url( - URL_AUTHENTICATION_EXECUTION_CONFIG.format( - url=self.baseurl, - realm=realm, - id=executionId), - method='POST', - http_agent=self.http_agent, headers=self.restheaders, - data=json.dumps(authenticationConfig), - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg="Unable to add authenticationConfig %s: %s" % (executionId, str(e))) - - def create_subflow(self, subflowName, flowAlias, realm='master', flowType='basic-flow'): - """ Create new sublow on the flow - - :param subflowName: name of the subflow to create - :param flowAlias: name of the parent flow - :return: HTTPResponse object on success - """ - try: - newSubFlow = {} - newSubFlow["alias"] = subflowName - newSubFlow["provider"] = "registration-page-form" - newSubFlow["type"] = flowType - open_url( - URL_AUTHENTICATION_FLOW_EXECUTIONS_FLOW.format( - url=self.baseurl, - realm=realm, - flowalias=quote(flowAlias, safe='')), - method='POST', - http_agent=self.http_agent, headers=self.restheaders, - data=json.dumps(newSubFlow), - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg="Unable to create new subflow %s: %s" % (subflowName, str(e))) - - def create_execution(self, execution, flowAlias, realm='master'): - """ Create new execution on the flow - - :param execution: name of execution to create - :param flowAlias: name of the parent flow - :return: HTTPResponse object on success - """ - try: - newExec = {} - newExec["provider"] = execution["providerId"] - newExec["requirement"] = execution["requirement"] - open_url( - URL_AUTHENTICATION_FLOW_EXECUTIONS_EXECUTION.format( - url=self.baseurl, - realm=realm, - flowalias=quote(flowAlias, safe='')), - method='POST', - http_agent=self.http_agent, headers=self.restheaders, - data=json.dumps(newExec), - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except HTTPError as e: - self.fail_open_url(e, msg="Unable to create new execution '%s' %s: %s: %s %s" - % (flowAlias, execution["providerId"], repr(e), ";".join([e.url, e.msg, str(e.code), str(e.hdrs)]), str(newExec))) - except Exception as e: - self.module.fail_json(msg="Unable to create new execution '%s' %s: %s" % (flowAlias, execution["providerId"], repr(e))) - - def change_execution_priority(self, executionId, diff, realm='master'): - """ Raise or lower execution priority of diff time - - :param executionId: id of execution to lower priority - :param realm: realm the client is in - :param diff: Integer number, raise of diff time if positive lower of diff time if negative - :return: HTTPResponse object on success - """ - try: - if diff > 0: - for i in range(diff): - open_url( - URL_AUTHENTICATION_EXECUTION_RAISE_PRIORITY.format( - url=self.baseurl, - realm=realm, - id=executionId), - method='POST', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - elif diff < 0: - for i in range(-diff): - open_url( - URL_AUTHENTICATION_EXECUTION_LOWER_PRIORITY.format( - url=self.baseurl, - realm=realm, - id=executionId), - method='POST', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg="Unable to change execution priority %s: %s" % (executionId, str(e))) - - def get_executions_representation(self, config, realm='master'): - """ - Get a representation of the executions for an authentication flow. - :param config: Representation of the authentication flow - :param realm: Realm - :return: Representation of the executions - """ - try: - # Get executions created - executions = json.load( - open_url( - URL_AUTHENTICATION_FLOW_EXECUTIONS.format( - url=self.baseurl, - realm=realm, - flowalias=quote(config["alias"], safe='')), - method='GET', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs)) - for execution in executions: - if "authenticationConfig" in execution: - execConfigId = execution["authenticationConfig"] - execConfig = json.load( - open_url( - URL_AUTHENTICATION_CONFIG.format( - url=self.baseurl, - realm=realm, - id=execConfigId), - method='GET', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs)) - execution["authenticationConfig"] = execConfig - return executions - except Exception as e: - self.fail_open_url(e, msg='Could not get executions for authentication flow %s in realm %s: %s' - % (config["alias"], realm, str(e))) - - def get_required_actions(self, realm='master'): - """ - Get required actions. - :param realm: Realm name (not id). - :return: List of representations of the required actions. - """ - - try: - required_actions = json.load( - open_url( - URL_AUTHENTICATION_REQUIRED_ACTIONS.format( - url=self.baseurl, - realm=realm - ), - method='GET', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs - ) - ) - - return required_actions - except Exception: - return None - - def register_required_action(self, rep, realm='master'): - """ - Register required action. - :param rep: JSON containing 'providerId', and 'name' attributes. - :param realm: Realm name (not id). - :return: Representation of the required action. - """ - - data = { - 'name': rep['name'], - 'providerId': rep['providerId'] - } - - try: - return open_url( - URL_AUTHENTICATION_REGISTER_REQUIRED_ACTION.format( - url=self.baseurl, - realm=realm - ), - method='POST', - http_agent=self.http_agent, headers=self.restheaders, - data=json.dumps(data), - timeout=self.connection_timeout, - validate_certs=self.validate_certs - ) - except Exception as e: - self.fail_open_url( - e, - msg='Unable to register required action %s in realm %s: %s' - % (rep["name"], realm, str(e)) - ) - - def update_required_action(self, alias, rep, realm='master'): - """ - Update required action. - :param alias: Alias of required action. - :param rep: JSON describing new state of required action. - :param realm: Realm name (not id). - :return: HTTPResponse object on success. - """ - - try: - return open_url( - URL_AUTHENTICATION_REQUIRED_ACTIONS_ALIAS.format( - url=self.baseurl, - alias=quote(alias, safe=''), - realm=realm - ), - method='PUT', - http_agent=self.http_agent, headers=self.restheaders, - data=json.dumps(rep), - timeout=self.connection_timeout, - validate_certs=self.validate_certs - ) - except Exception as e: - self.fail_open_url( - e, - msg='Unable to update required action %s in realm %s: %s' - % (alias, realm, str(e)) - ) - - def delete_required_action(self, alias, realm='master'): - """ - Delete required action. - :param alias: Alias of required action. - :param realm: Realm name (not id). - :return: HTTPResponse object on success. - """ - - try: - return open_url( - URL_AUTHENTICATION_REQUIRED_ACTIONS_ALIAS.format( - url=self.baseurl, - alias=quote(alias, safe=''), - realm=realm - ), - method='DELETE', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs - ) - except Exception as e: - self.fail_open_url( - e, - msg='Unable to delete required action %s in realm %s: %s' - % (alias, realm, str(e)) - ) - - def get_identity_providers(self, realm='master'): - """ Fetch representations for identity providers in a realm - :param realm: realm to be queried - :return: list of representations for identity providers - """ - idps_url = URL_IDENTITY_PROVIDERS.format(url=self.baseurl, realm=realm) - try: - return json.loads(to_native(open_url(idps_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of identity providers for realm %s: %s' - % (realm, str(e))) - except Exception as e: - self.fail_open_url(e, msg='Could not obtain list of identity providers for realm %s: %s' - % (realm, str(e))) - - def get_identity_provider(self, alias, realm='master'): - """ Fetch identity provider representation from a realm using the idp's alias. - If the identity provider does not exist, None is returned. - :param alias: Alias of the identity provider to fetch. - :param realm: Realm in which the identity provider resides; default 'master'. - """ - idp_url = URL_IDENTITY_PROVIDER.format(url=self.baseurl, realm=realm, alias=alias) - try: - return json.loads(to_native(open_url(idp_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not fetch identity provider %s in realm %s: %s' - % (alias, realm, str(e))) - except Exception as e: - self.module.fail_json(msg='Could not fetch identity provider %s in realm %s: %s' - % (alias, realm, str(e))) - - def create_identity_provider(self, idprep, realm='master'): - """ Create an identity provider. - :param idprep: Identity provider representation of the idp to be created. - :param realm: Realm in which this identity provider resides, default "master". - :return: HTTPResponse object on success - """ - idps_url = URL_IDENTITY_PROVIDERS.format(url=self.baseurl, realm=realm) - try: - return open_url(idps_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(idprep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create identity provider %s in realm %s: %s' - % (idprep['alias'], realm, str(e))) - - def update_identity_provider(self, idprep, realm='master'): - """ Update an existing identity provider. - :param idprep: Identity provider representation of the idp to be updated. - :param realm: Realm in which this identity provider resides, default "master". - :return HTTPResponse object on success - """ - idp_url = URL_IDENTITY_PROVIDER.format(url=self.baseurl, realm=realm, alias=idprep['alias']) - try: - return open_url(idp_url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(idprep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not update identity provider %s in realm %s: %s' - % (idprep['alias'], realm, str(e))) - - def delete_identity_provider(self, alias, realm='master'): - """ Delete an identity provider. - :param alias: Alias of the identity provider. - :param realm: Realm in which this identity provider resides, default "master". - """ - idp_url = URL_IDENTITY_PROVIDER.format(url=self.baseurl, realm=realm, alias=alias) - try: - return open_url(idp_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Unable to delete identity provider %s in realm %s: %s' - % (alias, realm, str(e))) - - def get_identity_provider_mappers(self, alias, realm='master'): - """ Fetch representations for identity provider mappers - :param alias: Alias of the identity provider. - :param realm: realm to be queried - :return: list of representations for identity provider mappers - """ - mappers_url = URL_IDENTITY_PROVIDER_MAPPERS.format(url=self.baseurl, realm=realm, alias=alias) - try: - return json.loads(to_native(open_url(mappers_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of identity provider mappers for idp %s in realm %s: %s' - % (alias, realm, str(e))) - except Exception as e: - self.fail_open_url(e, msg='Could not obtain list of identity provider mappers for idp %s in realm %s: %s' - % (alias, realm, str(e))) - - def get_identity_provider_mapper(self, mid, alias, realm='master'): - """ Fetch identity provider representation from a realm using the idp's alias. - If the identity provider does not exist, None is returned. - :param mid: Unique ID of the mapper to fetch. - :param alias: Alias of the identity provider. - :param realm: Realm in which the identity provider resides; default 'master'. - """ - mapper_url = URL_IDENTITY_PROVIDER_MAPPER.format(url=self.baseurl, realm=realm, alias=alias, id=mid) - try: - return json.loads(to_native(open_url(mapper_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not fetch mapper %s for identity provider %s in realm %s: %s' - % (mid, alias, realm, str(e))) - except Exception as e: - self.module.fail_json(msg='Could not fetch mapper %s for identity provider %s in realm %s: %s' - % (mid, alias, realm, str(e))) - - def create_identity_provider_mapper(self, mapper, alias, realm='master'): - """ Create an identity provider mapper. - :param mapper: IdentityProviderMapperRepresentation of the mapper to be created. - :param alias: Alias of the identity provider. - :param realm: Realm in which this identity provider resides, default "master". - :return: HTTPResponse object on success - """ - mappers_url = URL_IDENTITY_PROVIDER_MAPPERS.format(url=self.baseurl, realm=realm, alias=alias) - try: - return open_url(mappers_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(mapper), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create identity provider mapper %s for idp %s in realm %s: %s' - % (mapper['name'], alias, realm, str(e))) - - def update_identity_provider_mapper(self, mapper, alias, realm='master'): - """ Update an existing identity provider. - :param mapper: IdentityProviderMapperRepresentation of the mapper to be updated. - :param alias: Alias of the identity provider. - :param realm: Realm in which this identity provider resides, default "master". - :return HTTPResponse object on success - """ - mapper_url = URL_IDENTITY_PROVIDER_MAPPER.format(url=self.baseurl, realm=realm, alias=alias, id=mapper['id']) - try: - return open_url(mapper_url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(mapper), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not update mapper %s for identity provider %s in realm %s: %s' - % (mapper['id'], alias, realm, str(e))) - - def delete_identity_provider_mapper(self, mid, alias, realm='master'): - """ Delete an identity provider. - :param mid: Unique ID of the mapper to delete. - :param alias: Alias of the identity provider. - :param realm: Realm in which this identity provider resides, default "master". - """ - mapper_url = URL_IDENTITY_PROVIDER_MAPPER.format(url=self.baseurl, realm=realm, alias=alias, id=mid) - try: - return open_url(mapper_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Unable to delete mapper %s for identity provider %s in realm %s: %s' - % (mid, alias, realm, str(e))) - - def get_components(self, filter=None, realm='master'): - """ Fetch representations for components in a realm - :param realm: realm to be queried - :param filter: search filter - :return: list of representations for components - """ - comps_url = URL_COMPONENTS.format(url=self.baseurl, realm=realm) - if filter is not None: - comps_url += '?%s' % filter - - try: - return json.loads(to_native(open_url(comps_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except ValueError as e: - self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of components for realm %s: %s' - % (realm, str(e))) - except Exception as e: - self.fail_open_url(e, msg='Could not obtain list of components for realm %s: %s' - % (realm, str(e))) - - def get_component(self, cid, realm='master'): - """ Fetch component representation from a realm using its cid. - If the component does not exist, None is returned. - :param cid: Unique ID of the component to fetch. - :param realm: Realm in which the component resides; default 'master'. - """ - comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid) - try: - return json.loads(to_native(open_url(comp_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except HTTPError as e: - if e.code == 404: - return None - else: - self.fail_open_url(e, msg='Could not fetch component %s in realm %s: %s' - % (cid, realm, str(e))) - except Exception as e: - self.module.fail_json(msg='Could not fetch component %s in realm %s: %s' - % (cid, realm, str(e))) - - def create_component(self, comprep, realm='master'): - """ Create an component. - :param comprep: Component representation of the component to be created. - :param realm: Realm in which this component resides, default "master". - :return: Component representation of the created component - """ - comps_url = URL_COMPONENTS.format(url=self.baseurl, realm=realm) - try: - resp = open_url(comps_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(comprep), validate_certs=self.validate_certs) - comp_url = resp.getheader('Location') - if comp_url is None: - self.module.fail_json(msg='Could not create component in realm %s: %s' - % (realm, 'unexpected response')) - return json.loads(to_native(open_url(comp_url, method="GET", http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg='Could not create component in realm %s: %s' - % (realm, str(e))) - - def update_component(self, comprep, realm='master'): - """ Update an existing component. - :param comprep: Component representation of the component to be updated. - :param realm: Realm in which this component resides, default "master". - :return HTTPResponse object on success - """ - cid = comprep.get('id') - if cid is None: - self.module.fail_json(msg='Cannot update component without id') - comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid) - try: - return open_url(comp_url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(comprep), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not update component %s in realm %s: %s' - % (cid, realm, str(e))) - - def delete_component(self, cid, realm='master'): - """ Delete an component. - :param cid: Unique ID of the component. - :param realm: Realm in which this component resides, default "master". - """ - comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid) - try: - return open_url(comp_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Unable to delete component %s in realm %s: %s' - % (cid, realm, str(e))) - - def get_authz_authorization_scope_by_name(self, name, client_id, realm): - url = URL_AUTHZ_AUTHORIZATION_SCOPES.format(url=self.baseurl, client_id=client_id, realm=realm) - search_url = "%s/search?name=%s" % (url, quote(name, safe='')) - - try: - return json.loads(to_native(open_url(search_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception: - return False - - def create_authz_authorization_scope(self, payload, client_id, realm): - """Create an authorization scope for a Keycloak client""" - url = URL_AUTHZ_AUTHORIZATION_SCOPES.format(url=self.baseurl, client_id=client_id, realm=realm) - - try: - return open_url(url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(payload), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create authorization scope %s for client %s in realm %s: %s' % (payload['name'], client_id, realm, str(e))) - - def update_authz_authorization_scope(self, payload, id, client_id, realm): - """Update an authorization scope for a Keycloak client""" - url = URL_AUTHZ_AUTHORIZATION_SCOPE.format(url=self.baseurl, id=id, client_id=client_id, realm=realm) - - try: - return open_url(url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(payload), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create update scope %s for client %s in realm %s: %s' % (payload['name'], client_id, realm, str(e))) - - def remove_authz_authorization_scope(self, id, client_id, realm): - """Remove an authorization scope from a Keycloak client""" - url = URL_AUTHZ_AUTHORIZATION_SCOPE.format(url=self.baseurl, id=id, client_id=client_id, realm=realm) - - try: - return open_url(url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not delete scope %s for client %s in realm %s: %s' % (id, client_id, realm, str(e))) - - def get_user_by_id(self, user_id, realm='master'): - """ - Get a User by its ID. - :param user_id: ID of the user. - :param realm: Realm - :return: Representation of the user. - """ - try: - user_url = URL_USER.format( - url=self.baseurl, - realm=realm, - id=user_id) - userrep = json.load( - open_url( - user_url, - method='GET', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs)) - return userrep - except Exception as e: - self.fail_open_url(e, msg='Could not get user %s in realm %s: %s' - % (user_id, realm, str(e))) - - def create_user(self, userrep, realm='master'): - """ - Create a new User. - :param userrep: Representation of the user to create - :param realm: Realm - :return: Representation of the user created. - """ - try: - if 'attributes' in userrep and isinstance(userrep['attributes'], list): - attributes = copy.deepcopy(userrep['attributes']) - userrep['attributes'] = self.convert_user_attributes_to_keycloak_dict(attributes=attributes) - users_url = URL_USERS.format( - url=self.baseurl, - realm=realm) - open_url(users_url, - method='POST', - http_agent=self.http_agent, headers=self.restheaders, - data=json.dumps(userrep), - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - created_user = self.get_user_by_username( - username=userrep['username'], - realm=realm) - return created_user - except Exception as e: - self.fail_open_url(e, msg='Could not create user %s in realm %s: %s' - % (userrep['username'], realm, str(e))) - - def convert_user_attributes_to_keycloak_dict(self, attributes): - keycloak_user_attributes_dict = {} - for attribute in attributes: - if ('state' not in attribute or attribute['state'] == 'present') and 'name' in attribute: - keycloak_user_attributes_dict[attribute['name']] = attribute['values'] if 'values' in attribute else [] - return keycloak_user_attributes_dict - - def convert_keycloak_user_attributes_dict_to_module_list(self, attributes): - module_attributes_list = [] - for key in attributes: - attr = {} - attr['name'] = key - attr['values'] = attributes[key] - module_attributes_list.append(attr) - return module_attributes_list - - def update_user(self, userrep, realm='master'): - """ - Update a User. - :param userrep: Representation of the user to update. This representation must include the ID of the user. - :param realm: Realm - :return: Representation of the updated user. - """ - try: - if 'attributes' in userrep and isinstance(userrep['attributes'], list): - attributes = copy.deepcopy(userrep['attributes']) - userrep['attributes'] = self.convert_user_attributes_to_keycloak_dict(attributes=attributes) - user_url = URL_USER.format( - url=self.baseurl, - realm=realm, - id=userrep["id"]) - open_url( - user_url, - method='PUT', - http_agent=self.http_agent, headers=self.restheaders, - data=json.dumps(userrep), - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - updated_user = self.get_user_by_id( - user_id=userrep['id'], - realm=realm) - return updated_user - except Exception as e: - self.fail_open_url(e, msg='Could not update user %s in realm %s: %s' - % (userrep['username'], realm, str(e))) - - def delete_user(self, user_id, realm='master'): - """ - Delete a User. - :param user_id: ID of the user to be deleted - :param realm: Realm - :return: HTTP response. - """ - try: - user_url = URL_USER.format( - url=self.baseurl, - realm=realm, - id=user_id) - return open_url( - user_url, - method='DELETE', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not delete user %s in realm %s: %s' - % (user_id, realm, str(e))) - - def get_user_groups(self, user_id, realm='master'): - """ - Get groups for a user. - :param user_id: User ID - :param realm: Realm - :return: Representation of the client groups. - """ - try: - groups = [] - user_groups_url = URL_USER_GROUPS.format( - url=self.baseurl, - realm=realm, - id=user_id) - user_groups = json.load( - open_url( - user_groups_url, - method='GET', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs)) - for user_group in user_groups: - groups.append(user_group["name"]) - return groups - except Exception as e: - self.fail_open_url(e, msg='Could not get groups for user %s in realm %s: %s' - % (user_id, realm, str(e))) - - def add_user_in_group(self, user_id, group_id, realm='master'): - """ - Add a user to a group. - :param user_id: User ID - :param group_id: Group Id to add the user to. - :param realm: Realm - :return: HTTP Response - """ - try: - user_group_url = URL_USER_GROUP.format( - url=self.baseurl, - realm=realm, - id=user_id, - group_id=group_id) - return open_url( - user_group_url, - method='PUT', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not add user %s in group %s in realm %s: %s' - % (user_id, group_id, realm, str(e))) - - def remove_user_from_group(self, user_id, group_id, realm='master'): - """ - Remove a user from a group for a user. - :param user_id: User ID - :param group_id: Group Id to add the user to. - :param realm: Realm - :return: HTTP response - """ - try: - user_group_url = URL_USER_GROUP.format( - url=self.baseurl, - realm=realm, - id=user_id, - group_id=group_id) - return open_url( - user_group_url, - method='DELETE', - http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not remove user %s from group %s in realm %s: %s' - % (user_id, group_id, realm, str(e))) - - def update_user_groups_membership(self, userrep, groups, realm='master'): - """ - Update user's group membership - :param userrep: Representation of the user. This representation must include the ID. - :param realm: Realm - :return: True if group membership has been changed. False Otherwise. - """ - changed = False - try: - user_existing_groups = self.get_user_groups( - user_id=userrep['id'], - realm=realm) - groups_to_add_and_remove = self.extract_groups_to_add_to_and_remove_from_user(groups) - # If group membership need to be changed - if not is_struct_included(groups_to_add_and_remove['add'], user_existing_groups): - # Get available groups in the realm - realm_groups = self.get_groups(realm=realm) - for realm_group in realm_groups: - if "name" in realm_group and realm_group["name"] in groups_to_add_and_remove['add']: - self.add_user_in_group( - user_id=userrep["id"], - group_id=realm_group["id"], - realm=realm) - changed = True - elif "name" in realm_group and realm_group['name'] in groups_to_add_and_remove['remove']: - self.remove_user_from_group( - user_id=userrep['id'], - group_id=realm_group['id'], - realm=realm) - changed = True - return changed - except Exception as e: - self.module.fail_json(msg='Could not update group membership for user %s in realm %s: %s' - % (userrep['id]'], realm, str(e))) - - def extract_groups_to_add_to_and_remove_from_user(self, groups): - groups_extract = {} - groups_to_add = [] - groups_to_remove = [] - if isinstance(groups, list) and len(groups) > 0: - for group in groups: - group_name = group['name'] if isinstance(group, dict) and 'name' in group else group - if isinstance(group, dict) and ('state' not in group or group['state'] == 'present'): - groups_to_add.append(group_name) - else: - groups_to_remove.append(group_name) - groups_extract['add'] = groups_to_add - groups_extract['remove'] = groups_to_remove - - return groups_extract - - def convert_user_group_list_of_str_to_list_of_dict(self, groups): - list_of_groups = [] - if isinstance(groups, list) and len(groups) > 0: - for group in groups: - if isinstance(group, str): - group_dict = {} - group_dict['name'] = group - list_of_groups.append(group_dict) - return list_of_groups - - def create_authz_custom_policy(self, policy_type, payload, client_id, realm): - """Create a custom policy for a Keycloak client""" - url = URL_AUTHZ_CUSTOM_POLICY.format(url=self.baseurl, policy_type=policy_type, client_id=client_id, realm=realm) - - try: - return open_url(url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(payload), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create permission %s for client %s in realm %s: %s' % (payload['name'], client_id, realm, str(e))) - - def remove_authz_custom_policy(self, policy_id, client_id, realm): - """Remove a custom policy from a Keycloak client""" - url = URL_AUTHZ_CUSTOM_POLICIES.format(url=self.baseurl, client_id=client_id, realm=realm) - delete_url = "%s/%s" % (url, policy_id) - - try: - return open_url(delete_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not delete custom policy %s for client %s in realm %s: %s' % (id, client_id, realm, str(e))) - - def get_authz_permission_by_name(self, name, client_id, realm): - """Get authorization permission by name""" - url = URL_AUTHZ_POLICIES.format(url=self.baseurl, client_id=client_id, realm=realm) - search_url = "%s/search?name=%s" % (url, name.replace(' ', '%20')) - - try: - return json.loads(to_native(open_url(search_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception: - return False - - def create_authz_permission(self, payload, permission_type, client_id, realm): - """Create an authorization permission for a Keycloak client""" - url = URL_AUTHZ_PERMISSIONS.format(url=self.baseurl, permission_type=permission_type, client_id=client_id, realm=realm) - - try: - return open_url(url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(payload), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create permission %s for client %s in realm %s: %s' % (payload['name'], client_id, realm, str(e))) - - def remove_authz_permission(self, id, client_id, realm): - """Create an authorization permission for a Keycloak client""" - url = URL_AUTHZ_POLICY.format(url=self.baseurl, id=id, client_id=client_id, realm=realm) - - try: - return open_url(url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not delete permission %s for client %s in realm %s: %s' % (id, client_id, realm, str(e))) - - def update_authz_permission(self, payload, permission_type, id, client_id, realm): - """Update a permission for a Keycloak client""" - url = URL_AUTHZ_PERMISSION.format(url=self.baseurl, permission_type=permission_type, id=id, client_id=client_id, realm=realm) - - try: - return open_url(url, method='PUT', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(payload), validate_certs=self.validate_certs) - except Exception as e: - self.fail_open_url(e, msg='Could not create update permission %s for client %s in realm %s: %s' % (payload['name'], client_id, realm, str(e))) - - def get_authz_resource_by_name(self, name, client_id, realm): - """Get authorization resource by name""" - url = URL_AUTHZ_RESOURCES.format(url=self.baseurl, client_id=client_id, realm=realm) - search_url = "%s/search?name=%s" % (url, name.replace(' ', '%20')) - - try: - return json.loads(to_native(open_url(search_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception: - return False - - def get_authz_policy_by_name(self, name, client_id, realm): - """Get authorization policy by name""" - url = URL_AUTHZ_POLICIES.format(url=self.baseurl, client_id=client_id, realm=realm) - search_url = "%s/search?name=%s&permission=false" % (url, name.replace(' ', '%20')) - - try: - return json.loads(to_native(open_url(search_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception: - return False - - def get_client_role_scope_from_client(self, clientid, clientscopeid, realm="master"): - """ Fetch the roles associated with the client's scope for a specific client on the Keycloak server. - :param clientid: ID of the client from which to obtain the associated roles. - :param clientscopeid: ID of the client who owns the roles. - :param realm: Realm from which to obtain the scope. - :return: The client scope of roles from specified client. - """ - client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format(url=self.baseurl, realm=realm, id=clientid, scopeid=clientscopeid) - try: - return json.loads(to_native(open_url(client_role_scope_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg='Could not fetch roles scope for client %s in realm %s: %s' % (clientid, realm, str(e))) - - def update_client_role_scope_from_client(self, payload, clientid, clientscopeid, realm="master"): - """ Update and fetch the roles associated with the client's scope on the Keycloak server. - :param payload: List of roles to be added to the scope. - :param clientid: ID of the client to update scope. - :param clientscopeid: ID of the client who owns the roles. - :param realm: Realm from which to obtain the clients. - :return: The client scope of roles from specified client. - """ - client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format(url=self.baseurl, realm=realm, id=clientid, scopeid=clientscopeid) - try: - open_url(client_role_scope_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(payload), validate_certs=self.validate_certs) - - except Exception as e: - self.fail_open_url(e, msg='Could not update roles scope for client %s in realm %s: %s' % (clientid, realm, str(e))) - - return self.get_client_role_scope_from_client(clientid, clientscopeid, realm) - - def delete_client_role_scope_from_client(self, payload, clientid, clientscopeid, realm="master"): - """ Delete the roles contains in the payload from the client's scope on the Keycloak server. - :param payload: List of roles to be deleted. - :param clientid: ID of the client to delete roles from scope. - :param clientscopeid: ID of the client who owns the roles. - :param realm: Realm from which to obtain the clients. - :return: The client scope of roles from specified client. - """ - client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format(url=self.baseurl, realm=realm, id=clientid, scopeid=clientscopeid) - try: - open_url(client_role_scope_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(payload), validate_certs=self.validate_certs) - - except Exception as e: - self.fail_open_url(e, msg='Could not delete roles scope for client %s in realm %s: %s' % (clientid, realm, str(e))) - - return self.get_client_role_scope_from_client(clientid, clientscopeid, realm) - - def get_client_role_scope_from_realm(self, clientid, realm="master"): - """ Fetch the realm roles from the client's scope on the Keycloak server. - :param clientid: ID of the client from which to obtain the associated realm roles. - :param realm: Realm from which to obtain the clients. - :return: The client realm roles scope. - """ - client_role_scope_url = URL_CLIENT_ROLE_SCOPE_REALM.format(url=self.baseurl, realm=realm, id=clientid) - try: - return json.loads(to_native(open_url(client_role_scope_url, method='GET', http_agent=self.http_agent, headers=self.restheaders, - timeout=self.connection_timeout, - validate_certs=self.validate_certs).read())) - except Exception as e: - self.fail_open_url(e, msg='Could not fetch roles scope for client %s in realm %s: %s' % (clientid, realm, str(e))) - - def update_client_role_scope_from_realm(self, payload, clientid, realm="master"): - """ Update and fetch the realm roles from the client's scope on the Keycloak server. - :param payload: List of realm roles to add. - :param clientid: ID of the client to update scope. - :param realm: Realm from which to obtain the clients. - :return: The client realm roles scope. - """ - client_role_scope_url = URL_CLIENT_ROLE_SCOPE_REALM.format(url=self.baseurl, realm=realm, id=clientid) - try: - open_url(client_role_scope_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(payload), validate_certs=self.validate_certs) - - except Exception as e: - self.fail_open_url(e, msg='Could not update roles scope for client %s in realm %s: %s' % (clientid, realm, str(e))) - - return self.get_client_role_scope_from_realm(clientid, realm) - - def delete_client_role_scope_from_realm(self, payload, clientid, realm="master"): - """ Delete the realm roles contains in the payload from the client's scope on the Keycloak server. - :param payload: List of realm roles to delete. - :param clientid: ID of the client to delete roles from scope. - :param realm: Realm from which to obtain the clients. - :return: The client realm roles scope. - """ - client_role_scope_url = URL_CLIENT_ROLE_SCOPE_REALM.format(url=self.baseurl, realm=realm, id=clientid) - try: - open_url(client_role_scope_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout, - data=json.dumps(payload), validate_certs=self.validate_certs) - - except Exception as e: - self.fail_open_url(e, msg='Could not delete roles scope for client %s in realm %s: %s' % (clientid, realm, str(e))) - - return self.get_client_role_scope_from_realm(clientid, realm) - - def fail_open_url(self, e, msg, **kwargs): - try: - if isinstance(e, HTTPError): - msg = "%s: %s" % (msg, to_native(e.read())) - except Exception as ingore: - pass - self.module.fail_json(msg, **kwargs) diff --git a/plugins/modules/keycloak_client.py b/plugins/modules/keycloak_client.py deleted file mode 100644 index 0afa52b..0000000 --- a/plugins/modules/keycloak_client.py +++ /dev/null @@ -1,1137 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright (c) 2017, Eike Frost -# 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 -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: keycloak_client - -short_description: Allows administration of Keycloak clients via Keycloak API - - -description: - - This module allows the administration of Keycloak clients 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. - - - 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/8.0/rest-api/index.html). - Aliases are provided so camelCased versions can be used as well. - - - The Keycloak API does not always sanity check inputs e.g. you can set - SAML-specific settings on an OpenID Connect client for instance and vice versa. Be careful. - If you do not specify a setting, usually a sensible default is chosen. - -attributes: - check_mode: - support: full - diff_mode: - support: full - -options: - state: - description: - - State of the client - - On V(present), the client will be created (or updated if it exists already). - - On V(absent), the client will be removed if it exists - choices: ['present', 'absent'] - default: 'present' - type: str - - realm: - description: - - The realm to create the client in. - type: str - default: master - - client_id: - description: - - Client id of client to be worked on. This is usually an alphanumeric name chosen by - you. Either this or O(id) is required. If you specify both, O(id) takes precedence. - This is 'clientId' in the Keycloak REST API. - aliases: - - clientId - type: str - - id: - description: - - Id of client to be worked on. This is usually an UUID. Either this or O(client_id) - is required. If you specify both, this takes precedence. - type: str - - name: - description: - - Name of the client (this is not the same as O(client_id)). - type: str - - description: - description: - - Description of the client in Keycloak. - type: str - - root_url: - description: - - Root URL appended to relative URLs for this client. - This is 'rootUrl' in the Keycloak REST API. - aliases: - - rootUrl - type: str - - admin_url: - description: - - URL to the admin interface of the client. - This is 'adminUrl' in the Keycloak REST API. - aliases: - - adminUrl - type: str - - base_url: - description: - - Default URL to use when the auth server needs to redirect or link back to the client - This is 'baseUrl' in the Keycloak REST API. - aliases: - - baseUrl - type: str - - enabled: - description: - - Is this client enabled or not? - type: bool - - client_authenticator_type: - description: - - How do clients authenticate with the auth server? Either V(client-secret), - V(client-jwt), or V(client-x509) can be chosen. When using V(client-secret), the module parameter - O(secret) can set it, for V(client-jwt), you can use the keys C(use.jwks.url), - C(jwks.url), and C(jwt.credential.certificate) in the O(attributes) module parameter - to configure its behavior. For V(client-x509) you can use the keys C(x509.allow.regex.pattern.comparison) - and C(x509.subjectdn) in the O(attributes) module parameter to configure which certificate(s) to accept. - - This is 'clientAuthenticatorType' in the Keycloak REST API. - choices: ['client-secret', 'client-jwt', 'client-x509'] - aliases: - - clientAuthenticatorType - type: str - - secret: - description: - - When using O(client_authenticator_type=client-secret) (the default), you can - specify a secret here (otherwise one will be generated if it does not exit). If - changing this secret, the module will not register a change currently (but the - changed secret will be saved). - type: str - - registration_access_token: - description: - - The registration access token provides access for clients to the client registration - service. - This is 'registrationAccessToken' in the Keycloak REST API. - aliases: - - registrationAccessToken - type: str - - default_roles: - description: - - list of default roles for this client. If the client roles referenced do not exist - yet, they will be created. - This is 'defaultRoles' in the Keycloak REST API. - aliases: - - defaultRoles - type: list - elements: str - - redirect_uris: - description: - - Acceptable redirect URIs for this client. - This is 'redirectUris' in the Keycloak REST API. - aliases: - - redirectUris - type: list - elements: str - - web_origins: - description: - - List of allowed CORS origins. - This is 'webOrigins' in the Keycloak REST API. - aliases: - - webOrigins - type: list - elements: str - - not_before: - description: - - Revoke any tokens issued before this date for this client (this is a UNIX timestamp). - This is 'notBefore' in the Keycloak REST API. - type: int - aliases: - - notBefore - - bearer_only: - description: - - The access type of this client is bearer-only. - This is 'bearerOnly' in the Keycloak REST API. - aliases: - - bearerOnly - type: bool - - consent_required: - description: - - If enabled, users have to consent to client access. - This is 'consentRequired' in the Keycloak REST API. - aliases: - - consentRequired - type: bool - - standard_flow_enabled: - description: - - Enable standard flow for this client or not (OpenID connect). - This is 'standardFlowEnabled' in the Keycloak REST API. - aliases: - - standardFlowEnabled - type: bool - - implicit_flow_enabled: - description: - - Enable implicit flow for this client or not (OpenID connect). - This is 'implicitFlowEnabled' in the Keycloak REST API. - aliases: - - implicitFlowEnabled - type: bool - - direct_access_grants_enabled: - description: - - Are direct access grants enabled for this client or not (OpenID connect). - This is 'directAccessGrantsEnabled' in the Keycloak REST API. - aliases: - - directAccessGrantsEnabled - type: bool - - service_accounts_enabled: - description: - - Are service accounts enabled for this client or not (OpenID connect). - This is 'serviceAccountsEnabled' in the Keycloak REST API. - aliases: - - serviceAccountsEnabled - type: bool - - authorization_services_enabled: - description: - - Are authorization services enabled for this client or not (OpenID connect). - This is 'authorizationServicesEnabled' in the Keycloak REST API. - aliases: - - authorizationServicesEnabled - type: bool - - public_client: - description: - - Is the access type for this client public or not. - This is 'publicClient' in the Keycloak REST API. - aliases: - - publicClient - type: bool - - frontchannel_logout: - description: - - Is frontchannel logout enabled for this client or not. - This is 'frontchannelLogout' in the Keycloak REST API. - aliases: - - frontchannelLogout - type: bool - - protocol: - description: - - Type of client. - - At creation only, default value will be V(openid-connect) if O(protocol) is omitted. - - The V(docker-v2) value was added in community.general 8.6.0. - type: str - choices: ['openid-connect', 'saml', 'docker-v2'] - - full_scope_allowed: - description: - - Is the "Full Scope Allowed" feature set for this client or not. - This is 'fullScopeAllowed' in the Keycloak REST API. - aliases: - - fullScopeAllowed - type: bool - - node_re_registration_timeout: - description: - - Cluster node re-registration timeout for this client. - This is 'nodeReRegistrationTimeout' in the Keycloak REST API. - type: int - aliases: - - nodeReRegistrationTimeout - - registered_nodes: - description: - - dict of registered cluster nodes (with C(nodename) as the key and last registration - time as the value). - This is 'registeredNodes' in the Keycloak REST API. - type: dict - aliases: - - registeredNodes - - client_template: - description: - - Client template to use for this client. If it does not exist this field will silently - be dropped. - This is 'clientTemplate' in the Keycloak REST API. - type: str - aliases: - - clientTemplate - - use_template_config: - description: - - Whether or not to use configuration from the O(client_template). - This is 'useTemplateConfig' in the Keycloak REST API. - aliases: - - useTemplateConfig - type: bool - - use_template_scope: - description: - - Whether or not to use scope configuration from the O(client_template). - This is 'useTemplateScope' in the Keycloak REST API. - aliases: - - useTemplateScope - type: bool - - use_template_mappers: - description: - - Whether or not to use mapper configuration from the O(client_template). - This is 'useTemplateMappers' in the Keycloak REST API. - aliases: - - useTemplateMappers - type: bool - - always_display_in_console: - description: - - Whether or not to display this client in account console, even if the - user does not have an active session. - aliases: - - alwaysDisplayInConsole - type: bool - version_added: 4.7.0 - - surrogate_auth_required: - description: - - Whether or not surrogate auth is required. - This is 'surrogateAuthRequired' in the Keycloak REST API. - aliases: - - surrogateAuthRequired - type: bool - - authorization_settings: - description: - - a data structure defining the authorization settings for this client. For reference, - please see the Keycloak API docs at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_resourceserverrepresentation). - This is 'authorizationSettings' in the Keycloak REST API. - type: dict - aliases: - - authorizationSettings - - authentication_flow_binding_overrides: - description: - - Override realm authentication flow bindings. - type: dict - suboptions: - browser: - description: - - Flow ID of the browser authentication flow. - - O(authentication_flow_binding_overrides.browser) - and O(authentication_flow_binding_overrides.browser_name) are mutually exclusive. - type: str - - browser_name: - description: - - Flow name of the browser authentication flow. - - O(authentication_flow_binding_overrides.browser) - and O(authentication_flow_binding_overrides.browser_name) are mutually exclusive. - aliases: - - browserName - type: str - version_added: 9.1.0 - - direct_grant: - description: - - Flow ID of the direct grant authentication flow. - - O(authentication_flow_binding_overrides.direct_grant) - and O(authentication_flow_binding_overrides.direct_grant_name) are mutually exclusive. - aliases: - - directGrant - type: str - - direct_grant_name: - description: - - Flow name of the direct grant authentication flow. - - O(authentication_flow_binding_overrides.direct_grant) - and O(authentication_flow_binding_overrides.direct_grant_name) are mutually exclusive. - aliases: - - directGrantName - type: str - version_added: 9.1.0 - aliases: - - authenticationFlowBindingOverrides - version_added: 3.4.0 - - default_client_scopes: - description: - - List of default client scopes. - aliases: - - defaultClientScopes - type: list - elements: str - version_added: 4.7.0 - - optional_client_scopes: - description: - - List of optional client scopes. - aliases: - - optionalClientScopes - type: list - elements: str - version_added: 4.7.0 - - protocol_mappers: - description: - - a list of dicts defining protocol mappers for this client. - This is 'protocolMappers' in the Keycloak REST API. - aliases: - - protocolMappers - 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. This can contain various configuration - settings; an example is given in the examples section. While an exhaustive list of - permissible options is not available; possible options as of Keycloak 3.4 are listed below. The Keycloak - API does not validate whether a given option is appropriate for the protocol used; if specified - anyway, Keycloak will simply not use it. - type: dict - suboptions: - saml.authnstatement: - description: - - For SAML clients, boolean specifying whether or not a statement containing method and timestamp - should be included in the login response. - - saml.client.signature: - description: - - For SAML clients, boolean specifying whether a client signature is required and validated. - - saml.encrypt: - description: - - Boolean specifying whether SAML assertions should be encrypted with the client's public key. - - saml.force.post.binding: - description: - - For SAML clients, boolean specifying whether always to use POST binding for responses. - - saml.onetimeuse.condition: - description: - - For SAML clients, boolean specifying whether a OneTimeUse condition should be included in login responses. - - saml.server.signature: - description: - - Boolean specifying whether SAML documents should be signed by the realm. - - saml.server.signature.keyinfo.ext: - description: - - For SAML clients, boolean specifying whether REDIRECT signing key lookup should be optimized through inclusion - of the signing key id in the SAML Extensions element. - - saml.signature.algorithm: - description: - - Signature algorithm used to sign SAML documents. One of V(RSA_SHA256), V(RSA_SHA1), V(RSA_SHA512), or V(DSA_SHA1). - - saml.signing.certificate: - description: - - SAML signing key certificate, base64-encoded. - - saml.signing.private.key: - description: - - SAML signing key private key, base64-encoded. - - saml_assertion_consumer_url_post: - description: - - SAML POST Binding URL for the client's assertion consumer service (login responses). - - saml_assertion_consumer_url_redirect: - description: - - SAML Redirect Binding URL for the client's assertion consumer service (login responses). - - saml_force_name_id_format: - description: - - For SAML clients, Boolean specifying whether to ignore requested NameID subject format and using the configured one instead. - - saml_name_id_format: - description: - - For SAML clients, the NameID format to use (one of V(username), V(email), V(transient), or V(persistent)) - - saml_signature_canonicalization_method: - description: - - SAML signature canonicalization method. This is one of four values, namely - V(http://www.w3.org/2001/10/xml-exc-c14n#) for EXCLUSIVE, - V(http://www.w3.org/2001/10/xml-exc-c14n#WithComments) for EXCLUSIVE_WITH_COMMENTS, - V(http://www.w3.org/TR/2001/REC-xml-c14n-20010315) for INCLUSIVE, and - V(http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments) for INCLUSIVE_WITH_COMMENTS. - - saml_single_logout_service_url_post: - description: - - SAML POST binding url for the client's single logout service. - - saml_single_logout_service_url_redirect: - description: - - SAML redirect binding url for the client's single logout service. - - user.info.response.signature.alg: - description: - - For OpenID-Connect clients, JWA algorithm for signed UserInfo-endpoint responses. One of V(RS256) or V(unsigned). - - request.object.signature.alg: - description: - - For OpenID-Connect clients, JWA algorithm which the client needs to use when sending - OIDC request object. One of V(any), V(none), V(RS256). - - use.jwks.url: - description: - - For OpenID-Connect clients, boolean specifying whether to use a JWKS URL to obtain client - public keys. - - jwks.url: - description: - - For OpenID-Connect clients, URL where client keys in JWK are stored. - - jwt.credential.certificate: - description: - - For OpenID-Connect clients, client certificate for validating JWT issued by - client and signed by its key, base64-encoded. - - x509.subjectdn: - description: - - For OpenID-Connect clients, subject which will be used to authenticate the client. - type: str - version_added: 9.5.0 - - x509.allow.regex.pattern.comparison: - description: - - For OpenID-Connect clients, boolean specifying whether to allow C(x509.subjectdn) as regular expression. - type: bool - version_added: 9.5.0 - -extends_documentation_fragment: - - middleware_automation.keycloak.keycloak - - middleware_automation.keycloak.attributes - -author: - - Eike Frost (@eikef) -''' - -EXAMPLES = ''' -- name: Create or update Keycloak client (minimal example), authentication with credentials - middleware_automation.keycloak.keycloak_client: - auth_keycloak_url: https://auth.example.com/auth - auth_realm: master - auth_username: USERNAME - auth_password: PASSWORD - client_id: test - state: present - delegate_to: localhost - - -- name: Create or update Keycloak client (minimal example), authentication with token - middleware_automation.keycloak.keycloak_client: - auth_client_id: admin-cli - auth_keycloak_url: https://auth.example.com/auth - auth_realm: master - token: TOKEN - client_id: test - state: present - delegate_to: localhost - - -- name: Delete a Keycloak client - middleware_automation.keycloak.keycloak_client: - auth_client_id: admin-cli - auth_keycloak_url: https://auth.example.com/auth - auth_realm: master - auth_username: USERNAME - auth_password: PASSWORD - client_id: test - state: absent - delegate_to: localhost - - -- name: Create or update a Keycloak client (minimal example), with x509 authentication - middleware_automation.keycloak.keycloak_client: - auth_client_id: admin-cli - auth_keycloak_url: https://auth.example.com/auth - auth_realm: master - auth_username: USERNAME - auth_password: PASSWORD - realm: master - state: present - client_id: test - client_authenticator_type: client-x509 - attributes: - x509.subjectdn: "CN=client" - x509.allow.regex.pattern.comparison: false - - -- name: Create or update a Keycloak client (with all the bells and whistles) - middleware_automation.keycloak.keycloak_client: - auth_client_id: admin-cli - auth_keycloak_url: https://auth.example.com/auth - auth_realm: master - auth_username: USERNAME - auth_password: PASSWORD - state: present - realm: master - client_id: test - id: d8b127a3-31f6-44c8-a7e4-4ab9a3e78d95 - name: this_is_a_test - description: Description of this wonderful client - root_url: https://www.example.com/ - admin_url: https://www.example.com/admin_url - base_url: basepath - enabled: true - client_authenticator_type: client-secret - secret: REALLYWELLKEPTSECRET - redirect_uris: - - https://www.example.com/* - - http://localhost:8888/ - web_origins: - - https://www.example.com/* - not_before: 1507825725 - bearer_only: false - consent_required: false - standard_flow_enabled: true - implicit_flow_enabled: false - direct_access_grants_enabled: false - service_accounts_enabled: false - authorization_services_enabled: false - public_client: false - frontchannel_logout: false - protocol: openid-connect - full_scope_allowed: false - node_re_registration_timeout: -1 - client_template: test - use_template_config: false - use_template_scope: false - use_template_mappers: false - always_display_in_console: true - registered_nodes: - node01.example.com: 1507828202 - registration_access_token: eyJWT_TOKEN - surrogate_auth_required: false - default_roles: - - test01 - - test02 - authentication_flow_binding_overrides: - browser: 4c90336b-bf1d-4b87-916d-3677ba4e5fbb - 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 - - config: - attribute.name: Role - attribute.nameformat: Basic - single: false - consentRequired: false - name: role list - protocol: saml - protocolMapper: saml-role-list-mapper - attributes: - saml.authnstatement: true - saml.client.signature: true - saml.force.post.binding: true - saml.server.signature: true - saml.signature.algorithm: RSA_SHA256 - saml.signing.certificate: CERTIFICATEHERE - saml.signing.private.key: PRIVATEKEYHERE - saml_force_name_id_format: false - saml_name_id_format: username - saml_signature_canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#" - user.info.response.signature.alg: RS256 - request.object.signature.alg: RS256 - use.jwks.url: true - jwks.url: JWKS_URL_FOR_CLIENT_AUTH_JWT - jwt.credential.certificate: JWT_CREDENTIAL_CERTIFICATE_FOR_CLIENT_AUTH - delegate_to: localhost -''' - -RETURN = ''' -msg: - description: Message as to what action was taken. - returned: always - type: str - sample: "Client testclient has been updated" - -proposed: - description: Representation of proposed client. - returned: always - type: dict - sample: { - clientId: "test" - } - -existing: - description: Representation of existing client (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 after module execution (sample is truncated). - returned: on success - type: dict - sample: { - "adminUrl": "http://www.example.com/admin_url", - "attributes": { - "request.object.signature.alg": "RS256", - } - } -''' - -from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ - keycloak_argument_spec, get_token, KeycloakError, is_struct_included -from ansible.module_utils.basic import AnsibleModule -import copy - - -PROTOCOL_OPENID_CONNECT = 'openid-connect' -PROTOCOL_SAML = 'saml' -PROTOCOL_DOCKER_V2 = 'docker-v2' -CLIENT_META_DATA = ['authorizationServicesEnabled'] - - -def normalise_cr(clientrep, remove_ids=False): - """ Re-sorts any properties where the order so that diff's is minimised, and adds default values where appropriate so that the - the change detection is more effective. - - :param clientrep: the clientrep dict to be sanitized - :param remove_ids: If set to true, then the unique ID's of objects is removed to make the diff and checks for changed - not alert when the ID's of objects are not usually known, (e.g. for protocol_mappers) - :return: normalised clientrep dict - """ - # Avoid the dict passed in to be modified - clientrep = clientrep.copy() - - if 'attributes' in clientrep: - clientrep['attributes'] = list(sorted(clientrep['attributes'])) - - if 'defaultClientScopes' in clientrep: - clientrep['defaultClientScopes'] = list(sorted(clientrep['defaultClientScopes'])) - - if 'optionalClientScopes' in clientrep: - clientrep['optionalClientScopes'] = list(sorted(clientrep['optionalClientScopes'])) - - if 'redirectUris' in clientrep: - clientrep['redirectUris'] = list(sorted(clientrep['redirectUris'])) - - if 'protocolMappers' in clientrep: - clientrep['protocolMappers'] = sorted(clientrep['protocolMappers'], key=lambda x: (x.get('name'), x.get('protocol'), x.get('protocolMapper'))) - for mapper in clientrep['protocolMappers']: - if remove_ids: - mapper.pop('id', None) - - # Set to a default value. - mapper['consentRequired'] = mapper.get('consentRequired', False) - - return clientrep - - -def sanitize_cr(clientrep): - """ Removes probably sensitive details from a client representation. - - :param clientrep: the clientrep dict to be sanitized - :return: sanitized clientrep dict - """ - result = copy.deepcopy(clientrep) - if 'secret' in result: - result['secret'] = 'no_log' - if 'attributes' in result: - attributes = result['attributes'] - if isinstance(attributes, dict) and 'saml.signing.private.key' in attributes: - attributes['saml.signing.private.key'] = 'no_log' - return normalise_cr(result) - - -def get_authentication_flow_id(flow_name, realm, kc): - """ Get the authentication flow ID based on the flow name, realm, and Keycloak client. - - Args: - flow_name (str): The name of the authentication flow. - realm (str): The name of the realm. - kc (KeycloakClient): The Keycloak client instance. - - Returns: - str: The ID of the authentication flow. - - Raises: - KeycloakAPIException: If the authentication flow with the given name is not found in the realm. - """ - flow = kc.get_authentication_flow_by_alias(flow_name, realm) - if flow: - return flow["id"] - kc.module.fail_json(msg='Authentification flow %s not found in realm %s' % (flow_name, realm)) - - -def flow_binding_from_dict_to_model(newClientFlowBinding, realm, kc): - """ Convert a dictionary representing client flow bindings to a model representation. - - Args: - newClientFlowBinding (dict): A dictionary containing client flow bindings. - realm (str): The name of the realm. - kc (KeycloakClient): An instance of the KeycloakClient class. - - Returns: - dict: A dictionary representing the model flow bindings. The dictionary has two keys: - - "browser" (str or None): The ID of the browser authentication flow binding, or None if not provided. - - "direct_grant" (str or None): The ID of the direct grant authentication flow binding, or None if not provided. - - Raises: - KeycloakAPIException: If the authentication flow with the given name is not found in the realm. - - """ - - modelFlow = { - "browser": None, - "direct_grant": None - } - - for k, v in newClientFlowBinding.items(): - if not v: - continue - if k == "browser": - modelFlow["browser"] = v - elif k == "browser_name": - modelFlow["browser"] = get_authentication_flow_id(v, realm, kc) - elif k == "direct_grant": - modelFlow["direct_grant"] = v - elif k == "direct_grant_name": - modelFlow["direct_grant"] = get_authentication_flow_id(v, realm, kc) - - return modelFlow - - -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=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML, PROTOCOL_DOCKER_V2]), - protocolMapper=dict(type='str'), - config=dict(type='dict'), - ) - - authentication_flow_spec = dict( - browser=dict(type='str'), - browser_name=dict(type='str', aliases=['browserName']), - direct_grant=dict(type='str', aliases=['directGrant']), - direct_grant_name=dict(type='str', aliases=['directGrantName']), - ) - - meta_args = dict( - state=dict(default='present', choices=['present', 'absent']), - realm=dict(type='str', default='master'), - - id=dict(type='str'), - client_id=dict(type='str', aliases=['clientId']), - name=dict(type='str'), - description=dict(type='str'), - root_url=dict(type='str', aliases=['rootUrl']), - admin_url=dict(type='str', aliases=['adminUrl']), - base_url=dict(type='str', aliases=['baseUrl']), - surrogate_auth_required=dict(type='bool', aliases=['surrogateAuthRequired']), - enabled=dict(type='bool'), - client_authenticator_type=dict(type='str', choices=['client-secret', 'client-jwt', 'client-x509'], aliases=['clientAuthenticatorType']), - secret=dict(type='str', no_log=True), - registration_access_token=dict(type='str', aliases=['registrationAccessToken'], no_log=True), - default_roles=dict(type='list', elements='str', aliases=['defaultRoles']), - redirect_uris=dict(type='list', elements='str', aliases=['redirectUris']), - web_origins=dict(type='list', elements='str', aliases=['webOrigins']), - not_before=dict(type='int', aliases=['notBefore']), - bearer_only=dict(type='bool', aliases=['bearerOnly']), - consent_required=dict(type='bool', aliases=['consentRequired']), - standard_flow_enabled=dict(type='bool', aliases=['standardFlowEnabled']), - implicit_flow_enabled=dict(type='bool', aliases=['implicitFlowEnabled']), - direct_access_grants_enabled=dict(type='bool', aliases=['directAccessGrantsEnabled']), - service_accounts_enabled=dict(type='bool', aliases=['serviceAccountsEnabled']), - authorization_services_enabled=dict(type='bool', aliases=['authorizationServicesEnabled']), - public_client=dict(type='bool', aliases=['publicClient']), - frontchannel_logout=dict(type='bool', aliases=['frontchannelLogout']), - protocol=dict(type='str', choices=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML, PROTOCOL_DOCKER_V2]), - attributes=dict(type='dict'), - full_scope_allowed=dict(type='bool', aliases=['fullScopeAllowed']), - node_re_registration_timeout=dict(type='int', aliases=['nodeReRegistrationTimeout']), - registered_nodes=dict(type='dict', aliases=['registeredNodes']), - client_template=dict(type='str', aliases=['clientTemplate']), - use_template_config=dict(type='bool', aliases=['useTemplateConfig']), - use_template_scope=dict(type='bool', aliases=['useTemplateScope']), - use_template_mappers=dict(type='bool', aliases=['useTemplateMappers']), - always_display_in_console=dict(type='bool', aliases=['alwaysDisplayInConsole']), - authentication_flow_binding_overrides=dict( - type='dict', - aliases=['authenticationFlowBindingOverrides'], - options=authentication_flow_spec, - required_one_of=[['browser', 'direct_grant', 'browser_name', 'direct_grant_name']], - mutually_exclusive=[['browser', 'browser_name'], ['direct_grant', 'direct_grant_name']], - ), - protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']), - authorization_settings=dict(type='dict', aliases=['authorizationSettings']), - default_client_scopes=dict(type='list', elements='str', aliases=['defaultClientScopes']), - optional_client_scopes=dict(type='list', elements='str', aliases=['optionalClientScopes']), - ) - - argument_spec.update(meta_args) - - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True, - required_one_of=([['client_id', 'id'], - ['token', 'auth_realm', 'auth_username', 'auth_password']]), - required_together=([['auth_realm', 'auth_username', 'auth_password']])) - - 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') - cid = module.params.get('id') - state = module.params.get('state') - - # Filter and map the parameters names that apply to the client - client_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] - - # See if it already exists in Keycloak - if cid is None: - before_client = kc.get_client_by_clientid(module.params.get('client_id'), realm=realm) - if before_client is not None: - cid = before_client['id'] - else: - before_client = kc.get_client_by_id(cid, realm=realm) - - if before_client is None: - before_client = {} - - # Build a proposed changeset from parameters given to this module - changeset = {} - - for client_param in client_params: - new_param_value = module.params.get(client_param) - - # some lists in the Keycloak API are sorted, some are not. - if isinstance(new_param_value, list): - if client_param in ['attributes']: - try: - new_param_value = sorted(new_param_value) - except TypeError: - pass - # Unfortunately, the ansible argument spec checker introduces variables with null values when - # they are not specified - if client_param == 'protocol_mappers': - new_param_value = [{k: v for k, v in x.items() if v is not None} for x in new_param_value] - elif client_param == 'authentication_flow_binding_overrides': - new_param_value = flow_binding_from_dict_to_model(new_param_value, realm, kc) - - changeset[camel(client_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_client = before_client.copy() - desired_client.update(changeset) - - result['proposed'] = sanitize_cr(changeset) - result['existing'] = sanitize_cr(before_client) - - # Cater for when it doesn't exist (an empty dict) - if not before_client: - if state == 'absent': - # Do nothing and exit - if module._diff: - result['diff'] = dict(before='', after='') - result['changed'] = False - result['end_state'] = {} - result['msg'] = 'Client does not exist; doing nothing.' - module.exit_json(**result) - - # Process a creation - result['changed'] = True - - if 'clientId' not in desired_client: - module.fail_json(msg='client_id needs to be specified when creating a new client') - if 'protocol' not in desired_client: - desired_client['protocol'] = PROTOCOL_OPENID_CONNECT - - if module._diff: - result['diff'] = dict(before='', after=sanitize_cr(desired_client)) - - if module.check_mode: - module.exit_json(**result) - - # create it - kc.create_client(desired_client, realm=realm) - after_client = kc.get_client_by_clientid(desired_client['clientId'], realm=realm) - - result['end_state'] = sanitize_cr(after_client) - - result['msg'] = 'Client %s has been created.' % desired_client['clientId'] - 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 with the proposed updates we have - before_norm = normalise_cr(before_client, remove_ids=True) - desired_norm = normalise_cr(desired_client, remove_ids=True) - if module._diff: - result['diff'] = dict(before=sanitize_cr(before_norm), - after=sanitize_cr(desired_norm)) - result['changed'] = not is_struct_included(desired_norm, before_norm, CLIENT_META_DATA) - - module.exit_json(**result) - - # do the update - kc.update_client(cid, desired_client, realm=realm) - - after_client = kc.get_client_by_id(cid, realm=realm) - if before_client == after_client: - result['changed'] = False - if module._diff: - result['diff'] = dict(before=sanitize_cr(before_client), - after=sanitize_cr(after_client)) - - result['end_state'] = sanitize_cr(after_client) - - result['msg'] = 'Client %s has been updated.' % desired_client['clientId'] - module.exit_json(**result) - - else: - # Process a deletion (because state was not 'present') - result['changed'] = True - - if module._diff: - result['diff'] = dict(before=sanitize_cr(before_client), after='') - - if module.check_mode: - module.exit_json(**result) - - # delete it - kc.delete_client(cid, realm=realm) - result['proposed'] = {} - - result['end_state'] = {} - - result['msg'] = 'Client %s has been deleted.' % before_client['clientId'] - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/plugins/modules/keycloak_realm.py b/plugins/modules/keycloak_realm.py deleted file mode 100644 index a22795b..0000000 --- a/plugins/modules/keycloak_realm.py +++ /dev/null @@ -1,848 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright (c) 2017, Eike Frost -# Copyright (c) 2021, Christophe Gilles -# 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 -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: keycloak_realm - -short_description: Allows administration of Keycloak realm via Keycloak API - -version_added: 3.0.0 - -description: - - This module allows the administration of Keycloak realm via the Keycloak REST API. It - requires access to the REST API via 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 ones found in the - Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html). - Aliases are provided so camelCased versions can be used as well. - - - The Keycloak API does not always sanity check inputs e.g. you can set - SAML-specific settings on an OpenID Connect client for instance and vice versa. Be careful. - If you do not specify a setting, usually a sensible default is chosen. - -attributes: - check_mode: - support: full - diff_mode: - support: full - -options: - state: - description: - - State of the realm. - - On V(present), the realm will be created (or updated if it exists already). - - On V(absent), the realm will be removed if it exists. - choices: ['present', 'absent'] - default: 'present' - type: str - - id: - description: - - The realm to create. - type: str - realm: - description: - - The realm name. - type: str - access_code_lifespan: - description: - - The realm access code lifespan. - aliases: - - accessCodeLifespan - type: int - access_code_lifespan_login: - description: - - The realm access code lifespan login. - aliases: - - accessCodeLifespanLogin - type: int - access_code_lifespan_user_action: - description: - - The realm access code lifespan user action. - aliases: - - accessCodeLifespanUserAction - type: int - access_token_lifespan: - description: - - The realm access token lifespan. - aliases: - - accessTokenLifespan - type: int - access_token_lifespan_for_implicit_flow: - description: - - The realm access token lifespan for implicit flow. - aliases: - - accessTokenLifespanForImplicitFlow - type: int - account_theme: - description: - - The realm account theme. - aliases: - - accountTheme - type: str - action_token_generated_by_admin_lifespan: - description: - - The realm action token generated by admin lifespan. - aliases: - - actionTokenGeneratedByAdminLifespan - type: int - action_token_generated_by_user_lifespan: - description: - - The realm action token generated by user lifespan. - aliases: - - actionTokenGeneratedByUserLifespan - type: int - admin_events_details_enabled: - description: - - The realm admin events details enabled. - aliases: - - adminEventsDetailsEnabled - type: bool - admin_events_enabled: - description: - - The realm admin events enabled. - aliases: - - adminEventsEnabled - type: bool - admin_theme: - description: - - The realm admin theme. - aliases: - - adminTheme - type: str - attributes: - description: - - The realm attributes. - type: dict - browser_flow: - description: - - The realm browser flow. - aliases: - - browserFlow - type: str - browser_security_headers: - description: - - The realm browser security headers. - aliases: - - browserSecurityHeaders - type: dict - brute_force_protected: - description: - - The realm brute force protected. - aliases: - - bruteForceProtected - type: bool - client_authentication_flow: - description: - - The realm client authentication flow. - aliases: - - clientAuthenticationFlow - type: str - client_scope_mappings: - description: - - The realm client scope mappings. - aliases: - - clientScopeMappings - type: dict - default_default_client_scopes: - description: - - The realm default default client scopes. - aliases: - - defaultDefaultClientScopes - type: list - elements: str - default_groups: - description: - - The realm default groups. - aliases: - - defaultGroups - type: list - elements: str - default_locale: - description: - - The realm default locale. - aliases: - - defaultLocale - type: str - default_optional_client_scopes: - description: - - The realm default optional client scopes. - aliases: - - defaultOptionalClientScopes - type: list - elements: str - default_roles: - description: - - The realm default roles. - aliases: - - defaultRoles - type: list - elements: str - default_signature_algorithm: - description: - - The realm default signature algorithm. - aliases: - - defaultSignatureAlgorithm - type: str - direct_grant_flow: - description: - - The realm direct grant flow. - aliases: - - directGrantFlow - type: str - display_name: - description: - - The realm display name. - aliases: - - displayName - type: str - display_name_html: - description: - - The realm display name HTML. - aliases: - - displayNameHtml - type: str - docker_authentication_flow: - description: - - The realm docker authentication flow. - aliases: - - dockerAuthenticationFlow - type: str - duplicate_emails_allowed: - description: - - The realm duplicate emails allowed option. - aliases: - - duplicateEmailsAllowed - type: bool - edit_username_allowed: - description: - - The realm edit username allowed option. - aliases: - - editUsernameAllowed - type: bool - email_theme: - description: - - The realm email theme. - aliases: - - emailTheme - type: str - enabled: - description: - - The realm enabled option. - type: bool - enabled_event_types: - description: - - The realm enabled event types. - aliases: - - enabledEventTypes - type: list - elements: str - events_enabled: - description: - - Enables or disables login events for this realm. - aliases: - - eventsEnabled - type: bool - version_added: 3.6.0 - events_expiration: - description: - - The realm events expiration. - aliases: - - eventsExpiration - type: int - events_listeners: - description: - - The realm events listeners. - aliases: - - eventsListeners - type: list - elements: str - failure_factor: - description: - - The realm failure factor. - aliases: - - failureFactor - type: int - internationalization_enabled: - description: - - The realm internationalization enabled option. - aliases: - - internationalizationEnabled - type: bool - login_theme: - description: - - The realm login theme. - aliases: - - loginTheme - type: str - login_with_email_allowed: - description: - - The realm login with email allowed option. - aliases: - - loginWithEmailAllowed - type: bool - max_delta_time_seconds: - description: - - The realm max delta time in seconds. - aliases: - - maxDeltaTimeSeconds - type: int - max_failure_wait_seconds: - description: - - The realm max failure wait in seconds. - aliases: - - maxFailureWaitSeconds - type: int - minimum_quick_login_wait_seconds: - description: - - The realm minimum quick login wait in seconds. - aliases: - - minimumQuickLoginWaitSeconds - type: int - not_before: - description: - - The realm not before. - aliases: - - notBefore - type: int - offline_session_idle_timeout: - description: - - The realm offline session idle timeout. - aliases: - - offlineSessionIdleTimeout - type: int - offline_session_max_lifespan: - description: - - The realm offline session max lifespan. - aliases: - - offlineSessionMaxLifespan - type: int - offline_session_max_lifespan_enabled: - description: - - The realm offline session max lifespan enabled option. - aliases: - - offlineSessionMaxLifespanEnabled - type: bool - otp_policy_algorithm: - description: - - The realm otp policy algorithm. - aliases: - - otpPolicyAlgorithm - type: str - otp_policy_digits: - description: - - The realm otp policy digits. - aliases: - - otpPolicyDigits - type: int - otp_policy_initial_counter: - description: - - The realm otp policy initial counter. - aliases: - - otpPolicyInitialCounter - type: int - otp_policy_look_ahead_window: - description: - - The realm otp policy look ahead window. - aliases: - - otpPolicyLookAheadWindow - type: int - otp_policy_period: - description: - - The realm otp policy period. - aliases: - - otpPolicyPeriod - type: int - otp_policy_type: - description: - - The realm otp policy type. - aliases: - - otpPolicyType - type: str - otp_supported_applications: - description: - - The realm otp supported applications. - aliases: - - otpSupportedApplications - type: list - elements: str - password_policy: - description: - - The realm password policy. - aliases: - - passwordPolicy - type: str - permanent_lockout: - description: - - The realm permanent lockout. - aliases: - - permanentLockout - type: bool - quick_login_check_milli_seconds: - description: - - The realm quick login check in milliseconds. - aliases: - - quickLoginCheckMilliSeconds - type: int - refresh_token_max_reuse: - description: - - The realm refresh token max reuse. - aliases: - - refreshTokenMaxReuse - type: int - registration_allowed: - description: - - The realm registration allowed option. - aliases: - - registrationAllowed - type: bool - registration_email_as_username: - description: - - The realm registration email as username option. - aliases: - - registrationEmailAsUsername - type: bool - registration_flow: - description: - - The realm registration flow. - aliases: - - registrationFlow - type: str - remember_me: - description: - - The realm remember me option. - aliases: - - rememberMe - type: bool - reset_credentials_flow: - description: - - The realm reset credentials flow. - aliases: - - resetCredentialsFlow - type: str - reset_password_allowed: - description: - - The realm reset password allowed option. - aliases: - - resetPasswordAllowed - type: bool - revoke_refresh_token: - description: - - The realm revoke refresh token option. - aliases: - - revokeRefreshToken - type: bool - smtp_server: - description: - - The realm smtp server. - aliases: - - smtpServer - type: dict - ssl_required: - description: - - The realm ssl required option. - choices: ['all', 'external', 'none'] - aliases: - - sslRequired - type: str - sso_session_idle_timeout: - description: - - The realm sso session idle timeout. - aliases: - - ssoSessionIdleTimeout - type: int - sso_session_idle_timeout_remember_me: - description: - - The realm sso session idle timeout remember me. - aliases: - - ssoSessionIdleTimeoutRememberMe - type: int - sso_session_max_lifespan: - description: - - The realm sso session max lifespan. - aliases: - - ssoSessionMaxLifespan - type: int - sso_session_max_lifespan_remember_me: - description: - - The realm sso session max lifespan remember me. - aliases: - - ssoSessionMaxLifespanRememberMe - type: int - supported_locales: - description: - - The realm supported locales. - aliases: - - supportedLocales - type: list - elements: str - user_managed_access_allowed: - description: - - The realm user managed access allowed option. - aliases: - - userManagedAccessAllowed - type: bool - verify_email: - description: - - The realm verify email option. - aliases: - - verifyEmail - type: bool - wait_increment_seconds: - description: - - The realm wait increment in seconds. - aliases: - - waitIncrementSeconds - type: int - -extends_documentation_fragment: - - middleware_automation.keycloak.keycloak - - middleware_automation.keycloak.attributes - -author: - - Christophe Gilles (@kris2kris) -''' - -EXAMPLES = ''' -- name: Create or update Keycloak realm (minimal example) - middleware_automation.keycloak.keycloak_realm: - auth_client_id: admin-cli - auth_keycloak_url: https://auth.example.com/auth - auth_realm: master - auth_username: USERNAME - auth_password: PASSWORD - id: realm - realm: realm - state: present - -- name: Delete a Keycloak realm - middleware_automation.keycloak.keycloak_realm: - auth_client_id: admin-cli - auth_keycloak_url: https://auth.example.com/auth - auth_realm: master - auth_username: USERNAME - auth_password: PASSWORD - id: test - state: absent -''' - -RETURN = ''' -msg: - description: Message as to what action was taken. - returned: always - type: str - sample: "Realm testrealm has been updated" - -proposed: - description: Representation of proposed realm. - returned: always - type: dict - sample: { - id: "test" - } - -existing: - description: Representation of existing realm (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 realm after module execution (sample is truncated). - returned: on success - type: dict - sample: { - "adminUrl": "http://www.example.com/admin_url", - "attributes": { - "request.object.signature.alg": "RS256", - } - } -''' - -from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ - keycloak_argument_spec, get_token, KeycloakError -from ansible.module_utils.basic import AnsibleModule - - -def normalise_cr(realmrep): - """ Re-sorts any properties where the order is important so that diff's is minimised and the change detection is more effective. - - :param realmrep: the realmrep dict to be sanitized - :return: normalised realmrep dict - """ - # Avoid the dict passed in to be modified - realmrep = realmrep.copy() - - if 'enabledEventTypes' in realmrep: - realmrep['enabledEventTypes'] = list(sorted(realmrep['enabledEventTypes'])) - - if 'otpSupportedApplications' in realmrep: - realmrep['otpSupportedApplications'] = list(sorted(realmrep['otpSupportedApplications'])) - - if 'supportedLocales' in realmrep: - realmrep['supportedLocales'] = list(sorted(realmrep['supportedLocales'])) - - return realmrep - - -def sanitize_cr(realmrep): - """ Removes probably sensitive details from a realm representation. - - :param realmrep: the realmrep dict to be sanitized - :return: sanitized realmrep dict - """ - result = realmrep.copy() - if 'secret' in result: - result['secret'] = '********' - if 'attributes' in result: - if 'saml.signing.private.key' in result['attributes']: - result['attributes'] = result['attributes'].copy() - result['attributes']['saml.signing.private.key'] = '********' - return normalise_cr(result) - - -def main(): - """ - Module execution - - :return: - """ - argument_spec = keycloak_argument_spec() - - meta_args = dict( - state=dict(default='present', choices=['present', 'absent']), - - id=dict(type='str'), - realm=dict(type='str'), - access_code_lifespan=dict(type='int', aliases=['accessCodeLifespan']), - access_code_lifespan_login=dict(type='int', aliases=['accessCodeLifespanLogin']), - access_code_lifespan_user_action=dict(type='int', aliases=['accessCodeLifespanUserAction']), - access_token_lifespan=dict(type='int', aliases=['accessTokenLifespan'], no_log=False), - access_token_lifespan_for_implicit_flow=dict(type='int', aliases=['accessTokenLifespanForImplicitFlow'], no_log=False), - account_theme=dict(type='str', aliases=['accountTheme']), - action_token_generated_by_admin_lifespan=dict(type='int', aliases=['actionTokenGeneratedByAdminLifespan'], no_log=False), - action_token_generated_by_user_lifespan=dict(type='int', aliases=['actionTokenGeneratedByUserLifespan'], no_log=False), - admin_events_details_enabled=dict(type='bool', aliases=['adminEventsDetailsEnabled']), - admin_events_enabled=dict(type='bool', aliases=['adminEventsEnabled']), - admin_theme=dict(type='str', aliases=['adminTheme']), - attributes=dict(type='dict'), - browser_flow=dict(type='str', aliases=['browserFlow']), - browser_security_headers=dict(type='dict', aliases=['browserSecurityHeaders']), - brute_force_protected=dict(type='bool', aliases=['bruteForceProtected']), - client_authentication_flow=dict(type='str', aliases=['clientAuthenticationFlow']), - client_scope_mappings=dict(type='dict', aliases=['clientScopeMappings']), - default_default_client_scopes=dict(type='list', elements='str', aliases=['defaultDefaultClientScopes']), - default_groups=dict(type='list', elements='str', aliases=['defaultGroups']), - default_locale=dict(type='str', aliases=['defaultLocale']), - default_optional_client_scopes=dict(type='list', elements='str', aliases=['defaultOptionalClientScopes']), - default_roles=dict(type='list', elements='str', aliases=['defaultRoles']), - default_signature_algorithm=dict(type='str', aliases=['defaultSignatureAlgorithm']), - direct_grant_flow=dict(type='str', aliases=['directGrantFlow']), - display_name=dict(type='str', aliases=['displayName']), - display_name_html=dict(type='str', aliases=['displayNameHtml']), - docker_authentication_flow=dict(type='str', aliases=['dockerAuthenticationFlow']), - duplicate_emails_allowed=dict(type='bool', aliases=['duplicateEmailsAllowed']), - edit_username_allowed=dict(type='bool', aliases=['editUsernameAllowed']), - email_theme=dict(type='str', aliases=['emailTheme']), - enabled=dict(type='bool'), - enabled_event_types=dict(type='list', elements='str', aliases=['enabledEventTypes']), - events_enabled=dict(type='bool', aliases=['eventsEnabled']), - events_expiration=dict(type='int', aliases=['eventsExpiration']), - events_listeners=dict(type='list', elements='str', aliases=['eventsListeners']), - failure_factor=dict(type='int', aliases=['failureFactor']), - internationalization_enabled=dict(type='bool', aliases=['internationalizationEnabled']), - login_theme=dict(type='str', aliases=['loginTheme']), - login_with_email_allowed=dict(type='bool', aliases=['loginWithEmailAllowed']), - max_delta_time_seconds=dict(type='int', aliases=['maxDeltaTimeSeconds']), - max_failure_wait_seconds=dict(type='int', aliases=['maxFailureWaitSeconds']), - minimum_quick_login_wait_seconds=dict(type='int', aliases=['minimumQuickLoginWaitSeconds']), - not_before=dict(type='int', aliases=['notBefore']), - offline_session_idle_timeout=dict(type='int', aliases=['offlineSessionIdleTimeout']), - offline_session_max_lifespan=dict(type='int', aliases=['offlineSessionMaxLifespan']), - offline_session_max_lifespan_enabled=dict(type='bool', aliases=['offlineSessionMaxLifespanEnabled']), - otp_policy_algorithm=dict(type='str', aliases=['otpPolicyAlgorithm']), - otp_policy_digits=dict(type='int', aliases=['otpPolicyDigits']), - otp_policy_initial_counter=dict(type='int', aliases=['otpPolicyInitialCounter']), - otp_policy_look_ahead_window=dict(type='int', aliases=['otpPolicyLookAheadWindow']), - otp_policy_period=dict(type='int', aliases=['otpPolicyPeriod']), - otp_policy_type=dict(type='str', aliases=['otpPolicyType']), - otp_supported_applications=dict(type='list', elements='str', aliases=['otpSupportedApplications']), - password_policy=dict(type='str', aliases=['passwordPolicy'], no_log=False), - permanent_lockout=dict(type='bool', aliases=['permanentLockout']), - quick_login_check_milli_seconds=dict(type='int', aliases=['quickLoginCheckMilliSeconds']), - refresh_token_max_reuse=dict(type='int', aliases=['refreshTokenMaxReuse'], no_log=False), - registration_allowed=dict(type='bool', aliases=['registrationAllowed']), - registration_email_as_username=dict(type='bool', aliases=['registrationEmailAsUsername']), - registration_flow=dict(type='str', aliases=['registrationFlow']), - remember_me=dict(type='bool', aliases=['rememberMe']), - reset_credentials_flow=dict(type='str', aliases=['resetCredentialsFlow']), - reset_password_allowed=dict(type='bool', aliases=['resetPasswordAllowed'], no_log=False), - revoke_refresh_token=dict(type='bool', aliases=['revokeRefreshToken']), - smtp_server=dict(type='dict', aliases=['smtpServer']), - ssl_required=dict(choices=["external", "all", "none"], aliases=['sslRequired']), - sso_session_idle_timeout=dict(type='int', aliases=['ssoSessionIdleTimeout']), - sso_session_idle_timeout_remember_me=dict(type='int', aliases=['ssoSessionIdleTimeoutRememberMe']), - sso_session_max_lifespan=dict(type='int', aliases=['ssoSessionMaxLifespan']), - sso_session_max_lifespan_remember_me=dict(type='int', aliases=['ssoSessionMaxLifespanRememberMe']), - supported_locales=dict(type='list', elements='str', aliases=['supportedLocales']), - user_managed_access_allowed=dict(type='bool', aliases=['userManagedAccessAllowed']), - verify_email=dict(type='bool', aliases=['verifyEmail']), - wait_increment_seconds=dict(type='int', aliases=['waitIncrementSeconds']), - ) - - argument_spec.update(meta_args) - - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True, - required_one_of=([['id', 'realm', 'enabled'], - ['token', 'auth_realm', 'auth_username', 'auth_password']]), - required_together=([['auth_realm', 'auth_username', 'auth_password']])) - - 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') - - # convert module parameters to realm representation parameters (if they belong in there) - params_to_ignore = list(keycloak_argument_spec().keys()) + ['state'] - - # Filter and map the parameters names that apply to the role - realm_params = [x for x in module.params - if x not in params_to_ignore and - module.params.get(x) is not None] - - # See whether the realm already exists in Keycloak - before_realm = kc.get_realm_by_id(realm=realm) - - if before_realm is None: - before_realm = {} - - # Build a proposed changeset from parameters given to this module - changeset = {} - - for realm_param in realm_params: - new_param_value = module.params.get(realm_param) - changeset[camel(realm_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_realm = before_realm.copy() - desired_realm.update(changeset) - - result['proposed'] = sanitize_cr(changeset) - before_realm_sanitized = sanitize_cr(before_realm) - result['existing'] = before_realm_sanitized - - # Cater for when it doesn't exist (an empty dict) - if not before_realm: - if state == 'absent': - # Do nothing and exit - if module._diff: - result['diff'] = dict(before='', after='') - result['changed'] = False - result['end_state'] = {} - result['msg'] = 'Realm does not exist, doing nothing.' - module.exit_json(**result) - - # Process a creation - result['changed'] = True - - if 'id' not in desired_realm: - module.fail_json(msg='id needs to be specified when creating a new realm') - - if module._diff: - result['diff'] = dict(before='', after=sanitize_cr(desired_realm)) - - if module.check_mode: - module.exit_json(**result) - - # create it - kc.create_realm(desired_realm) - after_realm = kc.get_realm_by_id(desired_realm['id']) - - result['end_state'] = sanitize_cr(after_realm) - - result['msg'] = 'Realm %s has been created.' % desired_realm['id'] - module.exit_json(**result) - - else: - if state == 'present': - # Process an update - - # doing an update - result['changed'] = True - if module.check_mode: - # We can only compare the current realm with the proposed updates we have - before_norm = normalise_cr(before_realm) - desired_norm = normalise_cr(desired_realm) - if module._diff: - result['diff'] = dict(before=sanitize_cr(before_norm), - after=sanitize_cr(desired_norm)) - result['changed'] = (before_norm != desired_norm) - - module.exit_json(**result) - - # do the update - kc.update_realm(desired_realm, realm=realm) - - after_realm = kc.get_realm_by_id(realm=realm) - - if before_realm == after_realm: - result['changed'] = False - - result['end_state'] = sanitize_cr(after_realm) - - if module._diff: - result['diff'] = dict(before=before_realm_sanitized, - after=sanitize_cr(after_realm)) - - result['msg'] = 'Realm %s has been updated.' % desired_realm['id'] - module.exit_json(**result) - - else: - # Process a deletion (because state was not 'present') - result['changed'] = True - - if module._diff: - result['diff'] = dict(before=before_realm_sanitized, after='') - - if module.check_mode: - module.exit_json(**result) - - # delete it - kc.delete_realm(realm=realm) - - result['proposed'] = {} - result['end_state'] = {} - - result['msg'] = 'Realm %s has been deleted.' % before_realm['id'] - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/plugins/modules/keycloak_role.py b/plugins/modules/keycloak_role.py deleted file mode 100644 index c48e9c9..0000000 --- a/plugins/modules/keycloak_role.py +++ /dev/null @@ -1,439 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright (c) 2019, Adam Goossens -# 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 -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: keycloak_role - -short_description: Allows administration of Keycloak roles via Keycloak API - -version_added: 3.4.0 - -description: - - This module allows you to add, remove or modify Keycloak roles 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. - - - 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/8.0/rest-api/index.html). - - - Attributes are multi-valued in the Keycloak API. All attributes are lists of individual values and will - be returned that way by this module. You may pass single values for attributes when calling the module, - and this will be translated into a list suitable for the API. - -attributes: - check_mode: - support: full - diff_mode: - support: full - -options: - state: - description: - - State of the role. - - On V(present), the role will be created if it does not yet exist, or updated with the parameters you provide. - - On V(absent), the role will be 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 (e.g. 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 - version_added: 7.1.0 - 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 - version_added: 7.1.0 - 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 - required: false - 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.attributes - -author: - - Laurent Paumier (@laurpaum) -''' - -EXAMPLES = ''' -- 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 - 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/auth - 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 - 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 - 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 - 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 = ''' -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" - } -''' - -from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ - keycloak_argument_spec, get_token, KeycloakError, is_struct_included -from ansible.module_utils.basic import AnsibleModule -import copy - - -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'], required=False), - 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']]), - required_together=([['auth_realm', 'auth_username', 'auth_password']])) - - 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 - # via 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'] = 'Role {name} has been created'.format(name=name) - module.exit_json(**result) - - else: - if state == 'present': - compare_exclude = [] - 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'] = "No changes required to role {name}.".format(name=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'] = "Role {name} has been updated".format(name=name) - 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'] = "Role {name} has been deleted".format(name=name) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/plugins/modules/keycloak_user_federation.py b/plugins/modules/keycloak_user_federation.py deleted file mode 100644 index 864cfbc..0000000 --- a/plugins/modules/keycloak_user_federation.py +++ /dev/null @@ -1,1119 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# 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 -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: keycloak_user_federation - -short_description: Allows administration of Keycloak user federations via Keycloak API - -version_added: 3.7.0 - -description: - - This module allows you to add, remove or modify Keycloak user federations 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. - - - 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: - check_mode: - support: full - diff_mode: - support: full - -options: - state: - description: - - State of the user federation. - - On V(present), the user federation will be created if it does not yet exist, or updated with - the parameters you provide. - - On V(absent), the user federation will be removed if it exists. - default: 'present' - type: str - choices: - - present - - absent - - realm: - description: - - The Keycloak realm under which this user federation resides. - default: 'master' - type: str - - id: - description: - - The unique ID for this user federation. If left empty, the user federation will be searched - by its O(name). - type: str - - name: - description: - - Display name of provider when linked in admin console. - type: str - - provider_id: - description: - - Provider for this user federation. Built-in providers are V(ldap), V(kerberos), and V(sssd). - Custom user storage providers can also be used. - aliases: - - providerId - type: str - - provider_type: - description: - - Component type for user federation (only supported value is V(org.keycloak.storage.UserStorageProvider)). - aliases: - - providerType - default: org.keycloak.storage.UserStorageProvider - type: str - - parent_id: - description: - - Unique ID for the parent of this user federation. Realm ID will be automatically used if left blank. - aliases: - - parentId - type: str - - remove_unspecified_mappers: - description: - - Remove mappers that are not specified in the configuration for this federation. - - Set to V(false) to keep mappers that are not listed in O(mappers). - type: bool - default: true - - bind_credential_update_mode: - description: - - The value of the config parameter O(config.bindCredential) is redacted in the Keycloak responses. - Comparing the redacted value with the desired value always evaluates to not equal. This means - the before and desired states are never equal if the parameter is set. - - Set to V(always) to include O(config.bindCredential) in the comparison of before and desired state. - Because of the redacted value returned by Keycloak the module will always detect a change - and make an update if a O(config.bindCredential) value is set. - - Set to V(only_indirect) to exclude O(config.bindCredential) when comparing the before state with the - desired state. The value of O(config.bindCredential) will only be updated if there are other changes - to the user federation that require an update. - type: str - default: always - choices: - - always - - only_indirect - - 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(ldap), V(kerberos) and V(sssd). - It is easiest to obtain valid config values by dumping an already-existing user federation - configuration through check-mode in the RV(existing) field. - - The value V(sssd) has been supported since middleware_automation.keycloak 2.0.0. - type: dict - suboptions: - enabled: - description: - - Enable/disable this user federation. - default: true - type: bool - - priority: - description: - - Priority of provider when doing a user lookup. Lowest first. - default: 0 - type: int - - importEnabled: - description: - - If V(true), LDAP users will be imported into Keycloak DB and synced by the configured - sync policies. - default: true - type: bool - - editMode: - description: - - V(READ_ONLY) is a read-only LDAP store. V(WRITABLE) means data will be synced back to LDAP - on demand. V(UNSYNCED) means user data will be imported, but not synced back to LDAP. - type: str - choices: - - READ_ONLY - - WRITABLE - - UNSYNCED - - syncRegistrations: - description: - - Should newly created users be created within LDAP store? Priority effects which - provider is chosen to sync the new user. - default: false - type: bool - - vendor: - description: - - LDAP vendor (provider). - - Use short name. For instance, write V(rhds) for "Red Hat Directory Server". - type: str - - usernameLDAPAttribute: - description: - - Name of LDAP attribute, which is mapped as Keycloak username. For many LDAP server - vendors it can be V(uid). For Active directory it can be V(sAMAccountName) or V(cn). - The attribute should be filled for all LDAP user records you want to import from - LDAP to Keycloak. - type: str - - rdnLDAPAttribute: - description: - - Name of LDAP attribute, which is used as RDN (top attribute) of typical user DN. - Usually it's the same as Username LDAP attribute, however it is not required. For - example for Active directory, it is common to use V(cn) as RDN attribute when - username attribute might be V(sAMAccountName). - type: str - - uuidLDAPAttribute: - description: - - Name of LDAP attribute, which is used as unique object identifier (UUID) for objects - in LDAP. For many LDAP server vendors, it is V(entryUUID); however some are different. - For example for Active directory it should be V(objectGUID). If your LDAP server does - not support the notion of UUID, you can use any other attribute that is supposed to - be unique among LDAP users in tree. - type: str - - userObjectClasses: - description: - - All values of LDAP objectClass attribute for users in LDAP divided by comma. - For example V(inetOrgPerson, organizationalPerson). Newly created Keycloak users - will be written to LDAP with all those object classes and existing LDAP user records - are found just if they contain all those object classes. - type: str - - connectionUrl: - description: - - Connection URL to your LDAP server. - type: str - - usersDn: - description: - - Full DN of LDAP tree where your users are. This DN is the parent of LDAP users. - type: str - - customUserSearchFilter: - description: - - Additional LDAP Filter for filtering searched users. Leave this empty if you don't - need additional filter. - type: str - - searchScope: - description: - - For one level, the search applies only for users in the DNs specified by User DNs. - For subtree, the search applies to the whole subtree. See LDAP documentation for - more details. - default: '1' - type: str - choices: - - '1' - - '2' - - authType: - description: - - Type of the Authentication method used during LDAP Bind operation. It is used in - most of the requests sent to the LDAP server. - default: 'none' - type: str - choices: - - none - - simple - - bindDn: - description: - - DN of LDAP user which will be used by Keycloak to access LDAP server. - type: str - - bindCredential: - description: - - Password of LDAP admin. - type: str - - startTls: - description: - - Encrypts the connection to LDAP using STARTTLS, which will disable connection pooling. - default: false - type: bool - - usePasswordModifyExtendedOp: - description: - - Use the LDAPv3 Password Modify Extended Operation (RFC-3062). The password modify - extended operation usually requires that LDAP user already has password in the LDAP - server. So when this is used with 'Sync Registrations', it can be good to add also - 'Hardcoded LDAP attribute mapper' with randomly generated initial password. - default: false - type: bool - - validatePasswordPolicy: - description: - - Determines if Keycloak should validate the password with the realm password policy - before updating it. - default: false - type: bool - - trustEmail: - description: - - If enabled, email provided by this provider is not verified even if verification is - enabled for the realm. - default: false - type: bool - - useTruststoreSpi: - description: - - Specifies whether LDAP connection will use the truststore SPI with the truststore - configured in standalone.xml/domain.xml. V(always) means that it will always use it. - V(never) means that it will not use it. V(ldapsOnly) means that it will use if - your connection URL use ldaps. Note even if standalone.xml/domain.xml is not - configured, the default Java cacerts or certificate specified by - C(javax.net.ssl.trustStore) property will be used. - default: ldapsOnly - type: str - choices: - - always - - ldapsOnly - - never - - connectionTimeout: - description: - - LDAP Connection Timeout in milliseconds. - type: int - - readTimeout: - description: - - LDAP Read Timeout in milliseconds. This timeout applies for LDAP read operations. - type: int - - pagination: - description: - - Does the LDAP server support pagination. - default: true - type: bool - - connectionPooling: - description: - - Determines if Keycloak should use connection pooling for accessing LDAP server. - default: true - type: bool - - connectionPoolingAuthentication: - description: - - A list of space-separated authentication types of connections that may be pooled. - type: str - choices: - - none - - simple - - DIGEST-MD5 - - connectionPoolingDebug: - description: - - A string that indicates the level of debug output to produce. Example valid values are - V(fine) (trace connection creation and removal) and V(all) (all debugging information). - type: str - - connectionPoolingInitSize: - description: - - The number of connections per connection identity to create when initially creating a - connection for the identity. - type: int - - connectionPoolingMaxSize: - description: - - The maximum number of connections per connection identity that can be maintained - concurrently. - type: int - - connectionPoolingPrefSize: - description: - - The preferred number of connections per connection identity that should be maintained - concurrently. - type: int - - connectionPoolingProtocol: - description: - - A list of space-separated protocol types of connections that may be pooled. - Valid types are V(plain) and V(ssl). - type: str - - connectionPoolingTimeout: - description: - - The number of milliseconds that an idle connection may remain in the pool without - being closed and removed from the pool. - type: int - - allowKerberosAuthentication: - description: - - Enable/disable HTTP authentication of users with SPNEGO/Kerberos tokens. The data - about authenticated users will be provisioned from this LDAP server. - default: false - type: bool - - kerberosRealm: - description: - - Name of kerberos realm. - type: str - - krbPrincipalAttribute: - description: - - Name of the LDAP attribute, which refers to Kerberos principal. - This is used to lookup appropriate LDAP user after successful Kerberos/SPNEGO authentication in Keycloak. - When this is empty, the LDAP user will be looked based on LDAP username corresponding - to the first part of his Kerberos principal. For instance, for principal C(john@KEYCLOAK.ORG), - it will assume that LDAP username is V(john). - type: str - - serverPrincipal: - description: - - Full name of server principal for HTTP service including server and domain name. For - example V(HTTP/host.foo.org@FOO.ORG). Use V(*) to accept any service principal in the - KeyTab file. - type: str - - keyTab: - description: - - Location of Kerberos KeyTab file containing the credentials of server principal. For - example V(/etc/krb5.keytab). - type: str - - debug: - description: - - Enable/disable debug logging to standard output for Krb5LoginModule. - type: bool - - useKerberosForPasswordAuthentication: - description: - - Use Kerberos login module for authenticate username/password against Kerberos server - instead of authenticating against LDAP server with Directory Service API. - default: false - type: bool - - allowPasswordAuthentication: - description: - - Enable/disable possibility of username/password authentication against Kerberos database. - type: bool - - batchSizeForSync: - description: - - Count of LDAP users to be imported from LDAP to Keycloak within a single transaction. - default: 1000 - type: int - - fullSyncPeriod: - description: - - Period for full synchronization in seconds. - default: -1 - type: int - - changedSyncPeriod: - description: - - Period for synchronization of changed or newly created LDAP users in seconds. - default: -1 - type: int - - updateProfileFirstLogin: - description: - - Update profile on first login. - type: bool - - cachePolicy: - description: - - Cache Policy for this storage provider. - type: str - default: 'DEFAULT' - choices: - - DEFAULT - - EVICT_DAILY - - EVICT_WEEKLY - - MAX_LIFESPAN - - NO_CACHE - - evictionDay: - description: - - Day of the week the entry will become invalid on. - type: str - - evictionHour: - description: - - Hour of day the entry will become invalid on. - type: str - - evictionMinute: - description: - - Minute of day the entry will become invalid on. - type: str - - maxLifespan: - description: - - Max lifespan of cache entry in milliseconds. - type: int - - referral: - description: - - Specifies if LDAP referrals should be followed or ignored. Please note that enabling - referrals can slow down authentication as it allows the LDAP server to decide which other - LDAP servers to use. This could potentially include untrusted servers. - type: str - choices: - - ignore - - follow - - 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. If no ID is given, the mapper will be searched by name. - type: str - - parentId: - description: - - Unique ID for the parent of this mapper. ID of the user federation will automatically - be used if left blank. - type: str - - providerId: - description: - - The mapper type for this mapper (for instance V(user-attribute-ldap-mapper)). - type: str - - providerType: - description: - - Component type for this mapper. - type: str - default: org.keycloak.storage.ldap.mappers.LDAPStorageMapper - - config: - description: - - Dict specifying the configuration options for the mapper; the contents differ - depending on the value of I(identityProviderMapper). - type: dict - -extends_documentation_fragment: - - middleware_automation.keycloak.keycloak - - middleware_automation.keycloak.attributes - -author: - - Laurent Paumier (@laurpaum) -''' - -EXAMPLES = ''' -- name: Create LDAP user federation - middleware_automation.keycloak.keycloak_user_federation: - auth_keycloak_url: https://keycloak.example.com/auth - auth_realm: master - auth_username: admin - auth_password: password - realm: my-realm - name: my-ldap - state: present - 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 - -- name: Create Kerberos user federation - middleware_automation.keycloak.keycloak_user_federation: - auth_keycloak_url: https://keycloak.example.com/auth - auth_realm: master - auth_username: admin - auth_password: password - realm: my-realm - name: my-kerberos - state: present - provider_id: kerberos - provider_type: org.keycloak.storage.UserStorageProvider - config: - priority: 0 - enabled: true - cachePolicy: DEFAULT - kerberosRealm: EXAMPLE.COM - serverPrincipal: HTTP/host.example.com@EXAMPLE.COM - keyTab: keytab - allowPasswordAuthentication: false - updateProfileFirstLogin: false - -- name: Create sssd user federation - middleware_automation.keycloak.keycloak_user_federation: - auth_keycloak_url: https://keycloak.example.com/auth - auth_realm: master - auth_username: admin - auth_password: password - realm: my-realm - name: my-sssd - state: present - provider_id: sssd - provider_type: org.keycloak.storage.UserStorageProvider - config: - priority: 0 - enabled: true - cachePolicy: DEFAULT - -- name: Delete user federation - middleware_automation.keycloak.keycloak_user_federation: - auth_keycloak_url: https://keycloak.example.com/auth - auth_realm: master - auth_username: admin - auth_password: password - realm: my-realm - name: my-federation - state: absent -''' - -RETURN = ''' -msg: - description: Message as to what action was taken. - returned: always - type: str - sample: "No changes required to user federation 164bb483-c613-482e-80fe-7f1431308799." - -proposed: - description: Representation of proposed user federation. - returned: always - type: dict - sample: { - "config": { - "allowKerberosAuthentication": "false", - "authType": "simple", - "batchSizeForSync": "1000", - "bindCredential": "**********", - "bindDn": "cn=directory reader", - "cachePolicy": "DEFAULT", - "connectionPooling": "true", - "connectionUrl": "ldaps://ldap.example.com:636", - "debug": "false", - "editMode": "READ_ONLY", - "enabled": "true", - "importEnabled": "true", - "pagination": "true", - "priority": "0", - "rdnLDAPAttribute": "uid", - "searchScope": "1", - "syncRegistrations": "false", - "trustEmail": "false", - "useKerberosForPasswordAuthentication": "false", - "useTruststoreSpi": "ldapsOnly", - "userObjectClasses": "inetOrgPerson, organizationalPerson", - "usernameLDAPAttribute": "uid", - "usersDn": "ou=Users,dc=example,dc=com", - "uuidLDAPAttribute": "entryUUID", - "validatePasswordPolicy": "false", - "vendor": "other" - }, - "name": "ldap", - "providerId": "ldap", - "providerType": "org.keycloak.storage.UserStorageProvider" - } - -existing: - description: Representation of existing user federation. - returned: always - type: dict - sample: { - "config": { - "allowKerberosAuthentication": "false", - "authType": "simple", - "batchSizeForSync": "1000", - "bindCredential": "**********", - "bindDn": "cn=directory reader", - "cachePolicy": "DEFAULT", - "changedSyncPeriod": "-1", - "connectionPooling": "true", - "connectionUrl": "ldaps://ldap.example.com:636", - "debug": "false", - "editMode": "READ_ONLY", - "enabled": "true", - "fullSyncPeriod": "-1", - "importEnabled": "true", - "pagination": "true", - "priority": "0", - "rdnLDAPAttribute": "uid", - "searchScope": "1", - "syncRegistrations": "false", - "trustEmail": "false", - "useKerberosForPasswordAuthentication": "false", - "useTruststoreSpi": "ldapsOnly", - "userObjectClasses": "inetOrgPerson, organizationalPerson", - "usernameLDAPAttribute": "uid", - "usersDn": "ou=Users,dc=example,dc=com", - "uuidLDAPAttribute": "entryUUID", - "validatePasswordPolicy": "false", - "vendor": "other" - }, - "id": "01122837-9047-4ae4-8ca0-6e2e891a765f", - "mappers": [ - { - "config": { - "always.read.value.from.ldap": "false", - "is.mandatory.in.ldap": "false", - "ldap.attribute": "mail", - "read.only": "true", - "user.model.attribute": "email" - }, - "id": "17d60ce2-2d44-4c2c-8b1f-1fba601b9a9f", - "name": "email", - "parentId": "01122837-9047-4ae4-8ca0-6e2e891a765f", - "providerId": "user-attribute-ldap-mapper", - "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper" - } - ], - "name": "myfed", - "parentId": "myrealm", - "providerId": "ldap", - "providerType": "org.keycloak.storage.UserStorageProvider" - } - -end_state: - description: Representation of user federation after module execution. - returned: on success - type: dict - sample: { - "config": { - "allowPasswordAuthentication": "false", - "cachePolicy": "DEFAULT", - "enabled": "true", - "kerberosRealm": "EXAMPLE.COM", - "keyTab": "/etc/krb5.keytab", - "priority": "0", - "serverPrincipal": "HTTP/host.example.com@EXAMPLE.COM", - "updateProfileFirstLogin": "false" - }, - "id": "cf52ae4f-4471-4435-a0cf-bb620cadc122", - "mappers": [], - "name": "kerberos", - "parentId": "myrealm", - "providerId": "kerberos", - "providerType": "org.keycloak.storage.UserStorageProvider" - } -''' - -from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ - keycloak_argument_spec, get_token, KeycloakError -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.six.moves.urllib.parse import urlencode -from copy import deepcopy - - -def normalize_kc_comp(comp): - if 'config' in comp: - # kc completely removes the parameter `krbPrincipalAttribute` if it is set to `''`; the unset kc parameter is equivalent to `''`; - # to make change detection and diff more accurate we set it again in the kc responses - if 'krbPrincipalAttribute' not in comp['config']: - comp['config']['krbPrincipalAttribute'] = [''] - - # kc stores a timestamp of the last sync in `lastSync` to time the periodic sync, it is removed to minimize diff/changes - comp['config'].pop('lastSync', None) - - -def sanitize(comp): - compcopy = deepcopy(comp) - if 'config' in compcopy: - compcopy['config'] = {k: v[0] for k, v in compcopy['config'].items()} - if 'bindCredential' in compcopy['config']: - compcopy['config']['bindCredential'] = '**********' - if 'mappers' in compcopy: - for mapper in compcopy['mappers']: - if 'config' in mapper: - mapper['config'] = {k: v[0] for k, v in mapper['config'].items()} - return compcopy - - -def main(): - """ - Module execution - - :return: - """ - argument_spec = keycloak_argument_spec() - - config_spec = dict( - allowKerberosAuthentication=dict(type='bool', default=False), - allowPasswordAuthentication=dict(type='bool'), - authType=dict(type='str', choices=['none', 'simple'], default='none'), - batchSizeForSync=dict(type='int', default=1000), - bindCredential=dict(type='str', no_log=True), - bindDn=dict(type='str'), - cachePolicy=dict(type='str', choices=['DEFAULT', 'EVICT_DAILY', 'EVICT_WEEKLY', 'MAX_LIFESPAN', 'NO_CACHE'], default='DEFAULT'), - changedSyncPeriod=dict(type='int', default=-1), - connectionPooling=dict(type='bool', default=True), - connectionPoolingAuthentication=dict(type='str', choices=['none', 'simple', 'DIGEST-MD5']), - connectionPoolingDebug=dict(type='str'), - connectionPoolingInitSize=dict(type='int'), - connectionPoolingMaxSize=dict(type='int'), - connectionPoolingPrefSize=dict(type='int'), - connectionPoolingProtocol=dict(type='str'), - connectionPoolingTimeout=dict(type='int'), - connectionTimeout=dict(type='int'), - connectionUrl=dict(type='str'), - customUserSearchFilter=dict(type='str'), - debug=dict(type='bool'), - editMode=dict(type='str', choices=['READ_ONLY', 'WRITABLE', 'UNSYNCED']), - enabled=dict(type='bool', default=True), - evictionDay=dict(type='str'), - evictionHour=dict(type='str'), - evictionMinute=dict(type='str'), - fullSyncPeriod=dict(type='int', default=-1), - importEnabled=dict(type='bool', default=True), - kerberosRealm=dict(type='str'), - keyTab=dict(type='str', no_log=False), - maxLifespan=dict(type='int'), - pagination=dict(type='bool', default=True), - priority=dict(type='int', default=0), - rdnLDAPAttribute=dict(type='str'), - readTimeout=dict(type='int'), - referral=dict(type='str', choices=['ignore', 'follow']), - searchScope=dict(type='str', choices=['1', '2'], default='1'), - serverPrincipal=dict(type='str'), - krbPrincipalAttribute=dict(type='str'), - startTls=dict(type='bool', default=False), - syncRegistrations=dict(type='bool', default=False), - trustEmail=dict(type='bool', default=False), - updateProfileFirstLogin=dict(type='bool'), - useKerberosForPasswordAuthentication=dict(type='bool', default=False), - usePasswordModifyExtendedOp=dict(type='bool', default=False, no_log=False), - useTruststoreSpi=dict(type='str', choices=['always', 'ldapsOnly', 'never'], default='ldapsOnly'), - userObjectClasses=dict(type='str'), - usernameLDAPAttribute=dict(type='str'), - usersDn=dict(type='str'), - uuidLDAPAttribute=dict(type='str'), - validatePasswordPolicy=dict(type='bool', default=False), - vendor=dict(type='str'), - ) - - mapper_spec = dict( - id=dict(type='str'), - name=dict(type='str'), - parentId=dict(type='str'), - providerId=dict(type='str'), - providerType=dict(type='str', default='org.keycloak.storage.ldap.mappers.LDAPStorageMapper'), - config=dict(type='dict'), - ) - - meta_args = dict( - config=dict(type='dict', options=config_spec), - state=dict(type='str', default='present', choices=['present', 'absent']), - realm=dict(type='str', default='master'), - id=dict(type='str'), - name=dict(type='str'), - provider_id=dict(type='str', aliases=['providerId']), - provider_type=dict(type='str', aliases=['providerType'], default='org.keycloak.storage.UserStorageProvider'), - parent_id=dict(type='str', aliases=['parentId']), - remove_unspecified_mappers=dict(type='bool', default=True), - bind_credential_update_mode=dict(type='str', default='always', choices=['always', 'only_indirect']), - 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=([['id', 'name'], - ['token', 'auth_realm', 'auth_username', 'auth_password']]), - required_together=([['auth_realm', 'auth_username', 'auth_password']])) - - 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') - config = module.params.get('config') - mappers = module.params.get('mappers') - cid = module.params.get('id') - name = module.params.get('name') - - # Keycloak API expects config parameters to be arrays containing a single string element - if config is not None: - module.params['config'] = { - k: [str(v).lower() if not isinstance(v, str) else v] - for k, v in config.items() - if config[k] is not None - } - - if mappers is not None: - for mapper in mappers: - if mapper.get('config') is not None: - mapper['config'] = { - k: [str(v).lower() if not isinstance(v, str) else v] - for k, v in mapper['config'].items() - if mapper['config'][k] is not None - } - - # Filter and map the parameters names that apply - comp_params = [x for x in module.params - if x not in list(keycloak_argument_spec().keys()) - + ['state', 'realm', 'mappers', 'remove_unspecified_mappers', 'bind_credential_update_mode'] - and module.params.get(x) is not None] - - # See if it already exists in Keycloak - if cid is None: - found = kc.get_components(urlencode(dict(type='org.keycloak.storage.UserStorageProvider', name=name)), realm) - if len(found) > 1: - module.fail_json(msg='No ID given and found multiple user federations with name `{name}`. Cannot continue.'.format(name=name)) - before_comp = next(iter(found), None) - if before_comp is not None: - cid = before_comp['id'] - else: - before_comp = kc.get_component(cid, realm) - - if before_comp is None: - before_comp = {} - - # if user federation exists, get associated mappers - if cid is not None and before_comp: - before_comp['mappers'] = sorted(kc.get_components(urlencode(dict(parent=cid)), realm), key=lambda x: x.get('name') or '') - - normalize_kc_comp(before_comp) - - # Build a proposed changeset from parameters given to this module - changeset = {} - - for param in comp_params: - new_param_value = module.params.get(param) - old_value = before_comp[camel(param)] if camel(param) in before_comp else None - if param == 'mappers': - new_param_value = [{k: v for k, v in x.items() if v is not None} for x in new_param_value] - 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: - if module.params['provider_id'] in ['kerberos', 'sssd']: - module.fail_json(msg='Cannot configure mappers for {type} provider.'.format(type=module.params['provider_id'])) - 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 cid is None: - old_mapper = {} - elif change.get('id') is not None: - old_mapper = next((before_mapper for before_mapper in before_comp.get('mappers', []) if before_mapper["id"] == change['id']), None) - if old_mapper is None: - old_mapper = {} - else: - found = [before_mapper for before_mapper in before_comp.get('mappers', []) if before_mapper['name'] == change['name']] - if len(found) > 1: - module.fail_json(msg='Found multiple mappers with name `{name}`. Cannot continue.'.format(name=change['name'])) - if len(found) == 1: - old_mapper = found[0] - else: - old_mapper = {} - new_mapper = old_mapper.copy() - new_mapper.update(change) - # changeset contains all desired mappers: those existing, to update or to create - if changeset.get('mappers') is None: - changeset['mappers'] = list() - changeset['mappers'].append(new_mapper) - changeset['mappers'] = sorted(changeset['mappers'], key=lambda x: x.get('name') or '') - - # to keep unspecified existing mappers we add them to the desired mappers list, unless they're already present - if not module.params['remove_unspecified_mappers'] and 'mappers' in before_comp: - changeset_mapper_ids = [mapper['id'] for mapper in changeset['mappers'] if 'id' in mapper] - changeset['mappers'].extend([mapper for mapper in before_comp['mappers'] if mapper['id'] not in changeset_mapper_ids]) - - # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) - desired_comp = before_comp.copy() - desired_comp.update(changeset) - - result['proposed'] = sanitize(changeset) - result['existing'] = sanitize(before_comp) - - # Cater for when it doesn't exist (an empty dict) - if not before_comp: - if state == 'absent': - # Do nothing and exit - if module._diff: - result['diff'] = dict(before='', after='') - result['changed'] = False - result['end_state'] = {} - result['msg'] = 'User federation does not exist; doing nothing.' - module.exit_json(**result) - - # Process a creation - result['changed'] = True - - if module.check_mode: - if module._diff: - result['diff'] = dict(before='', after=sanitize(desired_comp)) - module.exit_json(**result) - - # create it - desired_mappers = desired_comp.pop('mappers', []) - after_comp = kc.create_component(desired_comp, realm) - cid = after_comp['id'] - updated_mappers = [] - # when creating a user federation, keycloak automatically creates default mappers - default_mappers = kc.get_components(urlencode(dict(parent=cid)), realm) - - # create new mappers or update existing default mappers - for desired_mapper in desired_mappers: - found = [default_mapper for default_mapper in default_mappers if default_mapper['name'] == desired_mapper['name']] - if len(found) > 1: - module.fail_json(msg='Found multiple mappers with name `{name}`. Cannot continue.'.format(name=desired_mapper['name'])) - if len(found) == 1: - old_mapper = found[0] - else: - old_mapper = {} - - new_mapper = old_mapper.copy() - new_mapper.update(desired_mapper) - - if new_mapper.get('id') is not None: - kc.update_component(new_mapper, realm) - updated_mappers.append(new_mapper) - else: - if new_mapper.get('parentId') is None: - new_mapper['parentId'] = cid - updated_mappers.append(kc.create_component(new_mapper, realm)) - - if module.params['remove_unspecified_mappers']: - # we remove all unwanted default mappers - # we use ids so we dont accidently remove one of the previously updated default mapper - for default_mapper in default_mappers: - if not default_mapper['id'] in [x['id'] for x in updated_mappers]: - kc.delete_component(default_mapper['id'], realm) - - after_comp['mappers'] = kc.get_components(urlencode(dict(parent=cid)), realm) - normalize_kc_comp(after_comp) - if module._diff: - result['diff'] = dict(before='', after=sanitize(after_comp)) - result['end_state'] = sanitize(after_comp) - result['msg'] = "User federation {id} has been created".format(id=cid) - module.exit_json(**result) - - else: - if state == 'present': - # Process an update - - desired_copy = deepcopy(desired_comp) - before_copy = deepcopy(before_comp) - # exclude bindCredential when checking wether an update is required, therefore - # updating it only if there are other changes - if module.params['bind_credential_update_mode'] == 'only_indirect': - desired_copy.get('config', []).pop('bindCredential', None) - before_copy.get('config', []).pop('bindCredential', None) - # no changes - if desired_copy == before_copy: - result['changed'] = False - result['end_state'] = sanitize(desired_comp) - result['msg'] = "No changes required to user federation {id}.".format(id=cid) - module.exit_json(**result) - - # doing an update - result['changed'] = True - - if module._diff: - result['diff'] = dict(before=sanitize(before_comp), after=sanitize(desired_comp)) - - if module.check_mode: - module.exit_json(**result) - - # do the update - desired_mappers = desired_comp.pop('mappers', []) - kc.update_component(desired_comp, realm) - - for before_mapper in before_comp.get('mappers', []): - # remove unwanted existing mappers that will not be updated - if not before_mapper['id'] in [x['id'] for x in desired_mappers if 'id' in x]: - kc.delete_component(before_mapper['id'], realm) - - for mapper in desired_mappers: - if mapper in before_comp.get('mappers', []): - continue - if mapper.get('id') is not None: - kc.update_component(mapper, realm) - else: - if mapper.get('parentId') is None: - mapper['parentId'] = desired_comp['id'] - kc.create_component(mapper, realm) - - after_comp = kc.get_component(cid, realm) - after_comp['mappers'] = sorted(kc.get_components(urlencode(dict(parent=cid)), realm), key=lambda x: x.get('name') or '') - normalize_kc_comp(after_comp) - after_comp_sanitized = sanitize(after_comp) - before_comp_sanitized = sanitize(before_comp) - result['end_state'] = after_comp_sanitized - if module._diff: - result['diff'] = dict(before=before_comp_sanitized, after=after_comp_sanitized) - result['changed'] = before_comp_sanitized != after_comp_sanitized - result['msg'] = "User federation {id} has been updated".format(id=cid) - module.exit_json(**result) - - elif state == 'absent': - # Process a deletion - result['changed'] = True - - if module._diff: - result['diff'] = dict(before=sanitize(before_comp), after='') - - if module.check_mode: - module.exit_json(**result) - - # delete it - kc.delete_component(cid, realm) - - result['end_state'] = {} - - result['msg'] = "User federation {id} has been deleted".format(id=cid) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/requirements.txt b/requirements.txt index 5de7845..b2366a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ ################################################# -# python dependencies required to be installed +# python dependencies required to be installed # on the controller host with: # pip install -r requirements.txt # -netaddr -lxml # for middleware_automation.common.maven_artifact \ No newline at end of file +netaddr \ No newline at end of file diff --git a/requirements.yml b/requirements.yml index 06e5714..ab434b2 100644 --- a/requirements.yml +++ b/requirements.yml @@ -1,5 +1,7 @@ --- collections: - - name: middleware_automation.common + - name: middleware_automation.redhat_csp_download version: ">=1.2.1" - - name: ansible.posix + - name: middleware_automation.wildfly + version: ">=0.0.5" + - name: community.general diff --git a/roles/keycloak/README.md b/roles/keycloak/README.md index 3d3b560..71787b1 100644 --- a/roles/keycloak/README.md +++ b/roles/keycloak/README.md @@ -10,7 +10,6 @@ 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` @@ -20,12 +19,8 @@ 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 +* the `redhat_csp_download` role from [middleware_automation.redhat_csp_download](https://github.com/ansible-middleware/redhat-csp-download) collection if Red Hat Single Sign-on zip have to be downloaded from RHN. +* the `wildfly_driver` role from [middleware_automation.wildfly](https://github.com/ansible-middleware/wildfly) collection Versions @@ -33,19 +28,18 @@ 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)| +|`7.5.0 GA` |September 20, 2021 |`15.0.2` | `7.4.0` |[Release Notes](https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/7.5/html/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. +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)| +|`7.5.0 GA` |January 20, 2022 |`7.5.1 GA` |[Release Notes](https://access.redhat.com/articles/6646321)| + Role Defaults @@ -56,12 +50,9 @@ Role Defaults | 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` | @@ -69,19 +60,13 @@ Role Defaults |`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_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_service_pidfile`| pid file path for service | `/run/keycloak.pid` | |`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_home`| JAVA_HOME of installed JRE, leave empty for using specified keycloak_jvm_package RPM path | `None` | |`keycloak_java_opts`| Additional JVM options | `-Xms1024m -Xmx2048m` | @@ -89,37 +74,39 @@ Role Defaults | 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//`| -|`keycloak_version`| keycloak.org package version | `18.0.2` | +|`keycloak_rhsso_enable`| Enable Red Hat Single Sign-on installation | `False` | +|`keycloak_offline_install` | perform an offline install | `False`| +|`keycloak_download_url`| Download URL for keycloak | `https://github.com/keycloak/keycloak/releases/download//`| +|`keycloak_rhsso_download_url`| Download URL for RHSSO | `https://access.redhat.com/jbossnetwork/restricted/softwareDownload.html?softwareId=`| +|`keycloak_version`| keycloak.org package version | `15.0.2` | +|`keycloak_rhsso_version`| RHSSO version | `7.5.0` | +|`keycloak_rhsso_apply_patches`| Install RHSSO more recent cumulative patch | `False` | |`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` | +|`keycloak_rhn_url` | Base download URI for customer portal | `https://access.redhat.com/jbossnetwork/restricted/softwareDownload.html?softwareId=` | +|`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_archive` | keycloak install archive filename | `keycloak-{{ 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_rhsso_archive` | Red Hat SSO install archive filename | `rh-sso-{{ keycloak_rhsso_version }}-server-dist.zip` | +|`keycloak_rhsso_installdir`| Installation path for Red Hat SSO | `{{ keycloak_dest }}/rh-sso-{{ keycloak_rhsso_version | regex_replace('^([0-9])\.([0-9]*).*', '\1.\2') }}` | +|`keycloak_rhsso_download_url`| Full download URI for Red Hat SSO | `{{ keycloak_rhn_url }}{{ rhsso_rhn_id }}` | +|`keycloak_jboss_home` | Installation work directory | `{{ keycloak_rhsso_installdir if keycloak_rhsso_enable else keycloak_installdir }}` | |`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` | +|`keycloak_force_install` | Remove pre-existing versions of service | `False` | +|`keycloak_url` | URL for configuration rest calls | `http://{{ keycloak_host }}:{{ keycloak_http_port }}` | +|`keycloak_management_url` | URL for management console rest calls | `http://{{ keycloak_host }}:{{ keycloak_management_http_port }}` | +|`rhsso_rhn_id` | Customer Portal product ID for Red Hat SSO | `{{ rhsso_rhn_ids[keycloak_rhsso_version].id }}` | Role Variables @@ -130,28 +117,25 @@ 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/` | +|`keycloak_frontend_url` | frontend URL for keycloak endpoint | `http://localhost:8080/auth` | -The following parameters are _required_ only when `keycloak_ha_enabled` is true: +The following variables 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` | +|:---------|:------------|:---------| +|`keycloak_modcluster_url` | URL for the modcluster reverse proxy | `localhost` | +|`keycloak_jdbc_engine` | backend database engine when db is enabled: [ postgres, mariadb ] | `postgres` | +|`infinispan_url` | URL for the infinispan remote-cache server | `localhost:11122` | +|`infinispan_user` | username for connecting to infinispan | `supervisor` | +|`infinispan_pass` | password for connecting to infinispan | `supervisor` | +|`infinispan_sasl_mechanism`| Authentication type | `SCRAM-SHA-512` | +|`infinispan_use_ssl`| Enable hotrod TLS communication | `False` | +|`infinispan_trust_store_path`| Path to truststore with infinispan server certificate | `/etc/pki/java/cacerts` | +|`infinispan_trust_store_password`| Password for opening truststore | `changeit` | -The following parameters are _required_ only when `keycloak_db_enabled` is true: +The following variables are _required_ only when `keycloak_db_enabled` is True: | Variable | Description | Default | |:---------|:------------|:---------| @@ -161,17 +145,12 @@ The following parameters are _required_ only when `keycloak_db_enabled` is true: |`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 +Example Playbooks ----------------- +_NOTE_: use ansible vaults or other security systems for storing credentials. + + * The following is an example playbook that makes use of the role to install keycloak from remote: ```yaml @@ -179,10 +158,33 @@ Example Playbook - hosts: ... vars: keycloak_admin_password: "remembertochangeme" + collections: + - middleware_automation.keycloak roles: - middleware_automation.keycloak.keycloak ``` +* The following is an example playbook that makes use of the role to install Red Hat Single Sign-On from RHN: + +```yaml +--- +- name: Playbook for RHSSO + hosts: keycloak + collections: + - middleware_automation.redhat_csp_download + roles: + - redhat_csp_download + tasks: + - name: Keycloak Role + include_role: + name: keycloak + vars: + keycloak_admin_password: "remembertochangeme" + keycloak_rhsso_enable: True + rhn_username: '' + rhn_password: '' +``` + * The following example playbook makes use of the role to install keycloak from the controller node: @@ -197,10 +199,49 @@ Example Playbook name: keycloak vars: keycloak_admin_password: "remembertochangeme" - keycloak_offline_install: true + keycloak_offline_install: True # This should be the filename of keycloak archive on Ansible node: keycloak-16.1.0.zip ``` + +* This playbook installs Red Hat Single Sign-On from an alternate url: + +```yaml +--- +- hosts: keycloak + collections: + - middleware_automation.keycloak + tasks: + - name: Keycloak Role + include_role: + name: keycloak + vars: + keycloak_admin_password: "remembertochangeme" + keycloak_rhsso_enable: True + keycloak_rhsso_download_url: "" + # This should be the full of remote source rhsso zip file and can contain basic authentication credentials +``` + + +* The following is an example playbook that makes use of the role to install Red Hat Single Sign-On offline from the controller node, and apply latest cumulative patch: + +```yaml +--- +- hosts: keycloak + collections: + - middleware_automation.keycloak + tasks: + - name: Keycloak Role + include_role: + name: keycloak + vars: + keycloak_admin_password: "remembertochangeme" + keycloak_rhsso_enable: True + keycloak_offline_install: True + keycloak_rhsso_apply_patches: True + # This should be the filename of rhsso zip file on Ansible node: rh-sso-7.5-server-dist.zip +``` + License ------- diff --git a/roles/keycloak/defaults/main.yml b/roles/keycloak/defaults/main.yml index 137111f..7ef632a 100644 --- a/roles/keycloak/defaults/main.yml +++ b/roles/keycloak/defaults/main.yml @@ -1,38 +1,38 @@ --- ### 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_version: 15.0.2 +keycloak_archive: "keycloak-{{ 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 + +### Configuration specific to Red Hat Single Sign-On +keycloak_rhsso_version: 7.5.0 +rhsso_rhn_id: "{{ rhsso_rhn_ids[keycloak_rhsso_version].id }}" +keycloak_rhsso_archive: "rh-sso-{{ keycloak_rhsso_version }}-server-dist.zip" +keycloak_rhsso_installdir: "{{ keycloak_dest }}/rh-sso-{{ keycloak_rhsso_version | regex_replace('^([0-9])\\.([0-9]*).*', '\\1.\\2') }}" +keycloak_rhn_url: 'https://access.redhat.com/jbossnetwork/restricted/softwareDownload.html?softwareId=' +keycloak_rhsso_download_url: "{{ keycloak_rhn_url }}{{ rhsso_rhn_id }}" +keycloak_rhsso_apply_patches: False + +### keycloak/rhsso choice: by default install rhsso if rhn credentials are defined +keycloak_rhsso_enable: "{{ True if rhsso_rhn_id is defined and rhn_username is defined and rhn_password is defined else False }}" +# whether to install from local archive; filename must be keycloak_archive or keycloak_rhsso_archive depending on keycloak_rhsso_enable +keycloak_offline_install: False ### Install location and service settings +keycloak_jvm_package: java-1.8.0-openjdk-headless keycloak_java_home: keycloak_dest: /opt/keycloak -keycloak_jboss_home: "{{ keycloak_installdir }}" -keycloak_jboss_port_offset: 0 +keycloak_jboss_home: "{{ keycloak_rhsso_installdir if keycloak_rhsso_enable else keycloak_installdir }}" 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 +keycloak_service_pidfile: "/run/keycloak.pid" +keycloak_configure_firewalld: False ### administrator console password keycloak_admin_password: '' @@ -44,62 +44,44 @@ 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: [] +keycloak_prefer_ipv4: True ### Enable configuration for database backend, clustering and remote caches on infinispan -keycloak_ha_enabled: false +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 +keycloak_force_install: False -### mod_cluster reverse proxy list -keycloak_modcluster_enabled: "{{ True if keycloak_ha_enabled else False }}" +### mod_cluster reverse proxy 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: +keycloak_frontend_url: http://localhost:8080/auth ### 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 +infinispan_user: supervisor +infinispan_pass: supervisor +infinispan_url: localhost +infinispan_sasl_mechanism: SCRAM-SHA-512 +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 +infinispan_trust_store_path: /etc/pki/java/cacerts +infinispan_trust_store_password: changeit -### database backend engine: values [ 'postgres', 'mariadb', 'sqlserver' ] +### database backend engine: values [ 'postgres', 'mariadb' ] 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 @@ -110,15 +92,3 @@ keycloak_default_jdbc: 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 }}" diff --git a/roles/keycloak/meta/argument_specs.yml b/roles/keycloak/meta/argument_specs.yml index 5f6052d..983d59d 100644 --- a/roles/keycloak/meta/argument_specs.yml +++ b/roles/keycloak/meta/argument_specs.yml @@ -2,38 +2,82 @@ argument_specs: main: options: keycloak_version: - default: "18.0.2" + # line 3 of keycloak/defaults/main.yml + default: "15.0.2" description: "keycloak.org package version" type: "str" keycloak_archive: - default: "keycloak-legacy-{{ keycloak_version }}.zip" + # line 4 of keycloak/defaults/main.yml + default: "keycloak-{{ 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: + # line 33 of keycloak/defaults/main.yml default: false description: "Ensure firewalld is running and configure keycloak ports" type: "bool" keycloak_download_url: + # line 5 of keycloak/defaults/main.yml default: "https://github.com/keycloak/keycloak/releases/download/{{ keycloak_version }}/{{ keycloak_archive }}" description: "Download URL for keycloak" type: "str" keycloak_download_url_9x: + # line 6 of keycloak/defaults/main.yml default: "https://downloads.jboss.org/keycloak/{{ keycloak_version }}/{{ keycloak_archive }}" description: "Download URL for keycloak (deprecated)" type: "str" keycloak_installdir: + # line 7 of keycloak/defaults/main.yml default: "{{ keycloak_dest }}/keycloak-{{ keycloak_version }}" description: "Installation path" type: "str" + keycloak_rhsso_version: + # line 10 of keycloak/defaults/main.yml + default: "7.5.0" + description: "Red Hat Single Sign-On version" + type: "str" + rhsso_rhn_id: + # line 11 of keycloak/defaults/main.yml + default: "{{ rhsso_rhn_ids[keycloak_rhsso_version].id }}" + description: "Customer Portal product ID for Red Hat SSO" + type: "str" + keycloak_rhsso_archive: + # line 12 of keycloak/defaults/main.yml + default: "rh-sso-{{ keycloak_rhsso_version }}-server-dist.zip" + description: "ed Hat SSO install archive filename" + type: "str" + keycloak_rhsso_apply_patches: + # line 16 of keycloak/defaults/main.yml + default: false + description: "Install RHSSO more recent cumulative patch" + type: "bool" + keycloak_rhsso_installdir: + # line 13 of keycloak/defaults/main.yml + default: "{{ keycloak_dest }}/rh-sso-{{ keycloak_rhsso_version | regex_replace('^([0-9])\\.([0-9]*).*', '\\1.\\2') }}" + description: "Installation path for Red Hat SSO" + type: "str" + keycloak_rhn_url: + # line 14 of keycloak/defaults/main.yml + default: "https://access.redhat.com/jbossnetwork/restricted/softwareDownload.html?softwareId=" + description: "Base download URI for customer portal" + type: "str" + keycloak_rhsso_download_url: + # line 15 of keycloak/defaults/main.yml + default: "{{ keycloak_rhn_url }}{{ rhsso_rhn_id }}" + description: "Full download URI for Red Hat SSO" + type: "str" + keycloak_rhsso_enable: + # line 18 of keycloak/defaults/main.yml + default: "{{ True if rhsso_rhn_id is defined and rhn_username is defined and rhn_password is defined else False }}" + description: "Enable Red Hat Single Sign-on installation" + type: "str" keycloak_offline_install: + # line 20 of keycloak/defaults/main.yml default: false description: "Perform an offline install" type: "bool" keycloak_jvm_package: + # line 23 of keycloak/defaults/main.yml default: "java-1.8.0-openjdk-headless" description: "RHEL java package runtime rpm" type: "str" @@ -41,345 +85,212 @@ argument_specs: description: "JAVA_HOME of installed JRE, leave empty for using specified keycloak_jvm_package RPM path" type: "str" keycloak_dest: + # line 24 of keycloak/defaults/main.yml default: "/opt/keycloak" description: "Root installation directory" type: "str" keycloak_jboss_home: - default: "{{ keycloak_installdir }}" + # line 25 of keycloak/defaults/main.yml + default: "{{ keycloak_rhsso_installdir if keycloak_rhsso_enable else 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: + # line 26 of keycloak/defaults/main.yml default: "{{ keycloak_jboss_home }}/standalone/configuration" description: "Path for configuration" type: "str" keycloak_config_standalone_xml: + # line 27 of keycloak/defaults/main.yml default: "keycloak.xml" description: "Service configuration filename" type: "str" keycloak_config_path_to_standalone_xml: + # line 28 of keycloak/defaults/main.yml default: "{{ keycloak_jboss_home }}/standalone/configuration/{{ keycloak_config_standalone_xml }}" description: "Custom path for configuration" type: "str" keycloak_config_override_template: + # line 30 of keycloak/defaults/main.yml 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: + # line 29 of keycloak/defaults/main.yml default: "keycloak" description: "posix account username" type: "str" keycloak_service_group: + # line 30 of keycloak/defaults/main.yml default: "keycloak" description: "posix account group" type: "str" keycloak_service_pidfile: - default: "/run/keycloak/keycloak.pid" + # line 31 of keycloak/defaults/main.yml + default: "/run/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: + # line 34 of keycloak/defaults/main.yml 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: + # line 35 of keycloak/defaults/main.yml default: "localhost" description: "Hostname for service" type: "str" keycloak_http_port: + # line 36 of keycloak/defaults/main.yml default: 8080 description: "Listening HTTP port" type: "int" keycloak_https_port: + # line 37 of keycloak/defaults/main.yml default: 8443 description: "Listening HTTPS port" type: "int" keycloak_ajp_port: + # line 38 of keycloak/defaults/main.yml default: 8009 description: "Listening AJP port" type: "int" keycloak_jgroups_port: + # line 39 of keycloak/defaults/main.yml default: 7600 description: "jgroups cluster tcp port" type: "int" keycloak_management_http_port: + # line 40 of keycloak/defaults/main.yml default: 9990 description: "Management port (http)" type: "int" keycloak_management_https_port: + # line 41 of keycloak/defaults/main.yml default: 9993 description: "Management port (https)" type: "int" keycloak_java_opts: + # line 42 of keycloak/defaults/main.yml default: "-Xms1024m -Xmx2048m" description: "Additional JVM options" type: "str" keycloak_prefer_ipv4: + # line 43 of keycloak/defaults/main.yml default: true description: "Prefer IPv4 stack and addresses for port binding" type: "bool" keycloak_ha_enabled: + # line 46 of keycloak/defaults/main.yml 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: + # line 48 of keycloak/defaults/main.yml default: "{{ True if keycloak_ha_enabled else False }}" description: "Enable auto configuration for database backend" - type: "bool" + type: "str" keycloak_admin_user: + # line 51 of keycloak/defaults/main.yml default: "admin" description: "Administration console user account" type: "str" keycloak_auth_realm: + # line 52 of keycloak/defaults/main.yml default: "master" description: "Name for rest authentication realm" type: "str" keycloak_auth_client: + # line 53 of keycloak/defaults/main.yml default: "admin-cli" description: "Authentication client for configuration REST calls" type: "str" keycloak_force_install: + # line 55 of keycloak/defaults/main.yml 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: + # line 58 of keycloak/defaults/main.yml 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: + # line 59 of keycloak/defaults/main.yml 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: + infinispan_user: + # line 62 of keycloak/defaults/main.yml default: "supervisor" description: "Username for connecting to infinispan" type: "str" - keycloak_infinispan_pass: + infinispan_pass: + # line 63 of keycloak/defaults/main.yml default: "supervisor" description: "Password for connecting to infinispan" type: "str" - keycloak_infinispan_url: + infinispan_url: + # line 64 of keycloak/defaults/main.yml default: "localhost" description: "URL for the infinispan remote-cache server" type: "str" - keycloak_infinispan_sasl_mechanism: + infinispan_sasl_mechanism: + # line 65 of keycloak/defaults/main.yml default: "SCRAM-SHA-512" description: "Authentication type to infinispan server" type: "str" - keycloak_infinispan_use_ssl: + infinispan_use_ssl: + # line 66 of keycloak/defaults/main.yml default: false description: "Enable hotrod client TLS communication" type: "bool" - keycloak_infinispan_trust_store_path: + infinispan_trust_store_path: + # line 68 of keycloak/defaults/main.yml default: "/etc/pki/java/cacerts" description: "TODO document argument" type: "str" - keycloak_infinispan_trust_store_password: + infinispan_trust_store_password: + # line 69 of keycloak/defaults/main.yml default: "changeit" description: "Path to truststore containing infinispan server certificate" type: "str" keycloak_jdbc_engine: + # line 72 of keycloak/defaults/main.yml default: "postgres" - description: "Backend database flavour when db is enabled: [ postgres, mariadb, sqlserver ]" + description: "Backend database flavour when db is enabled: [ postgres, mariadb ]" type: "str" keycloak_db_user: + # line 74 of keycloak/defaults/main.yml default: "keycloak-user" description: "Username for connecting to database" type: "str" keycloak_db_pass: + # line 75 of keycloak/defaults/main.yml default: "keycloak-pass" description: "Password for connecting to database" type: "str" keycloak_jdbc_url: + # line 76 of keycloak/defaults/main.yml default: "{{ keycloak_default_jdbc[keycloak_jdbc_engine].url }}" description: "URL for connecting to backend database" type: "str" keycloak_jdbc_driver_version: + # line 77 of keycloak/defaults/main.yml default: "{{ keycloak_default_jdbc[keycloak_jdbc_engine].version }}" description: "Version for the JDBC driver to download" type: "str" keycloak_admin_password: + # line 4 of keycloak/vars/main.yml 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 }}" + # line 12 of keycloak/vars/main.yml + default: "http://{{ keycloak_host }}:{{ keycloak_http_port }}" description: "URL for configuration rest calls" type: "str" keycloak_management_url: - default: "http://{{ keycloak_host }}:{{ keycloak_management_http_port + keycloak_jboss_port_offset }}" + # line 13 of keycloak/vars/main.yml + default: "http://{{ keycloak_host }}:{{ keycloak_management_http_port }}" 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" - 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: "JBossNetwork API category for Single Sign-On" - type: "str" diff --git a/roles/keycloak/meta/main.yml b/roles/keycloak/meta/main.yml index 59499e6..4760762 100644 --- a/roles/keycloak/meta/main.yml +++ b/roles/keycloak/meta/main.yml @@ -1,7 +1,7 @@ --- collections: - - middleware_automation.common - - ansible.posix + - middleware_automation.redhat_csp_download + - middleware_automation.wildfly galaxy_info: role_name: keycloak @@ -12,12 +12,12 @@ galaxy_info: license: Apache License 2.0 - min_ansible_version: "2.16" + min_ansible_version: "2.9" platforms: - - name: EL - versions: - - "8" + - name: EL + versions: + - 8 galaxy_tags: - keycloak diff --git a/roles/keycloak/tasks/debian.yml b/roles/keycloak/tasks/debian.yml deleted file mode 100644 index acfadcc..0000000 --- a/roles/keycloak/tasks/debian.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- name: Include firewall config tasks - ansible.builtin.include_tasks: - file: iptables.yml - apply: - tags: - - firewall - when: keycloak_configure_iptables - tags: - - firewall diff --git a/roles/keycloak/tasks/fastpackages.yml b/roles/keycloak/tasks/fastpackages.yml index a89f7f6..78bc556 100644 --- a/roles/keycloak/tasks/fastpackages.yml +++ b/roles/keycloak/tasks/fastpackages.yml @@ -1,31 +1,22 @@ --- -- name: "Check if packages are already installed" # noqa command-instead-of-module this runs faster - ansible.builtin.command: "rpm -q {{ packages_list | join(' ') }}" - register: rpm_info - changed_when: false - failed_when: false - when: ansible_facts.os_family == "RedHat" +- name: Check packages to be installed + block: + - name: "Check if packages are already installed" + ansible.builtin.command: "rpm -q {{ packages_list | join(' ') }}" + args: + warn: no + register: rpm_info + changed_when: rpm_info.failed -- name: "Add missing packages to the yum install list" - ansible.builtin.set_fact: - packages_to_install: "{{ packages_to_install | default([]) + rpm_info.stdout_lines | \ - map('regex_findall', 'package (.+) is not installed$') | default([]) | flatten }}" - when: ansible_facts.os_family == "RedHat" + rescue: + - name: "Add missing packages to the yum install list" + ansible.builtin.set_fact: + packages_to_install: "{{ packages_to_install | default([]) + rpm_info.stdout_lines | map('regex_findall', 'package (.+) is not installed$') | flatten }}" + when: rpm_info.failed - name: "Install packages: {{ packages_to_install }}" - become: true - ansible.builtin.dnf: + become: yes + ansible.builtin.yum: name: "{{ packages_to_install }}" state: present - when: - - packages_to_install | default([]) | length > 0 - - ansible_facts.os_family == "RedHat" - -- name: "Install packages: {{ packages_list }}" - become: true - ansible.builtin.package: - name: "{{ packages_list }}" - state: present - when: - - packages_list | default([]) | length > 0 - - ansible_facts.os_family == "Debian" + when: packages_to_install | default([]) | length > 0 \ No newline at end of file diff --git a/roles/keycloak/tasks/firewalld.yml b/roles/keycloak/tasks/firewalld.yml index f48f580..58a6cac 100644 --- a/roles/keycloak/tasks/firewalld.yml +++ b/roles/keycloak/tasks/firewalld.yml @@ -6,19 +6,19 @@ - firewalld - name: Enable and start the firewalld service - become: true + become: yes ansible.builtin.systemd: name: firewalld - enabled: true + enabled: yes state: started -- name: "Configure firewall ports for {{ keycloak.service_name }}" - become: true - ansible.posix.firewalld: +- name: "Configure firewall for {{ keycloak.service_name }} ports" + become: yes + firewalld: port: "{{ item }}" permanent: true state: enabled - immediate: true + immediate: yes loop: - "{{ keycloak_http_port }}/tcp" - "{{ keycloak_https_port }}/tcp" diff --git a/roles/keycloak/tasks/install.yml b/roles/keycloak/tasks/install.yml index b620b03..26c5466 100644 --- a/roles/keycloak/tasks/install.yml +++ b/roles/keycloak/tasks/install.yml @@ -11,56 +11,47 @@ quiet: true - name: Check for an existing deployment - become: true + become: yes ansible.builtin.stat: path: "{{ keycloak_jboss_home }}" register: existing_deploy - name: Stop and restart if existing deployment exists and install forced - when: existing_deploy.stat.exists and keycloak_force_install | bool block: - name: "Stop the old {{ keycloak.service_name }} service" - become: true - failed_when: false + become: yes + ignore_errors: yes ansible.builtin.systemd: name: keycloak state: stopped - name: "Remove the old {{ keycloak.service_name }} deployment" - become: true + become: yes ansible.builtin.file: path: "{{ keycloak_jboss_home }}" state: absent + when: existing_deploy.stat.exists and keycloak_force_install|bool - name: Check for an existing deployment after possible forced removal - become: true + become: yes ansible.builtin.stat: path: "{{ keycloak_jboss_home }}" -- name: "Create service user/group for {{ keycloak.service_name }}" - become: true +- name: "Create {{ keycloak.service_name }} service user/group" + become: yes ansible.builtin.user: name: "{{ keycloak_service_user }}" home: /opt/keycloak - system: true - create_home: false + system: yes + create_home: no -- name: "Create install location for {{ keycloak.service_name }}" - become: true +- name: "Create {{ keycloak.service_name }} install location" + become: yes ansible.builtin.file: dest: "{{ keycloak_dest }}" state: directory owner: "{{ keycloak_service_user }}" group: "{{ keycloak_service_group }}" - mode: '0750' - -- name: Create pidfile folder - become: 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' + mode: 0750 ## check remote archive - name: Set download archive path @@ -68,7 +59,7 @@ archive: "{{ keycloak_dest }}/{{ keycloak.bundle }}" - name: Check download archive path - become: true + become: yes ansible.builtin.stat: path: "{{ archive }}" register: archive_path @@ -84,68 +75,44 @@ ansible.builtin.get_url: # noqa risky-file-permissions delegated, uses controller host user url: "{{ keycloak_download_url }}" dest: "{{ local_path.stat.path }}/{{ keycloak.bundle }}" - mode: '0644' + mode: 0644 delegate_to: localhost - run_once: true when: - archive_path is defined - archive_path.stat is defined - not archive_path.stat.exists - - not sso_enable is defined or not sso_enable + - not keycloak_rhsso_enable - not keycloak_offline_install -- name: Perform download from RHN using JBoss Network API +- name: Perform download from RHN + middleware_automation.redhat_csp_download.redhat_csp_download: + url: "{{ keycloak_rhsso_download_url }}" + dest: "{{ local_path.stat.path }}/{{ keycloak.bundle }}" + username: "{{ rhn_username }}" + password: "{{ rhn_password }}" + no_log: "{{ omit_rhn_output | default(true) }}" delegate_to: localhost - run_once: true when: - archive_path is defined - archive_path.stat is defined - not archive_path.stat.exists - - sso_enable is defined and sso_enable + - keycloak_rhsso_enable - not keycloak_offline_install - block: - - name: Retrieve product download using JBoss Network 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 }}" - register: 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: - rhn_filtered_products: "{{ rhn_products.results | selectattr('file_path', 'match', '[^/]*/' + sso_archive + '$') }}" - 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: "{{ (rhn_filtered_products | first).id }}" - dest: "{{ local_path.stat.path }}/{{ keycloak.bundle }}" - no_log: "{{ omit_rhn_output | default(true) }}" - delegate_to: localhost - run_once: true + - keycloak_rhn_url in keycloak_rhsso_download_url - 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: "{{ local_path.stat.path }}/{{ keycloak.bundle }}" - mode: '0644' + mode: 0644 delegate_to: localhost - run_once: true when: - archive_path is defined - archive_path.stat is defined - not archive_path.stat.exists - - sso_enable is defined and sso_enable + - keycloak_rhsso_enable - not keycloak_offline_install - - keycloak_rhsso_download_url is defined + - not keycloak_rhn_url in keycloak_rhsso_download_url - name: Check downloaded archive ansible.builtin.stat: @@ -160,29 +127,29 @@ dest: "{{ archive }}" owner: "{{ keycloak_service_user }}" group: "{{ keycloak_service_group }}" - mode: '0640' + mode: 0640 register: new_version_downloaded when: - not archive_path.stat.exists - local_archive_path.stat is defined - local_archive_path.stat.exists - become: true + become: yes - name: "Check target directory: {{ keycloak.home }}" ansible.builtin.stat: path: "{{ keycloak.home }}" register: path_to_workdir - become: true + become: yes -- name: "Extract {{ keycloak_service_desc }} archive on target" +- name: "Extract {{ 'Red Hat Single Sign-On' if keycloak_rhsso_enable else 'Keycloak' }} archive on target" ansible.builtin.unarchive: - remote_src: true + remote_src: yes src: "{{ archive }}" dest: "{{ keycloak_dest }}" creates: "{{ keycloak.home }}" owner: "{{ keycloak_service_user }}" group: "{{ keycloak_service_group }}" - become: true + become: yes when: - new_version_downloaded.changed or not path_to_workdir.stat.exists notify: @@ -200,98 +167,43 @@ owner: "{{ keycloak_service_user }}" group: "{{ keycloak_service_group }}" recurse: true - become: true - changed_when: false - -- name: Ensure permissions are correct on existing deploy - ansible.builtin.command: chown -R "{{ keycloak_service_user }}:{{ keycloak_service_group }}" "{{ keycloak.home }}" - when: keycloak_service_runas - become: true + become: yes changed_when: false # driver and configuration - name: "Install {{ keycloak_jdbc_engine }} driver" - ansible.builtin.include_tasks: jdbc_driver.yml + ansible.builtin.include_role: + name: middleware_automation.wildfly.wildfly_driver + vars: + wildfly_user: "{{ keycloak_service_user }}" + jdbc_driver_module_dir: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_dir }}" + jdbc_driver_version: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_version }}" + jdbc_driver_jar_filename: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_jar_filename }}" + jdbc_driver_jar_url: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_jar_url }}" + jdbc_driver_jar_installation_path: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_dir }}/{{ keycloak_jdbc[keycloak_jdbc_engine].driver_jar_filename }}" + jdbc_driver_module_name: "{{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }}" when: keycloak_jdbc[keycloak_jdbc_engine].enabled -- name: "Deploy custom {{ keycloak.service_name }} config to {{ keycloak_config_path_to_standalone_xml }} from {{ keycloak_config_override_template }}" - become: true +- name: "Deploy {{ keycloak.service_name }} config to {{ keycloak_config_path_to_standalone_xml }} from {{ keycloak.config_template_source }}" + become: yes ansible.builtin.template: - src: "templates/{{ keycloak_config_override_template }}" + src: "templates/{{ keycloak.config_template_source }}" dest: "{{ keycloak_config_path_to_standalone_xml }}" owner: "{{ keycloak_service_user }}" group: "{{ keycloak_service_group }}" - mode: '0640' + mode: 0640 notify: - restart keycloak - when: keycloak_config_override_template | length > 0 + when: not keycloak_remotecache.enabled or keycloak_config_override_template|length > 0 -- name: "Deploy standalone {{ keycloak.service_name }} config to {{ keycloak_config_path_to_standalone_xml }}" - become: 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 | 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 {{ keycloak.service_name }} config to {{ keycloak_config_path_to_standalone_xml }}" - become: 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 | length == 0 - -- name: "Deploy HA {{ keycloak.service_name }} config with infinispan remote cache store to {{ keycloak_config_path_to_standalone_xml }}" - become: true +- name: "Deploy {{ keycloak.service_name }} config with remote cache store to {{ keycloak_config_path_to_standalone_xml }}" + become: yes 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' + mode: 0640 notify: - restart keycloak - when: - - keycloak_ha_enabled - - keycloak_remote_cache_enabled - - keycloak_config_override_template | length == 0 - -- name: "Deploy profile.properties file to {{ keycloak_config_path_to_properties }}" - become: 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 | length > 0 + when: keycloak_remotecache.enabled diff --git a/roles/keycloak/tasks/iptables.yml b/roles/keycloak/tasks/iptables.yml deleted file mode 100644 index 8ebc16e..0000000 --- a/roles/keycloak/tasks/iptables.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -- name: Ensure required package iptables are installed - ansible.builtin.include_tasks: fastpackages.yml - vars: - packages_list: - - iptables - -- name: "Configure firewall ports for {{ keycloak.service_name }}" - become: 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 }}" diff --git a/roles/keycloak/tasks/jdbc_driver.yml b/roles/keycloak/tasks/jdbc_driver.yml deleted file mode 100644 index bec80e3..0000000 --- a/roles/keycloak/tasks/jdbc_driver.yml +++ /dev/null @@ -1,46 +0,0 @@ ---- -- 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: dest_path - become: 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: 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: 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: true diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml index f826b63..ba5ec87 100644 --- a/roles/keycloak/tasks/main.yml +++ b/roles/keycloak/tasks/main.yml @@ -1,47 +1,30 @@ --- # tasks file for keycloak + - name: Check prerequisites - ansible.builtin.include_tasks: - file: prereqs.yml - apply: - tags: - - prereqs + ansible.builtin.include_tasks: prereqs.yml tags: - prereqs -- name: Distro specific tasks - ansible.builtin.include_tasks: - file: "{{ ansible_os_family | lower }}.yml" - apply: - tags: - - unbound +- name: Include firewall config tasks + ansible.builtin.include_tasks: firewalld.yml + when: keycloak_configure_firewalld tags: - - unbound + - firewall - name: Include install tasks - ansible.builtin.include_tasks: - file: install.yml - apply: - tags: - - install + ansible.builtin.include_tasks: install.yml tags: - install - name: Include systemd tasks - ansible.builtin.include_tasks: - file: systemd.yml - apply: - tags: - - systemd + ansible.builtin.include_tasks: systemd.yml 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" + when: keycloak_rhsso_apply_patches and keycloak_rhsso_enable tags: - install - patch @@ -50,8 +33,7 @@ ansible.builtin.file: state: link src: "{{ keycloak_jboss_home }}/standalone/log" - dest: "{{ keycloak_log_target }}" - become: true + dest: /var/log/keycloak - name: Set admin credentials and restart if not already created block: @@ -60,7 +42,7 @@ 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: false + validate_certs: no register: keycloak_auth_response until: keycloak_auth_response.status == 200 retries: 2 @@ -74,8 +56,8 @@ - "-rmaster" - "-u{{ keycloak_admin_user }}" - "-p{{ keycloak_admin_password }}" - changed_when: true - become: true + changed_when: yes + become: yes - name: "Restart {{ keycloak.service_name }}" ansible.builtin.include_tasks: tasks/restart_keycloak.yml - name: "Wait until {{ keycloak.service_name }} becomes active {{ keycloak.health_url }}" diff --git a/roles/keycloak/tasks/prereqs.yml b/roles/keycloak/tasks/prereqs.yml index d97390c..31735d5 100644 --- a/roles/keycloak/tasks/prereqs.yml +++ b/roles/keycloak/tasks/prereqs.yml @@ -3,56 +3,44 @@ ansible.builtin.assert: that: - keycloak_admin_password | 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 + 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 + 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 + - (rhn_username is defined and keycloak_rhsso_enable) or not keycloak_rhsso_enable or keycloak_offline_install + - (rhn_password is defined and keycloak_rhsso_enable) or not keycloak_rhsso_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 }}" + success_msg: "{{ 'Installing Red Hat Single Sign-On' if keycloak_rhsso_enable else 'Installing keycloak.org' }}" - name: Validate persistence configuration ansible.builtin.assert: that: - - keycloak_jdbc_engine is defined and keycloak_jdbc_engine in [ 'postgres', 'mariadb', 'sqlserver' ] + - keycloak_jdbc_engine is defined and keycloak_jdbc_engine in [ 'postgres', 'mariadb' ] - keycloak_jdbc_url | length > 0 - keycloak_db_user | length > 0 - keycloak_db_pass | length > 0 - quiet: true + 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 }}" + packages_list: + - "{{ keycloak_jvm_package }}" + - unzip + - procps-ng + - initscripts \ No newline at end of file diff --git a/roles/keycloak/tasks/redhat.yml b/roles/keycloak/tasks/redhat.yml deleted file mode 100644 index ece5772..0000000 --- a/roles/keycloak/tasks/redhat.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- name: Include firewall config tasks - ansible.builtin.include_tasks: - file: firewalld.yml - apply: - tags: - - firewall - when: keycloak_configure_firewalld - tags: - - firewall diff --git a/roles/keycloak/tasks/restart_keycloak.yml b/roles/keycloak/tasks/restart_keycloak.yml index 7284bd0..eff9ddf 100644 --- a/roles/keycloak/tasks/restart_keycloak.yml +++ b/roles/keycloak/tasks/restart_keycloak.yml @@ -2,27 +2,6 @@ - name: "Restart and enable {{ keycloak.service_name }} service" ansible.builtin.systemd: name: keycloak - enabled: true + enabled: yes state: restarted - daemon_reload: true - become: true - delegate_to: "{{ ansible_play_hosts | first }}" - run_once: true - -- name: "Wait until {{ keycloak.service_name }} becomes active {{ keycloak.health_url }}" - ansible.builtin.uri: - url: "{{ keycloak.health_url }}" - 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 {{ keycloak.service_name }} service" - ansible.builtin.systemd: - name: keycloak - enabled: true - state: restarted - become: true - when: inventory_hostname != ansible_play_hosts | first + become: yes diff --git a/roles/keycloak/tasks/rhsso_cli.yml b/roles/keycloak/tasks/rhsso_cli.yml index e40dec8..c51cdc7 100644 --- a/roles/keycloak/tasks/rhsso_cli.yml +++ b/roles/keycloak/tasks/rhsso_cli.yml @@ -2,12 +2,12 @@ - name: Ensure required params for CLI have been provided ansible.builtin.assert: that: - - cli_query is defined + - query is defined fail_msg: "Missing required parameters to execute CLI." quiet: true -- name: "Execute CLI query: {{ cli_query }}" +- name: "Execute CLI query: {{ query }}" ansible.builtin.command: > - {{ keycloak.cli_path }} --connect --command='{{ cli_query }}' --controller={{ keycloak_host }}:{{ keycloak_management_http_port }} + {{ keycloak.cli_path }} --connect --command='{{ query }}' --controller={{ keycloak_host }}:{{ keycloak_management_http_port }} changed_when: false - register: cli_result + register: cli_result \ No newline at end of file diff --git a/roles/keycloak/tasks/rhsso_patch.yml b/roles/keycloak/tasks/rhsso_patch.yml index 23d75bf..f517e7a 100644 --- a/roles/keycloak/tasks/rhsso_patch.yml +++ b/roles/keycloak/tasks/rhsso_patch.yml @@ -2,141 +2,65 @@ ## check remote patch archive - name: Set download patch archive path ansible.builtin.set_fact: - patch_archive: "{{ keycloak_dest }}/{{ sso_patch_bundle }}" - patch_bundle: "{{ sso_patch_bundle }}" - patch_version: "{{ sso_patch_version }}" - when: sso_patch_version is defined + patch_archive: "{{ keycloak_dest }}/{{ keycloak.patch_bundle }}" - name: Check download patch archive path ansible.builtin.stat: path: "{{ patch_archive }}" register: patch_archive_path - when: sso_patch_version is defined - become: true -- name: Perform patch download from RHN via JBossNetwork API +- name: Perform download from RHN + middleware_automation.redhat_csp_download.redhat_csp_download: + url: "{{ keycloak_rhn_url }}{{ rhsso_rhn_ids[keycloak_rhsso_version].latest_cp.id }}" + dest: "{{ local_path.stat.path }}/{{ keycloak.patch_bundle }}" + username: "{{ rhn_username }}" + password: "{{ rhn_password }}" + no_log: "{{ omit_rhn_output | default(true) }}" delegate_to: localhost - run_once: true when: - - sso_enable is defined and sso_enable + - patch_archive_path is defined + - patch_archive_path.stat is defined + - not patch_archive_path.stat.exists + - keycloak_rhsso_enable - not keycloak_offline_install - - sso_apply_patches - block: - - name: Retrieve product download using JBossNetwork 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 }}" - register: rhn_products - no_log: "{{ omit_rhn_output | default(true) }}" - delegate_to: localhost - run_once: true - - - name: Determine patch versions list - ansible.builtin.set_fact: - filtered_versions: "{{ rhn_products.results | map(attribute='file_path') | \ - 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 is not defined or sso_patch_version | length == 0 - delegate_to: localhost - run_once: true - - - name: Determine latest version - ansible.builtin.set_fact: - sso_latest_version: "{{ filtered_versions | middleware_automation.common.version_sort | last }}" - when: sso_patch_version is not defined or sso_patch_version | length == 0 - delegate_to: localhost - run_once: true - - - name: Determine install zipfile from search results - ansible.builtin.set_fact: - rhn_filtered_products: "{{ rhn_products.results | selectattr('file_path', 'match', '[^/]*/rh-sso-' + sso_latest_version + '-patch.zip$') }}" - patch_bundle: "rh-sso-{{ sso_latest_version }}-patch.zip" - patch_version: "{{ sso_latest_version }}" - when: sso_patch_version is not defined or sso_patch_version | length == 0 - delegate_to: localhost - run_once: true - - - name: "Determine selected patch from supplied version: {{ sso_patch_version }}" - ansible.builtin.set_fact: - rhn_filtered_products: "{{ rhn_products.results | selectattr('file_path', 'match', '[^/]*/' + sso_patch_bundle + '$') }}" - patch_bundle: "{{ sso_patch_bundle }}" - 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: "{{ (rhn_filtered_products | sort | last).id }}" - dest: "{{ local_path.stat.path }}/{{ patch_bundle }}" - no_log: "{{ omit_rhn_output | default(true) }}" - delegate_to: localhost - run_once: true - -- name: Set download patch archive path - ansible.builtin.set_fact: - patch_archive: "{{ keycloak_dest }}/{{ patch_bundle }}" - -- name: Check download patch archive path - ansible.builtin.stat: - path: "{{ patch_archive }}" - register: patch_archive_path - become: true ## copy and unpack - name: Copy patch archive to target nodes ansible.builtin.copy: - src: "{{ local_path.stat.path }}/{{ patch_bundle }}" + src: "{{ local_path.stat.path }}/{{ keycloak.patch_bundle }}" dest: "{{ patch_archive }}" owner: "{{ keycloak_service_user }}" group: "{{ keycloak_service_group }}" - mode: '0640' + mode: 0640 register: new_version_downloaded when: - not patch_archive_path.stat.exists - local_archive_path.stat is defined - local_archive_path.stat.exists - become: true + become: yes - name: "Check installed patches" ansible.builtin.include_tasks: rhsso_cli.yml vars: - cli_query: "patch info" - args: - apply: - become: true - become_user: "{{ keycloak_service_user }}" + query: "patch info" - name: "Perform patching" - when: + when: - cli_result is defined - cli_result.stdout is defined - - patch_version | regex_replace('-[0-9]$', '') not in cli_result.stdout + - rhsso_rhn_ids[keycloak_rhsso_version].latest_cp.v not in cli_result.stdout block: - - name: "Apply patch {{ patch_version }} to server" + - name: "Apply patch {{ rhsso_rhn_ids[keycloak_rhsso_version].latest_cp.v }} to server" ansible.builtin.include_tasks: rhsso_cli.yml vars: - cli_query: "patch apply {{ patch_archive }}" - args: - apply: - become: true - become_user: "{{ keycloak_service_user }}" + query: "patch apply {{ patch_archive }}" - name: "Restart server to ensure patch content is running" ansible.builtin.include_tasks: rhsso_cli.yml vars: - cli_query: "shutdown --restart" + query: "shutdown --restart" when: - cli_result.rc == 0 - args: - apply: - become: true - become_user: "{{ keycloak_service_user }}" - name: "Wait until {{ keycloak.service_name }} becomes active {{ keycloak.health_url }}" ansible.builtin.uri: @@ -149,23 +73,15 @@ - name: "Query installed patch after restart" ansible.builtin.include_tasks: rhsso_cli.yml vars: - cli_query: "patch info" - args: - apply: - become: true - become_user: "{{ keycloak_service_user }}" - + query: "patch info" + - name: "Verify installed patch version" ansible.builtin.assert: that: - - patch_version not in cli_result.stdout + - rhsso_rhn_ids[keycloak_rhsso_version].latest_cp.v not in cli_result.stdout fail_msg: "Patch installation failed" success_msg: "Patch installation successful" - name: "Skipping patch" ansible.builtin.debug: - msg: "Cumulative patch {{ patch_version }} already installed, skipping patch installation." - when: - - cli_result is defined - - cli_result.stdout is defined - - patch_version in cli_result.stdout + msg: "Latest cumulative patch {{ rhsso_rhn_ids[keycloak_rhsso_version].latest_cp.v }} already installed, skipping patch installation." diff --git a/roles/keycloak/tasks/start_keycloak.yml b/roles/keycloak/tasks/start_keycloak.yml index 5aed248..bdf42f9 100644 --- a/roles/keycloak/tasks/start_keycloak.yml +++ b/roles/keycloak/tasks/start_keycloak.yml @@ -2,15 +2,14 @@ - name: "Start {{ keycloak.service_name }} service" ansible.builtin.systemd: name: keycloak - enabled: true + enabled: yes state: started - daemon_reload: true - become: true + become: yes - name: "Wait until {{ keycloak.service_name }} becomes active {{ keycloak.health_url }}" ansible.builtin.uri: url: "{{ keycloak.health_url }}" register: keycloak_status until: keycloak_status.status == 200 - retries: "{{ keycloak_service_start_retries }}" - delay: "{{ keycloak_service_start_delay }}" + retries: 25 + delay: 10 \ No newline at end of file diff --git a/roles/keycloak/tasks/stop_keycloak.yml b/roles/keycloak/tasks/stop_keycloak.yml index 7f30433..fd87802 100644 --- a/roles/keycloak/tasks/stop_keycloak.yml +++ b/roles/keycloak/tasks/stop_keycloak.yml @@ -2,6 +2,6 @@ - name: "Stop {{ keycloak.service_name }}" ansible.builtin.systemd: name: keycloak - enabled: true + enabled: yes state: stopped - become: true + become: yes diff --git a/roles/keycloak/tasks/systemd.yml b/roles/keycloak/tasks/systemd.yml index 1653406..871180f 100644 --- a/roles/keycloak/tasks/systemd.yml +++ b/roles/keycloak/tasks/systemd.yml @@ -1,23 +1,34 @@ --- - name: "Configure {{ keycloak.service_name }} service script wrapper" - become: true + become: yes ansible.builtin.template: src: keycloak-service.sh.j2 dest: "{{ keycloak_dest }}/keycloak-service.sh" owner: root group: root - mode: '0755' + mode: 0755 notify: - restart keycloak +- name: Determine JAVA_HOME for selected JVM RPM # noqa blocked_modules + ansible.builtin.shell: | + set -o pipefail + rpm -ql {{ keycloak_jvm_package }} | grep -Po '/usr/lib/jvm/.*(?=/bin/java$)' + args: + executable: /bin/bash + changed_when: False + register: rpm_java_home + - name: "Configure sysconfig file for {{ keycloak.service_name }} service" - become: true + become: yes ansible.builtin.template: src: keycloak-sysconfig.j2 - dest: "{{ keycloak_sysconf_file }}" + dest: /etc/sysconfig/keycloak owner: root group: root - mode: '0644' + mode: 0644 + vars: + keycloak_rpm_java_home: "{{ rpm_java_home.stdout }}" notify: - restart keycloak @@ -27,15 +38,21 @@ dest: /etc/systemd/system/keycloak.service owner: root group: root - mode: '0644' - become: true + mode: 0644 + become: yes register: systemdunit notify: - restart keycloak +- name: Reload systemd + become: yes + ansible.builtin.systemd: + daemon_reload: yes + when: systemdunit.changed + - name: "Start and wait for {{ keycloak.service_name }} service (first node db)" ansible.builtin.include_tasks: start_keycloak.yml - run_once: true + run_once: yes when: keycloak_db_enabled - name: "Start and wait for {{ keycloak.service_name }} service (remaining nodes)" @@ -44,7 +61,7 @@ - name: Check service status ansible.builtin.command: "systemctl status keycloak" register: keycloak_service_status - changed_when: false + changed_when: False - name: Verify service status ansible.builtin.assert: diff --git a/roles/keycloak/templates/15.0.8/standalone-infinispan.xml.j2 b/roles/keycloak/templates/15.0.8/standalone-infinispan.xml.j2 deleted file mode 100644 index 25d6cb0..0000000 --- a/roles/keycloak/templates/15.0.8/standalone-infinispan.xml.j2 +++ /dev/null @@ -1,761 +0,0 @@ - -{{ ansible_managed | comment('xml') }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE - h2 - - sa - sa - - - -{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %} - {{ keycloak_jdbc[keycloak_jdbc_engine].connection_url }} - {{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }} - - 20 - - - {{ keycloak_jdbc[keycloak_jdbc_engine].db_user }} - {{ keycloak_jdbc[keycloak_jdbc_engine].db_password }} - -{% else %} - jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE - h2 - - sa - sa - -{% endif %} - - -{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %} - - {{ keycloak_jdbc[keycloak_jdbc_engine].driver_class }} - {{ keycloak_jdbc[keycloak_jdbc_engine].xa_datasource_class }} - -{% endif %} - - org.h2.jdbcx.JdbcDataSource - - - - - - - - - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{% for cachename in [ "sessions", "offlineSessions", "clientSessions", "offlineClientSessions", "loginFailures", "actionTokens", "authenticationSessions" ] %} - - - true - org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory - false - {{ keycloak_remotecache.username }} - {{ keycloak_remotecache.password }} - {{ keycloak_remotecache.realm | default('default') }} - {{ keycloak_remotecache.server_name }} - {{ keycloak_remotecache.sasl_mechanism }} - {{ keycloak_remotecache.use_ssl }} - {{ keycloak_remotecache.trust_store_path }} - JKS - {{ keycloak_remotecache.trust_store_password }} - TOPOLOGY_AWARE - - -{% endfor %} - - - true - org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory - false - {{ keycloak_remotecache.username }} - {{ keycloak_remotecache.password }} - {{ keycloak_remotecache.realm | default('default') }} - {{ keycloak_remotecache.server_name }} - {{ keycloak_remotecache.sasl_mechanism }} - {{ keycloak_remotecache.use_ssl }} - {{ keycloak_remotecache.trust_store_path }} - JKS - {{ keycloak_remotecache.trust_store_password }} - TOPOLOGY_AWARE - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %} - - java:jboss/datasources/KeycloakDS - {{ keycloak_jdbc[keycloak_jdbc_engine].initialize_db }} - INSERT INTO JGROUPSPING (own_addr, cluster_name, ping_data) values (?, ?, ?) - DELETE FROM JGROUPSPING WHERE own_addr=? AND cluster_name=? - SELECT ping_data FROM JGROUPSPING WHERE cluster_name=? - -{% endif %} - - - - - - - - - 30000 - - - - - - - - - - - - - - - - auth - - - classpath:${jboss.home.dir}/providers/* - - - master - 900 - - 2592000 - true - true - ${jboss.home.dir}/themes - -{% if keycloak_ha_enabled %} - - - - - - - -{% endif %} - - - - - - - - - - - - jpa - - - basic - - - - - - - - - - - - - - - - - - - default - - - - - - - - ${keycloak.jta.lookup.provider:jboss} - - - - - - - - - - - ${keycloak.x509cert.lookup.provider:default} - - - - default - - - - - - - - - - - - - - -{% if keycloak_modcluster.enabled %} - - - - - - - -{% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{% if ansible_default_ipv4 is defined %} - -{% else %} - -{% endif %} - - - - - - - - - - - - - - - - - -{% if keycloak_modcluster.enabled %} - - - -{% endif %} - - - - - diff --git a/roles/keycloak/templates/15.0.8/standalone.xml.j2 b/roles/keycloak/templates/15.0.8/standalone.xml.j2 deleted file mode 100644 index 01c317b..0000000 --- a/roles/keycloak/templates/15.0.8/standalone.xml.j2 +++ /dev/null @@ -1,658 +0,0 @@ - -{{ ansible_managed | comment('xml') }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE - h2 - - sa - sa - - - -{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %} - {{ keycloak_jdbc[keycloak_jdbc_engine].connection_url }} - {{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }} - - 20 - - - {{ keycloak_jdbc[keycloak_jdbc_engine].db_user }} - {{ keycloak_jdbc[keycloak_jdbc_engine].db_password }} - -{% else %} - jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE - h2 - - sa - sa - -{% endif %} - - -{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %} - - {{ keycloak_jdbc[keycloak_jdbc_engine].driver_class }} - {{ keycloak_jdbc[keycloak_jdbc_engine].xa_datasource_class }} - -{% endif %} - - org.h2.jdbcx.JdbcDataSource - - - - - - - - - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - auth - - - classpath:${jboss.home.dir}/providers/* - - - master - 900 - - 2592000 - true - true - ${jboss.home.dir}/themes - -{% if keycloak_ha_enabled %} - - - - - - - -{% endif %} - - - - - - - - - - - - jpa - - - basic - - - - - - - - - - - - - - - - - - - default - - - - - - - - ${keycloak.jta.lookup.provider:jboss} - - - - - - - - - - - ${keycloak.x509cert.lookup.provider:default} - - - - default - - - - - - - - - - - - - - -{% if keycloak_modcluster.enabled %} - - - - - - - -{% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{% if keycloak_modcluster.enabled %} - - - -{% endif %} - - diff --git a/roles/keycloak/templates/9.0.2/standalone-infinispan.xml.j2 b/roles/keycloak/templates/9.0.2/standalone-infinispan.xml.j2 index 4f90ad8..2b2842b 100644 --- a/roles/keycloak/templates/9.0.2/standalone-infinispan.xml.j2 +++ b/roles/keycloak/templates/9.0.2/standalone-infinispan.xml.j2 @@ -725,7 +725,7 @@ {% if ansible_default_ipv4 is defined %} - + {% else %} {% endif %} @@ -734,7 +734,7 @@ - + diff --git a/roles/keycloak/templates/9.0.2/standalone.xml.j2 b/roles/keycloak/templates/9.0.2/standalone.xml.j2 index 4188e92..823357f 100644 --- a/roles/keycloak/templates/9.0.2/standalone.xml.j2 +++ b/roles/keycloak/templates/9.0.2/standalone.xml.j2 @@ -598,7 +598,7 @@ - + diff --git a/roles/keycloak/templates/jdbc_driver_module.xml.j2 b/roles/keycloak/templates/jdbc_driver_module.xml.j2 deleted file mode 100644 index c513693..0000000 --- a/roles/keycloak/templates/jdbc_driver_module.xml.j2 +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/roles/keycloak/templates/keycloak-profile.properties.j2 b/roles/keycloak/templates/keycloak-profile.properties.j2 deleted file mode 100644 index c618dc2..0000000 --- a/roles/keycloak/templates/keycloak-profile.properties.j2 +++ /dev/null @@ -1,3 +0,0 @@ -{% for feature in keycloak.features %} -feature.{{ feature.name }}={{ feature.status | default('enabled') }} -{% endfor %} \ No newline at end of file diff --git a/roles/keycloak/templates/keycloak-service.sh.j2 b/roles/keycloak/templates/keycloak-service.sh.j2 index 98efb34..577959e 100755 --- a/roles/keycloak/templates/keycloak-service.sh.j2 +++ b/roles/keycloak/templates/keycloak-service.sh.j2 @@ -1,5 +1,5 @@ #!/bin/bash -eu -{{ ansible_managed | comment }} +# {{ ansible_managed }} set +u -o pipefail diff --git a/roles/keycloak/templates/keycloak-sysconfig.j2 b/roles/keycloak/templates/keycloak-sysconfig.j2 index 33889df..68474c3 100644 --- a/roles/keycloak/templates/keycloak-sysconfig.j2 +++ b/roles/keycloak/templates/keycloak-sysconfig.j2 @@ -1,6 +1,6 @@ -{{ ansible_managed | comment }} +# {{ ansible_managed }} JAVA_OPTS='{{ keycloak_java_opts }}' -JAVA_HOME={{ keycloak_java_home | default(keycloak_pkg_java_home, true) }} +JAVA_HOME={{ keycloak_java_home | default(keycloak_rpm_java_home, true) }} JBOSS_HOME={{ keycloak.home }} KEYCLOAK_BIND_ADDRESS={{ keycloak_bind_address }} KEYCLOAK_HTTP_PORT={{ keycloak_http_port }} @@ -8,12 +8,4 @@ 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 %} +LAUNCH_JBOSS_IN_BACKGROUND=1 \ No newline at end of file diff --git a/roles/keycloak/templates/keycloak.service.j2 b/roles/keycloak/templates/keycloak.service.j2 index 9a04e88..0fcecbf 100644 --- a/roles/keycloak/templates/keycloak.service.j2 +++ b/roles/keycloak/templates/keycloak.service.j2 @@ -1,29 +1,17 @@ -{{ ansible_managed | comment }} +# {{ ansible_managed }} [Unit] Description={{ keycloak.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 }} +Type=forking +EnvironmentFile=-/etc/sysconfig/keycloak PIDFile={{ keycloak_service_pidfile }} -ExecStart={{ keycloak.home }}/bin/standalone.sh $WILDFLY_OPTS -WorkingDirectory={{ keycloak.home }} +ExecStart={{ keycloak_dest }}/keycloak-service.sh start +ExecStop={{ keycloak_dest }}/keycloak-service.sh stop 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 diff --git a/roles/keycloak/templates/standalone-ha.xml.j2 b/roles/keycloak/templates/standalone-ha.xml.j2 deleted file mode 100644 index d027c35..0000000 --- a/roles/keycloak/templates/standalone-ha.xml.j2 +++ /dev/null @@ -1,707 +0,0 @@ - -{{ ansible_managed | comment('xml') }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE - h2 - - sa - sa - - - -{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %} - {{ keycloak_jdbc[keycloak_jdbc_engine].connection_url }} - {{ keycloak_jdbc[keycloak_jdbc_engine].driver_module_name }} - - 20 - - - {{ keycloak_jdbc[keycloak_jdbc_engine].db_user }} - {{ keycloak_jdbc[keycloak_jdbc_engine].db_password }} - - - {{ keycloak_jdbc[keycloak_jdbc_engine].validate_query }} - {{ keycloak_db_background_validate_on_match }} -{% if keycloak_db_background_validation_millis | int > 0 or keycloak_db_background_validation %} - {{ keycloak_db_background_validation }} - {{ keycloak_db_background_validation_millis }} -{% endif %} - -{% else %} - jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE - h2 - - sa - sa - -{% endif %} - - -{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %} - - {{ keycloak_jdbc[keycloak_jdbc_engine].driver_class }} - {{ keycloak_jdbc[keycloak_jdbc_engine].xa_datasource_class }} - -{% endif %} - - org.h2.jdbcx.JdbcDataSource - - - - - - - - - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{% if keycloak_ha_discovery == 'JDBC_PING' and keycloak_jdbc[keycloak_jdbc_engine].enabled %} - - java:jboss/datasources/KeycloakDS - {{ keycloak_jdbc[keycloak_jdbc_engine].initialize_db }} - INSERT INTO JGROUPSPING (own_addr, cluster_name, ping_data) values (?, ?, ?) - DELETE FROM JGROUPSPING WHERE own_addr=? AND cluster_name=? - SELECT ping_data FROM JGROUPSPING WHERE cluster_name=? - -{% elif keycloak_ha_discovery == 'TCPPING' %} - - {{ keycloak_cluster_nodes | map(attribute='inventory_host') | join (',') }} - 0 - 3000 - 2 - -{% endif %} - - - - - - - - - 30000 - - - - - - - - - - - - - - - - auth - - - classpath:${jboss.home.dir}/providers/* - - - master - 900 - - 2592000 - true - true - ${jboss.home.dir}/themes - -{% if keycloak_ha_enabled %} - - - - - - - -{% endif %} - - - - - - - - - - - - jpa - - - basic - - - - - - - - - - - - - - - - - - - default - - - - - - - - ${keycloak.jta.lookup.provider:jboss} - - - - - - - - - - - ${keycloak.x509cert.lookup.provider:default} - - - - default - - - - -{% if keycloak_modcluster.admin_url | length > 0 %} - -{% endif %} - - - - - - - - - - -{% if keycloak_modcluster.enabled %} - - - - - - - -{% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{% if keycloak_jgroups_subnet is defined and keycloak_jgroups_subnet is not none and keycloak_jgroups_subnet | string | length > 0 %} - -{% elif ansible_default_ipv4 is defined and (ansible_default_ipv4.network + '/' + ansible_default_ipv4.netmask) | ansible.utils.ipaddr('net') | length > 0 %} - -{% else %} - -{% endif %} - - - - - - - - - - - - - - - - - -{% if keycloak_modcluster.enabled %} -{% for modcluster in keycloak_modcluster.reverse_proxy_urls %} - - - -{% endfor %} -{% endif %} -{% if keycloak_ha_discovery == 'TCPPING' %} -{% for node in keycloak_cluster_nodes %} - - - -{% endfor %} -{% endif %} - - - - - diff --git a/roles/keycloak/templates/standalone-infinispan.xml.j2 b/roles/keycloak/templates/standalone-infinispan.xml.j2 index 18e5a7c..8e58b53 100644 --- a/roles/keycloak/templates/standalone-infinispan.xml.j2 +++ b/roles/keycloak/templates/standalone-infinispan.xml.j2 @@ -1,5 +1,5 @@ -{{ ansible_managed | comment('xml') }} + @@ -16,6 +16,7 @@ + @@ -30,6 +31,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -44,8 +70,8 @@ - - + + @@ -136,14 +162,6 @@ {{ keycloak_jdbc[keycloak_jdbc_engine].db_user }} {{ keycloak_jdbc[keycloak_jdbc_engine].db_password }} - - {{ keycloak_jdbc[keycloak_jdbc_engine].validate_query }} - {{ keycloak_db_background_validate_on_match }} -{% if keycloak_db_background_validation_millis | int > 0 or keycloak_db_background_validation %} - {{ keycloak_db_background_validation }} - {{ keycloak_db_background_validation_millis }} -{% endif %} - {% else %} jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE h2 @@ -226,9 +244,6 @@ - - - @@ -302,13 +317,6 @@ - - - - - - - @@ -496,7 +504,7 @@ -{% if keycloak_ha_discovery == 'JDBC_PING' and keycloak_jdbc[keycloak_jdbc_engine].enabled %} +{% if keycloak_jdbc[keycloak_jdbc_engine].enabled %} java:jboss/datasources/KeycloakDS {{ keycloak_jdbc[keycloak_jdbc_engine].initialize_db }} @@ -504,13 +512,6 @@ DELETE FROM JGROUPSPING WHERE own_addr=? AND cluster_name=? SELECT ping_data FROM JGROUPSPING WHERE cluster_name=? -{% elif keycloak_ha_discovery == 'TCPPING' %} - - {{ keycloak_cluster_nodes | map(attribute='inventory_host') | join (',') }} - 0 - 3000 - 2 - {% endif %} @@ -619,10 +620,7 @@ - -{% if keycloak_modcluster.admin_url | length > 0 %} - -{% endif %} + @@ -633,22 +631,54 @@ -{% if keycloak_modcluster.enabled %} +{% if keycloak_modcluster.enabled %} - + -{% endif %} +{% endif %} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -671,10 +701,10 @@ - + - + @@ -685,9 +715,6 @@ - - - @@ -697,22 +724,20 @@ - + -{% if keycloak_jgroups_subnet is defined and keycloak_jgroups_subnet is not none and keycloak_jgroups_subnet | string | length > 0 %} - -{% elif ansible_default_ipv4 is defined and (ansible_default_ipv4.network + '/' + ansible_default_ipv4.netmask) | ansible.utils.ipaddr('net') | length > 0 %} - +{% if ansible_default_ipv4 is defined %} + {% else %} {% endif %} - + - + @@ -725,18 +750,9 @@ {% if keycloak_modcluster.enabled %} -{% for modcluster in keycloak_modcluster.reverse_proxy_urls %} - - + + -{% endfor %} -{% endif %} -{% if keycloak_ha_discovery == 'TCPPING' %} -{% for node in keycloak_cluster_nodes %} - - - -{% endfor %} {% endif %} diff --git a/roles/keycloak/templates/standalone.xml.j2 b/roles/keycloak/templates/standalone.xml.j2 index 6c3c0f8..3207ef9 100644 --- a/roles/keycloak/templates/standalone.xml.j2 +++ b/roles/keycloak/templates/standalone.xml.j2 @@ -1,5 +1,5 @@ -{{ ansible_managed | comment('xml') }} + @@ -15,6 +15,7 @@ + @@ -29,6 +30,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -43,8 +69,8 @@ - - + + @@ -123,14 +149,6 @@ {{ keycloak_jdbc[keycloak_jdbc_engine].db_user }} {{ keycloak_jdbc[keycloak_jdbc_engine].db_password }} - - {{ keycloak_jdbc[keycloak_jdbc_engine].validate_query }} - {{ keycloak_db_background_validate_on_match }} -{% if keycloak_db_background_validation_millis | int > 0 or keycloak_db_background_validation %} - {{ keycloak_db_background_validation }} - {{ keycloak_db_background_validation_millis }} -{% endif %} - {% else %} jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE h2 @@ -213,9 +231,6 @@ - - - @@ -289,13 +304,6 @@ - - - - - - - @@ -525,10 +533,7 @@ - -{% if keycloak_modcluster.admin_url | length > 0 %} - -{% endif %} + @@ -539,22 +544,54 @@ -{% if keycloak_modcluster.enabled %} +{% if keycloak_modcluster.enabled %} - + -{% endif %} +{% endif %} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -577,10 +614,10 @@ - + - + @@ -590,21 +627,18 @@ - - - - + - + - + @@ -615,12 +649,10 @@ -{% if keycloak_modcluster.enabled %} -{% for modcluster in keycloak_modcluster.reverse_proxy_urls %} - - +{% if keycloak_modcluster.enabled %} + + -{% endfor %} -{% endif %} +{% endif %} diff --git a/roles/keycloak/vars/debian.yml b/roles/keycloak/vars/debian.yml deleted file mode 100644 index b005b0a..0000000 --- a/roles/keycloak/vars/debian.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -keycloak_varjvm_package: "{{ keycloak_jvm_package | default('openjdk-11-jdk-headless') }}" -keycloak_prereq_package_list: - - "{{ keycloak_varjvm_package }}" - - unzip - - procps - - apt - - tzdata -keycloak_configure_iptables: true -keycloak_sysconf_file: /etc/default/keycloak -keycloak_pkg_java_home: "/usr/lib/jvm/java-{{ keycloak_varjvm_package | \ - regex_search('(?!:openjdk-)[0-9.]+') }}-openjdk-{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}" diff --git a/roles/keycloak/vars/main.yml b/roles/keycloak/vars/main.yml index fe706db..5a6b059 100644 --- a/roles/keycloak/vars/main.yml +++ b/roles/keycloak/vars/main.yml @@ -1,18 +1,26 @@ --- # internal variables below +rhsso_rhn_ids: + '7.5.0': # noqa vars_in_vars_files_have_valid_names + id: '101971' + latest_cp: + id: '103836' + v: '7.5.1' +# locations +keycloak_url: "http://{{ keycloak_host }}:{{ keycloak_http_port }}" +keycloak_management_url: "http://{{ keycloak_host }}:{{ keycloak_management_http_port }}" keycloak: home: "{{ keycloak_jboss_home }}" config_dir: "{{ keycloak_config_dir }}" - bundle: "{{ keycloak_archive }}" - service_name: "{{ keycloak_service_name }}" + bundle: "{{ keycloak_rhsso_archive if keycloak_rhsso_enable else keycloak_archive }}" + patch_bundle: "rh-sso-{{ rhsso_rhn_ids[keycloak_rhsso_version].latest_cp.v }}-patch.zip" + service_name: "{{ 'rhsso' if keycloak_rhsso_enable else 'keycloak' }}" health_url: "{{ keycloak_management_url }}/health" cli_path: "{{ keycloak_jboss_home }}/bin/jboss-cli.sh" - config_template_source: "{{ keycloak_config_override_template if keycloak_config_override_template | length > 0 \ - else 'standalone-ha.xml.j2' if keycloak_remote_cache_enabled else 'standalone.xml.j2' }}" - features: "{{ keycloak_features }}" + config_template_source: "{{ keycloak_config_override_template if keycloak_config_override_template | length > 0 else 'standalone.xml.j2' }}" # database keycloak_jdbc: @@ -24,12 +32,10 @@ keycloak_jdbc: driver_module_dir: "{{ keycloak_jboss_home }}/modules/org/postgresql/main" driver_version: "{{ keycloak_jdbc_driver_version }}" driver_jar_filename: "postgresql-{{ keycloak_jdbc_driver_version }}.jar" - driver_jar_url: > - {{ keycloak_maven_central }}org/postgresql/postgresql/{{ keycloak_jdbc_driver_version }}/postgresql-{{ keycloak_jdbc_driver_version }}.jar + driver_jar_url: "https://repo.maven.apache.org/maven2/org/postgresql/postgresql/{{ keycloak_jdbc_driver_version }}/postgresql-{{ keycloak_jdbc_driver_version }}.jar" connection_url: "{{ keycloak_jdbc_url }}" db_user: "{{ keycloak_db_user }}" db_password: "{{ keycloak_db_pass }}" - validate_query: "{{ keycloak_db_valid_conn_sql | default('select 1') }}" initialize_db: > CREATE TABLE IF NOT EXISTS JGROUPSPING ( own_addr varchar(200) NOT NULL, @@ -40,17 +46,15 @@ keycloak_jdbc: mariadb: enabled: "{{ (keycloak_ha_enabled or keycloak_db_enabled) and keycloak_jdbc_engine == 'mariadb' }}" driver_class: org.mariadb.jdbc.Driver - xa_datasource_class: org.mariadb.jdbc.MariaDbDataSource + xa_datasource_class: org.mariadb.jdbc.MySQLDataSource driver_module_name: "org.mariadb" driver_module_dir: "{{ keycloak_jboss_home }}/modules/org/mariadb/main" driver_version: "{{ keycloak_jdbc_driver_version }}" driver_jar_filename: "mariadb-java-client-{{ keycloak_jdbc_driver_version }}.jar" - driver_jar_url: > - {{ keycloak_maven_central }}org/mariadb/jdbc/mariadb-java-client/{{ keycloak_jdbc_driver_version }}/mariadb-java-client-{{ keycloak_jdbc_driver_version }}.jar + driver_jar_url: "https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/{{ keycloak_jdbc_driver_version }}/mariadb-java-client-{{ keycloak_jdbc_driver_version }}.jar" connection_url: "{{ keycloak_jdbc_url }}" - db_user: "{{ keycloak_db_user }}" + db_user: "{{ keycloak_db_user }}" db_password: "{{ keycloak_db_pass }}" - validate_query: "{{ keycloak_db_valid_conn_sql | default('select 1') }}" initialize_db: > CREATE TABLE IF NOT EXISTS JGROUPSPING ( own_addr varchar(200) NOT NULL, @@ -59,49 +63,21 @@ keycloak_jdbc: ping_data varbinary(5000) DEFAULT NULL, PRIMARY KEY (own_addr, cluster_name)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin - sqlserver: - enabled: "{{ (keycloak_ha_enabled or keycloak_db_enabled) and keycloak_jdbc_engine == 'sqlserver' }}" - driver_class: com.microsoft.sqlserver.jdbc.SQLServerDriver - xa_datasource_class: com.microsoft.sqlserver.jdbc.SQLServerXADataSource - driver_module_name: "com.microsoft.sqlserver" - driver_module_dir: "{{ keycloak_jboss_home }}/modules/com/microsoft/sqlserver/main" - driver_version: "{{ keycloak_jdbc_driver_version }}" - driver_jar_filename: "mssql-java-client-{{ keycloak_jdbc_driver_version }}.jar" - driver_jar_url: > - {{ keycloak_maven_central }}com/microsoft/sqlserver/mssql-jdbc/{{ keycloak_jdbc_driver_version }}.jre11/mssql-jdbc-{{ keycloak_jdbc_driver_version }}.jre11.jar - connection_url: "{{ keycloak_jdbc_url }}" - db_user: "{{ keycloak_db_user }}" - db_password: "{{ keycloak_db_pass }}" - validate_query: "{{ keycloak_db_valid_conn_sql | default('select 1') }}" - initialize_db: > - IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[JGROUPSPING]') AND type in (N'U')) - BEGIN - CREATE TABLE JGROUPSPING ( - own_addr varchar(200) NOT NULL, - cluster_name varchar(200) NOT NULL, - updated DATETIME2 DEFAULT SYSUTCDATETIME(), - ping_data varbinary(5000) DEFAULT NULL, - PRIMARY KEY (own_addr, cluster_name)) - END # reverse proxy mod_cluster keycloak_modcluster: - enabled: "{{ keycloak_ha_enabled or keycloak_modcluster_enabled }}" - reverse_proxy_urls: "{{ keycloak_modcluster_urls }}" + enabled: "{{ keycloak_ha_enabled }}" + reverse_proxy_url: "{{ keycloak_modcluster_url }}" frontend_url: "{{ keycloak_frontend_url }}" - force_frontend_url: "{{ keycloak_frontend_url_force }}" - admin_url: "{{ keycloak_admin_url | default('') }}" # infinispan keycloak_remotecache: enabled: "{{ keycloak_ha_enabled }}" - username: "{{ keycloak_infinispan_user }}" - password: "{{ keycloak_infinispan_pass }}" + username: "{{ infinispan_user }}" + password: "{{ infinispan_pass }}" realm: default - sasl_mechanism: "{{ keycloak_infinispan_sasl_mechanism }}" - server_name: "{{ keycloak_infinispan_url }}" - use_ssl: "{{ keycloak_infinispan_use_ssl }}" - trust_store_path: "{{ keycloak_infinispan_trust_store_path }}" - trust_store_password: "{{ keycloak_infinispan_trust_store_password }}" - -keycloak_maven_central: https://repo1.maven.org/maven2/ + sasl_mechanism: "{{ infinispan_sasl_mechanism }}" + server_name: "{{ infinispan_url }}" + use_ssl: "{{ infinispan_use_ssl }}" + trust_store_path: "{{ infinispan_trust_store_path }}" + trust_store_password: "{{ infinispan_trust_store_password }}" \ No newline at end of file diff --git a/roles/keycloak/vars/redhat.yml b/roles/keycloak/vars/redhat.yml deleted file mode 100644 index 6c36847..0000000 --- a/roles/keycloak/vars/redhat.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -keycloak_varjvm_package: "{{ keycloak_jvm_package | default('java-1.8.0-openjdk-headless') }}" -keycloak_prereq_package_list: - - "{{ keycloak_varjvm_package }}" - - unzip - - procps-ng - - initscripts - - tzdata-java -keycloak_sysconf_file: /etc/sysconfig/keycloak -keycloak_pkg_java_home: "/etc/alternatives/jre_{{ keycloak_varjvm_package | regex_search('(?<=java-)[0-9.]+') }}" diff --git a/roles/keycloak_quarkus/README.md b/roles/keycloak_quarkus/README.md index c461203..19237d5 100644 --- a/roles/keycloak_quarkus/README.md +++ b/roles/keycloak_quarkus/README.md @@ -1,181 +1,75 @@ keycloak_quarkus ================ - -Install [keycloak](https://keycloak.org/) >= 20.0.0 (quarkus) server configurations. - -Requirements ------------- +Install [keycloak](https://keycloak.org/) >= 17.0.0 (quarkus) server configurations. -This role requires the `python3-netaddr` and `lxml` library installed on the controller node. - -* to install via yum/dnf: `dnf install python3-netaddr python3-lxml` -* to install via apt: `apt install python3-netaddr python3-lxml` -* 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 Role Defaults ------------- -#### Installation options - -| Variable | Description | Default | -|:---------|:------------|:--------| -|`keycloak_quarkus_version`| keycloak.org package version | `26.2.4` | -|`keycloak_quarkus_offline_install` | Perform an offline install | `False`| -|`keycloak_quarkus_dest`| Installation root path | `/opt/keycloak` | -|`keycloak_quarkus_download_url` | Download URL for keycloak | `https://github.com/keycloak/keycloak/releases/download/{{ keycloak_quarkus_version }}/{{ keycloak_quarkus_archive }}` | -|`keycloak_quarkus_download_path`| Path local to controller for offline/download of install archives | `{{ lookup('env', 'PWD') }}` | - - -#### Service configuration - -| Variable | Description | Default | -|:---------|:------------|:--------| -|`keycloak_quarkus_bootstrap_admin_user`| Administration console user account | `admin` | -|`keycloak_quarkus_admin_user`| Deprecated, use `keycloak_quarkus_bootstrap_admin_user` instead. | | -|`keycloak_quarkus_bind_address`| Deprecated, use `keycloak_quarkus_http_host` instead | `0.0.0.0` | -|`keycloak_quarkus_host`| Deprecated, use `keycloak_quarkus_hostname` instead. | | -|`keycloak_quarkus_port`| Deprecated, use `keycloak_quarkus_hostname` instead. | | -|`keycloak_quarkus_path`| Deprecated, use `keycloak_quarkus_hostname` instead. | | -|`keycloak_quarkus_service_user`| Posix account username | `keycloak` | -|`keycloak_quarkus_service_group`| Posix account group | `keycloak` | -|`keycloak_quarkus_service_restart_always`| systemd restart always behavior activation | `False` | -|`keycloak_quarkus_service_restart_on_failure`| systemd restart on-failure behavior activation | `False` | -|`keycloak_quarkus_service_restartsec`| systemd RestartSec | `10s` | -|`keycloak_quarkus_jvm_package`| RHEL java package runtime | `java-21-openjdk-headless` | -|`keycloak_quarkus_java_home`| JAVA_HOME of installed JRE, leave empty for using specified keycloak_quarkus_jvm_package RPM path | `None` | -|`keycloak_quarkus_java_heap_opts`| Heap memory JVM setting | `-Xms1024m -Xmx2048m` | -|`keycloak_quarkus_java_jvm_opts`| Other JVM settings | same as keycloak | -|`keycloak_quarkus_java_opts`| JVM arguments; if overridden, it takes precedence over `keycloak_quarkus_java_*` | `{{ keycloak_quarkus_java_heap_opts + ' ' + keycloak_quarkus_java_jvm_opts }}` | -|`keycloak_quarkus_additional_env_vars` | List of additional env variables of { key: str, value: str} to be put in sysconfig file | `[]` | -|`keycloak_quarkus_frontend_url`| Deprecated, use `keycloak_quarkus_hostname` instead. | | -|`keycloak_quarkus_admin_url`| Deprecated, use `keycloak_quarkus_hostname_admin` instead. | | -|`keycloak_quarkus_health_check_url`| Full URL (including scheme, host, path, fragment etc.) used for health check endpoint; keycloak_quarkus_hostname will NOT be prepended; helpful when health checks should happen against http port, but keycloak_quarkus_hostname uses https scheme per default | `` | -|`keycloak_quarkus_health_check_url_path`| Path to the health check endpoint; keycloak_quarkus_hostname will be prepended automatically; Note that keycloak_quarkus_health_check_url takes precedence over this property | `realms/master/.well-known/openid-configuration` | -|`keycloak_quarkus_proxy_headers`| Parse reverse proxy headers (`forwarded` or `xforwarded`) | `""` | -|`keycloak_quarkus_config_key_store_file`| Path to the configuration key store; only used if `keycloak_quarkus_keystore_password` is not empty | `{{ keycloak.home }}/conf/conf_store.p12` if `keycloak_quarkus_keystore_password != ''`, else `''` | -|`keycloak_quarkus_config_key_store_password`| Password of the configuration keystore; if non-empty, `keycloak_quarkus_db_pass` will be saved to the keystore at `keycloak_quarkus_config_key_store_file` instead of being written to the configuration file in clear text | `""` | -|`keycloak_quarkus_configure_firewalld` | Ensure firewalld is running and configure keycloak ports | `False` | -|`keycloak_quarkus_configure_iptables` | Ensure iptables is configured for keycloak ports | `False` | - - -#### High-availability +* Service configuration | Variable | Description | Default | |:---------|:------------|:--------| |`keycloak_quarkus_ha_enabled`| Enable auto configuration for database backend, clustering and remote caches on infinispan | `False` | -|`keycloak_quarkus_ha_discovery`| Discovery protocol for HA cluster members | `JDBCPING` | |`keycloak_quarkus_db_enabled`| Enable auto configuration for database backend | `True` if `keycloak_quarkus_ha_enabled` is True, else `False` | -|`keycloak_quarkus_jgroups_ip`| Host jgroups IP. If changing this variable you must make sure it is always set for all hosts in your cluster. | `{{ ansible_default_ipv4.address }}` | -|`keycloak_quarkus_jgroups_port`| jgroups cluster tcp port | `7800` | -|`keycloak_quarkus_systemd_wait_for_port` | Whether systemd unit should wait for keycloak port before returning | `{{ keycloak_quarkus_ha_enabled }}` | -|`keycloak_quarkus_systemd_wait_for_port_number`| Which port the systemd unit should wait for | `{{ keycloak_quarkus_https_port }}` | -|`keycloak_quarkus_systemd_wait_for_log` | Whether systemd unit should wait for service to be up in logs | `false` | -|`keycloak_quarkus_systemd_wait_for_timeout`| How long to wait for service to be alive (seconds) | `60` | -|`keycloak_quarkus_systemd_wait_for_delay`| Activation delay for service systemd unit (seconds) | `10` | -|`keycloak_quarkus_restart_strategy`| Strategy task file for restarting in HA (one of provided restart/['serial.yml','none.yml','serial_then_parallel.yml']) or path to file when providing custom strategy | `restart/serial.yml` | -|`keycloak_quarkus_restart_health_check`| Whether to wait for successful health check after restart | `true` | -|`keycloak_quarkus_restart_health_check_delay`| Seconds to let pass before starting healch checks | `10` | -|`keycloak_quarkus_restart_health_check_retries`| Number of attempts for successful health check before failing | `25` | -|`keycloak_quarkus_restart_pause`| Seconds to wait between restarts in HA strategy | `15` | +|`keycloak_quarkus_admin_user`| Administration console user account | `admin` | +|`keycloak_quarkus_bind_address`| Address for binding service ports | `0.0.0.0` | +|`keycloak_quarkus_host`| hostname | `localhost` | +|`keycloak_quarkus_http_port`| HTTP port | `8080` | +|`keycloak_quarkus_https_port`| TLS HTTP port | `8443` | +|`keycloak_quarkus_ajp_port`| AJP port | `8009` | +|`keycloak_quarkus_jgroups_port`| jgroups cluster tcp port | `7600` | +|`keycloak_quarkus_java_opts`| Additional JVM options | `-Xms1024m -Xmx2048m` | +|`keycloak_quarkus_service_user`| Posix account username | `keycloak` | +|`keycloak_quarkus_service_group`| Posix account group | `keycloak` | +|`keycloak_quarkus_service_pidfile`| Pid file path for service | `/run/keycloak.pid` | +|`keycloak_quarkus_jvm_package`| RHEL java package runtime | `java-11-openjdk-headless` | +|`keycloak_quarkus_frontend_url`| Service public URL | `http://localhost:8080/auth` | +|`keycloak_quarkus_http_relative_path` | Service context path | `auth` | -#### Hostname configuration +* Database configuration | Variable | Description | Default | |:---------|:------------|:--------| -|`keycloak_quarkus_hostname`| Address at which is the server exposed. Can be a full URL, or just a hostname. When only hostname is provided, scheme, port and context path are resolved from the request. | | -|`keycloak_quarkus_hostname_admin`| Set the base URL for accessing the administration console, including scheme, host, port and path | | -|`keycloak_quarkus_hostname_strict`| Disables dynamically resolving the hostname from request headers | `true` | -|`keycloak_quarkus_hostname_backchannel_dynamic`| Enables dynamic resolving of backchannel URLs, including hostname, scheme, port and context path. Set to true if your application accesses Keycloak via a private network. If set to true, hostname option needs to be specified as a full URL. | `false` | -|`keycloak_quarkus_hostname_strict_backchannel`| Deprecated, use (the inverted!)`keycloak_quarkus_hostname_backchannel_dynamic` instead. | | - - -#### HTTP(S) configuration -| Variable | Description | Default | -|:---------|:------------|:--------| -|`keycloak_quarkus_http_relative_path`| Set the path relative to / for serving resources. The path must start with a / | `/` | -|`keycloak_quarkus_http_host`| The http host, ie. the address used to bind the service | `0.0.0.0` | -|`keycloak_quarkus_http_port`| HTTP listening port | `8080` | -|`keycloak_quarkus_https_port`| TLS HTTP listening port | `8443` | -|`keycloak_quarkus_http_management_port`| Port of the management interface. Relevant only when something is exposed on the management interface - see the guide for details. | `9000` | -|`keycloak_quarkus_https_key_store_file`| The file path to the key store | `{{ keycloak.home }}/conf/key_store.p12` | -|`keycloak_quarkus_https_key_store_password`| Password for the key store | `""` | -|`keycloak_quarkus_https_trust_store_enabled`| Enable configuration of the https trust store | `False` | -|`keycloak_quarkus_https_trust_store_file`| The file path to the trust store | `{{ keycloak.home }}/conf/trust_store.p12` | -|`keycloak_quarkus_https_trust_store_password`| Password for the trust store | `""` | -|`keycloak_quarkus_https_key_file_enabled`| Enable listener on HTTPS port | `False` | -|`keycloak_quarkus_key_file_copy_enabled`| Enable copy of key file to target host | `False` | -|`keycloak_quarkus_key_content`| Content of the TLS private key. Use `"{{ lookup('file', 'server.key.pem') }}"` to lookup a file. | `""` | -|`keycloak_quarkus_key_file`| The file path to a private key in PEM format | `/etc/pki/tls/private/server.key.pem` | -|`keycloak_quarkus_cert_file_copy_enabled`| Enable copy of cert file to target host | `False`| -|`keycloak_quarkus_cert_file_src`| Set the source file path | `""` | -|`keycloak_quarkus_cert_file`| The file path to a server certificate or certificate chain in PEM format | `/etc/pki/tls/certs/server.crt.pem` | -|`keycloak_quarkus_https_key_store_enabled`| Enable configuration of HTTPS via a key store | `False` | -|`keycloak_quarkus_key_store_file`| Deprecated, use `keycloak_quarkus_https_key_store_file` instead. || -|`keycloak_quarkus_key_store_password`| Deprecated, use `keycloak_quarkus_https_key_store_password` instead.|| -|`keycloak_quarkus_http_relative_path` | Set the path relative to / for serving resources. The path must start with a / | `/` | -|`keycloak_quarkus_http_management_relative_path` | Set the path relative to / for serving resources from management interface. The path must start with a /. If not given, the value is inherited from HTTP options. Relevant only when something is exposed on the management interface - see the guide for details. | `/` | -|`keycloak_quarkus_http_enabled`| Enable listener on HTTP port | `True` | - - -#### Database configuration - -| Variable | Description | Default | -|:---------|:------------|:--------| -|`keycloak_quarkus_db_engine` | Database engine [mariadb,postres,mssql] | `postgres` | +|`keycloak_quarkus_jdbc_engine` | Database engine [mariadb,postres] | `postgres` | |`keycloak_quarkus_db_user` | User for database connection | `keycloak-user` | |`keycloak_quarkus_db_pass` | Password for database connection | `keycloak-pass` | -|`keycloak_quarkus_db_url` | JDBC URL for connecting to database | `jdbc:postgresql://localhost:5432/keycloak` | -|`keycloak_quarkus_db_driver_version` | Version for JDBC engine driver | `9.4.1212` | +|`keycloak_quarkus_jdbc_url` | JDBC URL for connecting to database | `jdbc:postgresql://localhost:5432/keycloak` | +|`keycloak_quarkus_jdbc_driver_version` | Version for JDBC driver | `9.4.1212` | -#### Cache configuration +* Remote caches configuration | Variable | Description | Default | |:---------|:------------|:--------| -|`keycloak_quarkus_cache_remote` | Whether to connect to remote cache infinispan server | `false` | -|`keycloak_quarkus_cache_remote_username` | Username for connecting to infinispan | `supervisor` | -|`keycloak_quarkus_cache_remote_password` | Password for connecting to infinispan | `supervisor` | -|`keycloak_quarkus_cache_remote_host` | Hostname for connecting to infinispan | `localhost` | -|`keycloak_quarkus_cache_remote_port`| Port for connecting to infinispan | `11222` | -|`keycloak_quarkus_cache_remote_sasl_mechanism` | Infinispan auth mechanism | `SCRAM-SHA-512` | -|`keycloak_quarkus_cache_remote_tls_enabled` | Whether infinispan uses TLS connection | `false` | +|`keycloak_quarkus_ispn_user` | Username for connecting to infinispan | `supervisor` | +|`keycloak_quarkus_ispn_pass` | Password for connecting to infinispan | `supervisor` | +|`keycloak_quarkus_ispn_url` | URL for connecting to infinispan | `localhost` | +|`keycloak_quarkus_ispn_sasl_mechanism` | Infinispan auth mechanism | `SCRAM-SHA-512` | +|`keycloak_quarkus_ispn_use_ssl` | Whether infinispan uses TLS connection | `false` | +|`keycloak_quarkus_ispn_trust_store_path` | Path to infinispan server trust certificate | `/etc/pki/java/cacerts` | +|`keycloak_quarkus_ispn_trust_store_password` | Password for infinispan certificate keystore | `changeit` | -#### Logging configuration +* Install options | Variable | Description | Default | -|:---------|:------------|:--------| -|`keycloak_quarkus_log`| Enable one or more log handlers in a comma-separated list | `file` | -|`keycloak_quarkus_log_level`| The log level of the root category or a comma-separated list of individual categories and their levels | `info` | -|`keycloak_quarkus_log_file`| Set the log file path and filename relative to keycloak home | `data/log/keycloak.log` | -|`keycloak_quarkus_log_format`| Set a format specific to file log entries | `%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n` | -|`keycloak_quarkus_log_target`| Set the destination of the keycloak log folder link | `/var/log/keycloak` | -|`keycloak_quarkus_log_max_file_size`| Set the maximum log file size before a log rotation happens; A size configuration option recognises string in this format (shown as a regular expression): `[0-9]+[KkMmGgTtPpEeZzYy]?`. If no suffix is given, assume bytes. | `10M` | -|`keycloak_quarkus_log_max_backup_index`| Set the maximum number of archived log files to keep" | `10` | -|`keycloak_quarkus_log_file_suffix`| Set the log file handler rotation file suffix. When used, the file will be rotated based on its suffix; Note: If the suffix ends with `.zip` or `.gz`, the rotation file will also be compressed. | `.yyyy-MM-dd.zip` | +|:---------|:------------|:---------| +|`keycloak_quarkus_offline_install` | Perform an offline install | `False`| +|`keycloak_quarkus_download_url`| Download URL for keycloak | `https://github.com/keycloak/keycloak/releases/download//`| +|`keycloak_quarkus_version`| keycloak.org package version | `17.0.1` | +|`keycloak_quarkus_dest`| Installation root path | `/opt/keycloak` | +|`keycloak_quarkus_download_url` | Download URL for keycloak | `https://github.com/keycloak/keycloak/releases/download/{{ keycloak_quarkus_version }}/{{ keycloak_quarkus_archive }}` | +|`keycloak_quarkus_configure_firewalld` | Ensure firewalld is running and configure keycloak ports | `False` | -#### Miscellaneous configuration +* Miscellaneous configuration | Variable | Description | Default | |:---------|:------------|:--------| |`keycloak_quarkus_metrics_enabled`| Whether to enable metrics | `False` | -|`keycloak_quarkus_health_enabled`| If the server should expose health check endpoints on the management interface | `True` | |`keycloak_quarkus_archive` | keycloak install archive filename | `keycloak-{{ keycloak_quarkus_version }}.zip` | |`keycloak_quarkus_installdir` | Installation path | `{{ keycloak_quarkus_dest }}/keycloak-{{ keycloak_quarkus_version }}` | |`keycloak_quarkus_home` | Installation work directory | `{{ keycloak_quarkus_installdir }}` | @@ -183,110 +77,17 @@ Role Defaults |`keycloak_quarkus_master_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_quarkus_proxy_mode`| The proxy address forwarding mode if the server is behind a reverse proxy | `edge` | -|`keycloak_quarkus_start_dev`| Whether to start the service in development mode (start-dev) | `False` | -|`keycloak_quarkus_transaction_xa_enabled`| Whether to use XA transactions | `True` | -|`keycloak_quarkus_spi_sticky_session_encoder_infinispan_should_attach_route`| If the route should be attached to cookies to reflect the node that owns a particular session. If false, route is not attached to cookies and we rely on the session affinity capabilities from reverse proxy | `True` | -|`keycloak_quarkus_show_deprecation_warnings`| Whether deprecation warnings should be shown | `True` | - - -#### Vault configuration - -| Variable | Description | Default | -|:---------|:------------|:--------| -|`keycloak_quarkus_ks_vault_enabled`| Whether to enable the vault SPI | `false` | -|`keycloak_quarkus_ks_vault_file`| The keystore path for the vault SPI | `{{ keycloak_quarkus_config_dir }}/keystore.p12` | -|`keycloak_quarkus_ks_vault_type`| Type of the keystore used for the vault SPI | `PKCS12` | - - -#### Configuring providers - -| Variable | Description | Default | -|:---------|:------------|:--------| -|`keycloak_quarkus_providers`| List of provider definitions; see below | `[]` | - -Providers support different sources: - -* `url`: http download for providers not requiring authentication -* `maven`: maven download for providers hosted publicly on Apache Maven Central or private Maven repositories like Github Maven requiring authentication -* `local_path`: static providers to be uploaded - -Provider definition: - -```yaml -keycloak_quarkus_providers: - - id: http-client # required; "{{ id }}.jar" identifies the file name on RHBK - spi: connections # required if neither url, local_path nor maven are specified; required for setting properties - default: true # optional, whether to set default for spi, default false - restart: true # optional, whether to rebuild config and restart the service after deploying, default true - url: https://.../.../custom_spi.jar # optional, url for download via http - local_path: my_theme_spi.jar # optional, path on local controller for SPI to be uploaded - maven: # optional, for download using maven - repository_url: https://maven.pkg.github.com/OWNER/REPOSITORY # optional, maven repo url - group_id: my.group # optional, maven group id - artifact_id: artifact # optional, maven artifact id - version: 24.0.5 # optional, defaults to latest - username: user # optional, cf. https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry#authenticating-to-github-packages - password: pat # optional, provide a PAT for accessing Github's Apache Maven registry - properties: # optional, list of key-values - - key: default-connection-pool-size - value: 10 - checksum: sha256:D98291AC[...]B6DC7B97 # optional, checksum used to verify integrity: - # for `url` SPIs, use format: :, cf. ; - # for `local_path` SPIs, use SHA1 format - # for `maven` SPIs, this field is ignored since maven has integrity verification methods enabled by default -``` - -the definition above will generate the following build command: - -``` -bin/kc.sh build --spi-connections-provider=http-client --spi-connections-http-client-default-connection-pool-size=10 -``` - - -#### Configuring policies - -| Variable | Description | Default | -|:---------|:------------|:--------| -|`keycloak_quarkus_policies`| List of policy definitions; see below | `[]` | - -Provider definition: - -```yaml -keycloak_quarkus_policies: - - name: john-the-ripper.txt # required, resulting file name - url: https://github.com/danielmiessler/SecLists/raw/master/Passwords/Software/john-the-ripper.txt # required, url for download - type: password-blacklists # optional, defaults to `password-blacklists`; supported values: [`password-blacklists`] -``` +|`keycloak_url` | URL for configuration rest calls | `http://{{ keycloak_quarkus_host }}:{{ keycloak_http_port }}` | +|`keycloak_management_url` | URL for management console rest calls | `http://{{ keycloak_quarkus_host }}:{{ keycloak_management_http_port }}` | Role Variables -------------- -| Variable | Description | Required | -|:---------|:------------|----------| -|`keycloak_quarkus_bootstrap_admin_password`| Password of console admin account | `yes` | -|`keycloak_quarkus_admin_pass`| Deprecated, use `keycloak_quarkus_bootstrap_admin_password` instead. | | -|`keycloak_quarkus_ks_vault_pass`| The password for accessing the keystore vault SPI | `no` | -|`keycloak_quarkus_alternate_download_url`| Alternate location with optional authentication for downloading RHBK | `no` | -|`keycloak_quarkus_download_user`| Optional username for http authentication | `no*` | -|`keycloak_quarkus_download_pass`| Optional password for http authentication | `no*` | -|`keycloak_quarkus_download_validate_certs`| Whether to validate certs for URL `keycloak_quarkus_alternate_download_url` | `no` | -|`keycloak_quarkus_jdbc_download_user`| Optional username for http authentication | `no*` | -|`keycloak_quarkus_jdbc_download_pass`| Optional password for http authentication | `no*` | -|`keycloak_quarkus_jdbc_download_validate_certs`| Whether to validate certs for URL `keycloak_quarkus_download_validate_certs` | `no` | - -`*` username/password authentication credentials must be both declared or both undefined - - -Role custom facts ------------------ - -The role uses the following [custom facts](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_vars_facts.html#adding-custom-facts) found in `/etc/ansible/facts.d/keycloak.fact` (and thus identified by the `ansible_local.keycloak.` prefix): - | Variable | Description | |:---------|:------------| -|`general.bootstrapped` | A custom fact indicating whether this role has been used for bootstrapping keycloak on the respective host before; set to `false` (e.g., when starting off with a new, empty database) ensures that the initial admin user as defined by `keycloak_quarkus_bootstrap_admin_user[_password]` gets created | +|`keycloak_quarkus_admin_pass`| Password of console admin account | + License ------- diff --git a/roles/keycloak_quarkus/defaults/main.yml b/roles/keycloak_quarkus/defaults/main.yml index d53c790..6778308 100644 --- a/roles/keycloak_quarkus/defaults/main.yml +++ b/roles/keycloak_quarkus/defaults/main.yml @@ -1,196 +1,70 @@ --- ### Configuration specific to keycloak -keycloak_quarkus_version: 26.2.4 +keycloak_quarkus_version: 17.0.1 keycloak_quarkus_archive: "keycloak-{{ keycloak_quarkus_version }}.zip" -keycloak_quarkus_download_url: "https://github.com/keycloak/keycloak/releases/download/{{ keycloak_quarkus_version }}/{{ keycloak_quarkus_archive }}" +keycloak_quarkus_download_url: "https://github.com/keycloak/keycloak/releases/download/{{ keycloak_quarkus_version }}/{{ keycloak_quarkus_archive }}" keycloak_quarkus_installdir: "{{ keycloak_quarkus_dest }}/keycloak-{{ keycloak_quarkus_version }}" # whether to install from local archive -keycloak_quarkus_offline_install: false - -keycloak_quarkus_show_deprecation_warnings: true +keycloak_quarkus_offline_install: False ### Install location and service settings -keycloak_quarkus_java_home: +keycloak_quarkus_jvm_package: java-11-openjdk-headless keycloak_quarkus_dest: /opt/keycloak keycloak_quarkus_home: "{{ keycloak_quarkus_installdir }}" keycloak_quarkus_config_dir: "{{ keycloak_quarkus_home }}/conf" -keycloak_quarkus_download_path: "{{ lookup('env', 'PWD') }}" -keycloak_quarkus_start_dev: false keycloak_quarkus_service_user: keycloak keycloak_quarkus_service_group: keycloak -keycloak_quarkus_service_restart_always: false -keycloak_quarkus_service_restart_on_failure: false -keycloak_quarkus_service_restartsec: "10s" - -keycloak_quarkus_configure_firewalld: false -keycloak_quarkus_configure_iptables: false +keycloak_quarkus_service_pidfile: "/run/keycloak.pid" +keycloak_quarkus_configure_firewalld: False ### administrator console password -keycloak_quarkus_bootstrap_admin_user: admin -keycloak_quarkus_bootstrap_admin_password: +keycloak_quarkus_admin_user: admin +keycloak_quarkus_admin_pass: '' keycloak_quarkus_master_realm: master ### Configuration settings -keycloak_quarkus_bind_address: 0.0.0.0 # deprecated use keycloak_quarkus_http_host -keycloak_quarkus_http_host: 0.0.0.0 -keycloak_quarkus_http_enabled: true +keycloak_quarkus_bind_address: 0.0.0.0 +keycloak_quarkus_host: localhost keycloak_quarkus_http_port: 8080 keycloak_quarkus_https_port: 8443 -keycloak_quarkus_http_management_port: 9000 -keycloak_quarkus_jgroups_port: 7800 -keycloak_quarkus_jgroups_bind_address: "{{ ansible_default_ipv4.address }}" -keycloak_quarkus_jgroups_external_addr: "{{ keycloak_quarkus_jgroups_bind_address }}" -keycloak_quarkus_jgroups_external_port: "{{ keycloak_quarkus_jgroups_port }}" -keycloak_quarkus_java_heap_opts: "-Xms1024m -Xmx2048m" -keycloak_quarkus_java_jvm_opts: > - -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 - -Dsun.err.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -XX:+ExitOnOutOfMemoryError - -Djava.security.egd=file:/dev/urandom -XX:+UseParallelGC -XX:GCTimeRatio=4 - -XX:AdaptiveSizePolicyWeight=90 -XX:FlightRecorderOptions=stackdepth=512 -keycloak_quarkus_jgroups_opts: > - -Djgroups.bind.address={{ keycloak_quarkus_jgroups_bind_address }} - -Djgroups.external_port={{ keycloak_quarkus_jgroups_external_port }} - -Djgroups.external_addr={{ keycloak_quarkus_jgroups_external_addr }} -keycloak_quarkus_java_opts: "{{ ' '.join((keycloak_quarkus_jgroups_opts, keycloak_quarkus_java_heap_opts, keycloak_quarkus_java_jvm_opts)) }}" -keycloak_quarkus_additional_env_vars: [] - -### TLS/HTTPS configuration -keycloak_quarkus_https_key_file_enabled: false -keycloak_quarkus_key_file_copy_enabled: false -keycloak_quarkus_key_content: "" -keycloak_quarkus_key_file: "/etc/pki/tls/private/server.key.pem" -keycloak_quarkus_cert_file_copy_enabled: false -keycloak_quarkus_cert_file_src: "" -keycloak_quarkus_cert_file: "/etc/pki/tls/certs/server.crt.pem" -#### key store configuration -keycloak_quarkus_https_key_store_enabled: false -keycloak_quarkus_https_key_store_file: "{{ keycloak.home }}/conf/key_store.p12" -keycloak_quarkus_https_key_store_password: '' -##### trust store configuration -keycloak_quarkus_https_trust_store_enabled: false -keycloak_quarkus_https_trust_store_file: "{{ keycloak.home }}/conf/trust_store.p12" -keycloak_quarkus_https_trust_store_password: '' -### configuration key store configuration -keycloak_quarkus_config_key_store_file: "{{ keycloak.home }}/conf/conf_store.p12" -keycloak_quarkus_config_key_store_password: '' +keycloak_quarkus_ajp_port: 8009 +keycloak_quarkus_jgroups_port: 7600 +keycloak_quarkus_java_opts: "-Xms1024m -Xmx2048m" ### Enable configuration for database backend, clustering and remote caches on infinispan -keycloak_quarkus_ha_enabled: false -keycloak_quarkus_ha_discovery: "JDBCPING" +keycloak_quarkus_ha_enabled: False ### Enable database configuration, must be enabled when HA is configured -keycloak_quarkus_db_enabled: "{{ keycloak_quarkus_ha_enabled }}" -keycloak_quarkus_systemd_wait_for_port: "{{ keycloak_quarkus_ha_enabled }}" -keycloak_quarkus_systemd_wait_for_port_number: "{{ keycloak_quarkus_https_port }}" -keycloak_quarkus_systemd_wait_for_log: false -keycloak_quarkus_systemd_wait_for_timeout: 60 -keycloak_quarkus_systemd_wait_for_delay: 10 +keycloak_quarkus_db_enabled: "{{ True if keycloak_quarkus_ha_enabled else False }}" ### keycloak frontend url -keycloak_quarkus_hostname: -keycloak_quarkus_hostname_admin: +keycloak_quarkus_http_relative_path: auth +keycloak_quarkus_frontend_url: http://localhost:8080/auth -### Set the path relative to / for serving resources. The path must start with a / -### (set to `/auth` for retrocompatibility with pre-quarkus releases) -keycloak_quarkus_http_relative_path: / - -# Disables dynamically resolving the hostname from request headers. -# Should always be set to true in production, unless proxy verifies the Host header. -keycloak_quarkus_hostname_strict: true -# Enables dynamic resolving of backchannel URLs, including hostname, scheme, port and context path. -# Set to true if your application accesses Keycloak via a private network. If set to true, keycloak_quarkus_hostname option needs to be specified as a full URL. -keycloak_quarkus_hostname_backchannel_dynamic: false - -# The proxy headers that should be accepted by the server. ['', 'forwarded', 'xforwarded'] -keycloak_quarkus_proxy_headers: "" - -# deprecated: proxy address forwarding mode if the server is behind a reverse proxy. [none, edge, reencrypt, passthrough] -keycloak_quarkus_proxy_mode: edge - -# disable xa transactions -keycloak_quarkus_transaction_xa_enabled: true - -# If the route should be attached to cookies to reflect the node that owns a particular session. -# If false, route is not attached to cookies and we rely on the session affinity capabilities from reverse proxy -keycloak_quarkus_spi_sticky_session_encoder_infinispan_should_attach_route: true - -keycloak_quarkus_metrics_enabled: false -keycloak_quarkus_health_enabled: true - -### caches; must read: https://www.keycloak.org/2024/12/storing-sessions-in-kc26 -### embedded caches -# https://www.keycloak.org/server/caching -keycloak_quarkus_cache_metrics_enabled: false -keycloak_quarkus_cache_embedded_authorization_max_count: -keycloak_quarkus_cache_embedded_client_sessions_max_count: -keycloak_quarkus_cache_embedded_crl_max_count: -keycloak_quarkus_cache_embedded_keys_max_count: -keycloak_quarkus_cache_embedded_offline_client_sessions_max_count: -keycloak_quarkus_cache_embedded_offline_sessions_max_count: -keycloak_quarkus_cache_embedded_realms_max_count: -keycloak_quarkus_cache_embedded_sessions_max_count: -keycloak_quarkus_cache_embedded_users_max_count: -keycloak_quarkus_cache_embedded_mtls_enabled: true -keycloak_quarkus_cache_embedded_mtls_key_store_file: "{{ keycloak.home }}/conf/cache_key_store.p12" -keycloak_quarkus_cache_embedded_mtls_key_store_password: '' -keycloak_quarkus_cache_embedded_mtls_rotation_interval_days: 30 -keycloak_quarkus_cache_embedded_mtls_trust_store_file: "{{ keycloak.home }}/conf/cache_trust_store.p12" -keycloak_quarkus_cache_embedded_mtls_trust_store_password: '' +keycloak_quarkus_metrics_enabled: False ### infinispan remote caches access (hotrod) -# https://www.keycloak.org/server/caching#_remote_cache -keycloak_quarkus_cache_remote: false -keycloak_quarkus_cache_remote_username: supervisor -keycloak_quarkus_cache_remote_password: supervisor -keycloak_quarkus_cache_remote_host: localhost -keycloak_quarkus_cache_remote_port: 11222 -keycloak_quarkus_cache_remote_tls_enabled: false -keycloak_quarkus_cache_remote_sasl_mechanism: SCRAM-SHA-512 - +keycloak_quarkus_ispn_user: supervisor +keycloak_quarkus_ispn_pass: supervisor +keycloak_quarkus_ispn_url: localhost +keycloak_quarkus_ispn_sasl_mechanism: SCRAM-SHA-512 +keycloak_quarkus_ispn_use_ssl: False +# if ssl is enabled, import ispn server certificate here +keycloak_quarkus_ispn_trust_store_path: /etc/pki/java/cacerts +keycloak_quarkus_ispn_trust_store_password: changeit ### database backend engine: values [ 'postgres', 'mariadb' ] -keycloak_quarkus_db_engine: postgres +keycloak_quarkus_jdbc_engine: postgres ### database backend credentials keycloak_quarkus_db_user: keycloak-user keycloak_quarkus_db_pass: keycloak-pass -keycloak_quarkus_db_url: "{{ keycloak_quarkus_default_jdbc[keycloak_quarkus_db_engine].url }}" -keycloak_quarkus_db_driver_version: "{{ keycloak_quarkus_default_jdbc[keycloak_quarkus_db_engine].version }}" -# override the variables above, following defaults show recommended version as per -# https://access.redhat.com/articles/7033107 +keycloak_quarkus_jdbc_url: "{{ keycloak_quarkus_default_jdbc[keycloak_quarkus_jdbc_engine].url }}" +keycloak_quarkus_jdbc_driver_version: "{{ keycloak_quarkus_default_jdbc[keycloak_quarkus_jdbc_engine].version }}" +# override the variables above, following defaults show minimum supported versions keycloak_quarkus_default_jdbc: postgres: url: 'jdbc:postgresql://localhost:5432/keycloak' - version: 42.7.5 + version: 9.4.1212 mariadb: url: 'jdbc:mariadb://localhost:3306/keycloak' - version: 3.5.2 - mssql: - url: 'jdbc:sqlserver://localhost:1433;databaseName=keycloak;' - version: 12.8.1 - driver_jar_url: "https://repo1.maven.org/maven2/com/microsoft/sqlserver/mssql-jdbc/12.8.1.jre11/mssql-jdbc-12.8.1.jre11.jar" -### logging configuration -keycloak_quarkus_log: file -keycloak_quarkus_log_level: info -keycloak_quarkus_log_file: data/log/keycloak.log -keycloak_quarkus_log_format: '%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n' -keycloak_quarkus_log_target: /var/log/keycloak -keycloak_quarkus_log_max_file_size: 10M -keycloak_quarkus_log_max_backup_index: 10 -keycloak_quarkus_log_file_suffix: '.yyyy-MM-dd.zip' - -# keystore-based vault -keycloak_quarkus_ks_vault_enabled: false -keycloak_quarkus_ks_vault_file: "{{ keycloak_quarkus_config_dir }}/keystore.p12" -keycloak_quarkus_ks_vault_type: PKCS12 -keycloak_quarkus_ks_vault_pass: - -keycloak_quarkus_providers: [] -keycloak_quarkus_policies: [] -keycloak_quarkus_supported_policy_types: ['password-blacklists'] - -# files in restart directory (one of [ 'serial', 'none', 'serial_then_parallel' ]), or path to file when providing custom strategy -keycloak_quarkus_restart_strategy: restart/serial.yml -keycloak_quarkus_restart_health_check: true -keycloak_quarkus_restart_health_check_delay: 10 -keycloak_quarkus_restart_health_check_retries: 25 -keycloak_quarkus_restart_pause: 15 + version: 2.7.4 \ No newline at end of file diff --git a/roles/keycloak_quarkus/handlers/main.yml b/roles/keycloak_quarkus/handlers/main.yml index eec7789..00cab00 100644 --- a/roles/keycloak_quarkus/handlers/main.yml +++ b/roles/keycloak_quarkus/handlers/main.yml @@ -1,21 +1,4 @@ --- -- name: "Invalidate {{ keycloak.service_name }} theme cache" - ansible.builtin.include_tasks: invalidate_theme_cache.yml - listen: "invalidate keycloak theme cache" -# handler should be invoked anytime a [build configuration](https://www.keycloak.org/server/all-config?f=build) changes -- name: "Rebuild {{ keycloak.service_name }} config" - ansible.builtin.include_tasks: rebuild_config.yml - listen: "rebuild keycloak config" -- name: "Bootstrapped" - ansible.builtin.include_tasks: bootstrapped.yml - listen: bootstrapped - name: "Restart {{ keycloak.service_name }}" - ansible.builtin.include_tasks: - file: "{{ keycloak_quarkus_restart_strategy if keycloak_quarkus_ha_enabled else 'restart.yml' }}" - listen: "restart keycloak" -- name: "Display deprecation warning" - ansible.builtin.fail: - msg: "Deprecation warning: you are using the deprecated variable '{{ deprecated_variable | d('NotSet') }}', check docs on how to upgrade." - failed_when: false - changed_when: true - listen: "print deprecation warning" + ansible.builtin.include_tasks: restart.yml + listen: "restart keycloak" \ No newline at end of file diff --git a/roles/keycloak_quarkus/meta/argument_specs.yml b/roles/keycloak_quarkus/meta/argument_specs.yml index 1683321..78382f9 100644 --- a/roles/keycloak_quarkus/meta/argument_specs.yml +++ b/roles/keycloak_quarkus/meta/argument_specs.yml @@ -2,607 +2,202 @@ argument_specs: main: options: keycloak_quarkus_version: - default: "26.2.4" + # line 3 of defaults/main.yml + default: "17.0.1" description: "keycloak.org package version" type: "str" keycloak_quarkus_archive: + # line 4 of defaults/main.yml default: "keycloak-{{ keycloak_quarkus_version }}.zip" description: "keycloak install archive filename" type: "str" keycloak_quarkus_download_url: + # line 5 of defaults/main.yml default: "https://github.com/keycloak/keycloak/releases/download/{{ keycloak_quarkus_version }}/{{ keycloak_quarkus_archive }}" description: "Download URL for keycloak" type: "str" keycloak_quarkus_installdir: + # line 6 of defaults/main.yml default: "{{ keycloak_quarkus_dest }}/keycloak-{{ keycloak_quarkus_version }}" description: "Installation path" type: "str" keycloak_quarkus_offline_install: + # line 9 of defaults/main.yml default: false description: "Perform an offline install" type: "bool" keycloak_quarkus_jvm_package: - default: "java-21-openjdk-headless" + # line 12 of defaults/main.yml + default: "java-11-openjdk-headless" description: "RHEL java package runtime" type: "str" - keycloak_quarkus_java_home: - description: "JAVA_HOME of installed JRE, leave empty for using specified keycloak_jvm_package RPM path" - type: "str" keycloak_quarkus_dest: + # line 13 of defaults/main.yml default: "/opt/keycloak" description: "Installation root path" type: "str" keycloak_quarkus_home: + # line 14 of defaults/main.yml default: "{{ keycloak_quarkus_installdir }}" description: "Installation work directory" type: "str" keycloak_quarkus_config_dir: + # line 15 of defaults/main.yml default: "{{ keycloak_quarkus_home }}/conf" description: "Path for configuration" type: "str" keycloak_quarkus_service_user: + # line 16 of defaults/main.yml default: "keycloak" description: "Posix account username" type: "str" keycloak_quarkus_service_group: + # line 17 of defaults/main.yml default: "keycloak" description: "Posix account group" type: "str" + keycloak_quarkus_service_pidfile: + # line 18 of defaults/main.yml + default: "/run/keycloak.pid" + description: "Pid file path for service" + type: "str" keycloak_quarkus_configure_firewalld: + # line 19 of defaults/main.yml default: false description: "Ensure firewalld is running and configure keycloak ports" type: "bool" - keycloak_quarkus_configure_iptables: - default: false - description: "Ensure firewalld is running and configure keycloak ports" - type: "bool" - keycloak_quarkus_service_restart_always: - default: false - description: "systemd restart always behavior of service; takes precedence over keycloak_service_restart_on_failure if true" - type: "bool" - keycloak_quarkus_service_restart_on_failure: - default: false - description: "systemd restart on-failure behavior of service" - type: "bool" - keycloak_quarkus_service_restartsec: - default: "10s" - description: "systemd RestartSec for service" - type: "str" - keycloak_quarkus_bootstrap_admin_user: + keycloak_quarkus_admin_user: + # line 22 of defaults/main.yml default: "admin" - description: "Administration user account, only for bootstrapping" + description: "Administration console user account" type: "str" - keycloak_quarkus_bootstrap_admin_password: - required: true - description: "Password of admin account, only for bootstrapping" + keycloak_quarkus_admin_pass: + # line 23 of defaults/main.yml + default: "" + description: "Password of console admin account" type: "str" keycloak_quarkus_master_realm: + # line 24 of defaults/main.yml default: "master" description: "Name for rest authentication realm" type: "str" keycloak_quarkus_bind_address: + # line 27 of defaults/main.yml default: "0.0.0.0" - description: "Deprecated, use `keycloak_quarkus_http_host`" - type: "str" - keycloak_quarkus_hostname: - description: >- - Address at which is the server exposed. - Can be a full URL, or just a hostname. When only hostname is provided, scheme, port and context path are resolved from the request. + description: "Address for binding service ports" type: "str" keycloak_quarkus_host: - description: "Deprecated in v26, use keycloak_quarkus_hostname instead." - type: "str" - keycloak_quarkus_port: - description: "Deprecated in v26, use keycloak_quarkus_hostname instead." - type: "int" - keycloak_quarkus_path: - description: "Deprecated in v26, use keycloak_quarkus_hostname instead." - type: "str" - keycloak_quarkus_http_enabled: - default: true - description: "Enable listener on HTTP port" - type: "bool" - keycloak_quarkus_http_host: - default: '0.0.0.0' - description: "HTTP host, address for binding service ports" + # line 28 of defaults/main.yml + default: "localhost" + description: "hostname" type: "str" keycloak_quarkus_http_port: + # line 29 of defaults/main.yml default: 8080 description: "HTTP port" type: "int" - keycloak_quarkus_health_check_url: - description: "Full URL (including scheme, host, path, fragment etc.) used for health check endpoint; keycloak_quarkus_hostname will NOT be prepended; helpful when health checks should happen against http port, but keycloak_quarkus_hostname uses https scheme per default" - type: "str" - keycloak_quarkus_health_check_url_path: - default: "realms/master/.well-known/openid-configuration" - description: "Path to the health check endpoint; keycloak_quarkus_hostname will be prepended automatically; Note that keycloak_quarkus_health_check_url takes precedence over this property" - type: "str" - keycloak_quarkus_https_key_file_enabled: - default: false - description: "Enable configuration of HTTPS via files in PEM format" - type: "bool" - keycloak_quarkus_key_file_copy_enabled: - default: false - description: "Enable copy of key file to target host" - type: "bool" - keycloak_quarkus_key_content: - default: "" - description: "Content of the TLS private key" - type: "str" - keycloak_quarkus_key_file: - default: "/etc/pki/tls/private/server.key.pem" - description: "The file path to a private key in PEM format" - type: "str" - keycloak_quarkus_cert_file_copy_enabled: - default: false - description: "Enable copy of cert file to target host" - type: "bool" - keycloak_quarkus_cert_file_src: - default: "" - description: "Set the source file path" - type: "str" - keycloak_quarkus_cert_file: - default: "/etc/pki/tls/certs/server.crt.pem" - description: "The file path to a server certificate or certificate chain in PEM format" - type: "str" - keycloak_quarkus_https_key_store_enabled: - default: false - description: "Enable configuration of HTTPS via a key store" - type: "bool" - keycloak_quarkus_key_store_file: - default: "" - description: "Deprecated, use `keycloak_quarkus_https_key_store_file` instead." - type: "str" - keycloak_quarkus_key_store_password: - default: "" - description: "Deprecated, use `keycloak_quarkus_https_key_store_password` instead." - type: "str" - keycloak_quarkus_https_key_store_file: - default: "{{ keycloak.home }}/conf/key_store.p12" - description: "The file path to the key store" - type: "str" - keycloak_quarkus_https_key_store_password: - default: "" - description: "Password for the key store" - type: "str" - keycloak_quarkus_https_trust_store_enabled: - default: false - description: "Enable configuration of the https trust store" - type: "bool" - keycloak_quarkus_https_trust_store_file: - default: "{{ keycloak.home }}/conf/trust_store.p12" - description: "The file path to the trust store" - type: "str" - keycloak_quarkus_https_trust_store_password: - default: "" - description: "Password for the trust store" - type: "str" - keycloak_quarkus_config_key_store_file: - default: "{{ keycloak.home }}/conf/conf_store.p12" - description: "Path to the configuration key store; only used if `keycloak_quarkus_keystore_password` is not empty" - type: "str" - keycloak_quarkus_config_key_store_password: - default: "" - description: > - Password of the configuration key store; if non-empty, `keycloak_quarkus_db_pass` will be saved to the key store - at `keycloak_quarkus_config_key_store_file` (instead of being written to the configuration file in clear text) - type: "str" keycloak_quarkus_https_port: + # line 30 of defaults/main.yml default: 8443 description: "HTTPS port" type: "int" - keycloak_quarkus_http_management_port: - default: 9000 - description: "Port of the management interface. Relevant only when something is exposed on the management interface - see the guide for details." + keycloak_quarkus_ajp_port: + # line 31 of defaults/main.yml + default: 8009 + description: "AJP port" + type: "int" + keycloak_quarkus_jgroups_port: + # line 32 of defaults/main.yml + default: 7600 + description: "jgroups cluster tcp port" type: "int" - keycloak_quarkus_java_heap_opts: - default: "-Xms1024m -Xmx2048m" - description: "Heap memory JVM setting" - type: "str" - keycloak_quarkus_java_jvm_opts: - default: > - -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.err.encoding=UTF-8 - -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -XX:+ExitOnOutOfMemoryError -Djava.security.egd=file:/dev/urandom -XX:+UseParallelGC - -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:FlightRecorderOptions=stackdepth=512 - description: "Other JVM settings" - type: "str" keycloak_quarkus_java_opts: - default: "{{ ' '.join((keycloak_quarkus_jgroups_opts, keycloak_quarkus_java_heap_opts, keycloak_quarkus_java_jvm_opts)) }}" - description: "JVM arguments, by default heap_opts + jvm_opts, if overriden it takes precedence over them" + # line 33 of defaults/main.yml + default: "-Xms1024m -Xmx2048m" + description: "Additional JVM options" type: "str" - keycloak_quarkus_additional_env_vars: - default: "[]" - description: "List of additional env variables of { key: str, value: str} to be put in sysconfig file" - type: "list" keycloak_quarkus_ha_enabled: + # line 36 of defaults/main.yml default: false description: "Enable auto configuration for database backend, clustering and remote caches on infinispan" type: "bool" - keycloak_quarkus_ha_discovery: - default: "TCPPING" - description: "Discovery protocol for HA cluster members" - type: "str" keycloak_quarkus_db_enabled: + # line 38 of defaults/main.yml default: "{{ True if keycloak_quarkus_ha_enabled else False }}" description: "Enable auto configuration for database backend" type: "str" keycloak_quarkus_http_relative_path: - required: false - default: / - description: "Set the path relative to / for serving resources. The path must start with a /" - type: "str" - keycloak_quarkus_http_management_relative_path: - required: false - description: "Set the path relative to / for serving resources from management interface. The path must start with a /. If not given, the value is inherited from HTTP options. Relevant only when something is exposed on the management interface - see the guide for details." + # line 41 of defaults/main.yml + default: "auth" + description: "Service context path" type: "str" keycloak_quarkus_frontend_url: - required: false - description: "Deprecated in v26, use keycloak_quarkus_hostname instead." - type: "str" - keycloak_quarkus_hostname_admin: - required: false - description: "Service URL for the admin console" - type: "str" - keycloak_quarkus_admin_url: - required: false - description: "Deprecated in v26, use keycloak_quarkus_hostname_admin instead." + # line 41 of defaults/main.yml + default: "http://localhost:8080/auth" + description: "Service public URL" type: "str" keycloak_quarkus_metrics_enabled: + # line 43 of defaults/main.yml default: false description: "Whether to enable metrics" type: "bool" - keycloak_quarkus_health_enabled: - default: true - description: "If the server should expose health check endpoints on the management interface" - type: "bool" - keycloak_quarkus_cache_remote: - description: "Whether to connect to remote cache infinispan server" - default: false - type: 'bool' - keycloak_quarkus_cache_remote_username: + keycloak_quarkus_ispn_user: + # line 46 of defaults/main.yml default: "supervisor" description: "Username for connecting to infinispan" type: "str" - keycloak_quarkus_cache_remote_password: + keycloak_quarkus_ispn_pass: + # line 47 of defaults/main.yml default: "supervisor" description: "Password for connecting to infinispan" type: "str" - keycloak_quarkus_cache_remote_host: + keycloak_quarkus_ispn_url: + # line 48 of defaults/main.yml default: "localhost" - description: "Hostname for connecting to infinispan" + description: "URL for connecting to infinispan" type: "str" - keycloak_quarkus_cache_remote_port: - default: "11222" - description: "Port for connecting to infinispan" - type: "str" - keycloak_quarkus_cache_remote_sasl_mechanism: + keycloak_quarkus_ispn_sasl_mechanism: + # line 49 of defaults/main.yml default: "SCRAM-SHA-512" description: "Infinispan auth mechanism" type: "str" - keycloak_quarkus_cache_remote_tls_enabled: + keycloak_quarkus_ispn_use_ssl: + # line 50 of defaults/main.yml default: false description: "Whether infinispan uses TLS connection" type: "bool" - keycloak_quarkus_db_engine: + keycloak_quarkus_ispn_trust_store_path: + # line 52 of defaults/main.yml + default: "/etc/pki/java/cacerts" + description: "Path to infinispan server trust certificate" + type: "str" + keycloak_quarkus_ispn_trust_store_password: + # line 53 of defaults/main.yml + default: "changeit" + description: "Password for infinispan certificate keystore" + type: "str" + keycloak_quarkus_jdbc_engine: + # line 56 of defaults/main.yml default: "postgres" - description: "Database engine [mariadb,postres,mssql]" + description: "Database engine [mariadb,postres]" type: "str" keycloak_quarkus_db_user: + # line 58 of defaults/main.yml default: "keycloak-user" description: "User for database connection" type: "str" keycloak_quarkus_db_pass: + # line 59 of defaults/main.yml default: "keycloak-pass" description: "Password for database connection" type: "str" - keycloak_quarkus_db_url: - default: "{{ keycloak_quarkus_default_jdbc[keycloak_quarkus_db_engine].url }}" + keycloak_quarkus_jdbc_url: + # line 60 of defaults/main.yml + default: "{{ keycloak_quarkus_default_jdbc[keycloak_quarkus_jdbc_engine].url }}" description: "JDBC URL for connecting to database" type: "str" - keycloak_quarkus_db_driver_version: - default: "{{ keycloak_quarkus_default_jdbc[keycloak_quarkus_db_engine].version }}" + keycloak_quarkus_jdbc_driver_version: + # line 61 of defaults/main.yml + default: "{{ keycloak_quarkus_default_jdbc[keycloak_quarkus_jdbc_engine].version }}" description: "Version for JDBC driver" type: "str" - keycloak_quarkus_log: - default: "file" - type: "str" - description: "Enable one or more log handlers in a comma-separated list" - keycloak_quarkus_log_level: - default: "info" - type: "str" - description: "The log level of the root category or a comma-separated list of individual categories and their levels" - keycloak_quarkus_log_file: - default: "data/log/keycloak.log" - type: "str" - description: "Set the log file path and filename relative to keycloak home" - keycloak_quarkus_log_format: - default: '%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n' - type: "str" - description: "Set a format specific to file log entries" - keycloak_quarkus_log_target: - default: '/var/log/keycloak' - type: "str" - description: "Set the destination of the keycloak log folder link" - keycloak_quarkus_log_max_file_size: - default: 10M - type: "str" - description: > - Set the maximum log file size before a log rotation happens; A size configuration option recognises string in this format (shown as a regular - expression): [0-9]+[KkMmGgTtPpEeZzYy]?. If no suffix is given, assume bytes. - keycloak_quarkus_log_max_backup_index: - default: 10 - type: "str" - description: "Set the maximum number of archived log files to keep" - keycloak_quarkus_log_file_suffix: - default: '.yyyy-MM-dd.zip' - type: "str" - description: > - Set the log file handler rotation file suffix. When used, the file will be rotated based on its suffix. Note: If the suffix ends - with .zip or .gz, the rotation file will also be compressed. - keycloak_quarkus_proxy_mode: - default: 'edge' - type: "str" - description: "The proxy address forwarding mode if the server is behind a reverse proxy. Set to 'none' if not using a proxy" - keycloak_quarkus_proxy_headers: - default: "" - type: "str" - description: "Parse reverse proxy headers (`forwarded` or `xforwarded`), overrides the deprecated keycloak_quarkus_proxy_mode argument" - keycloak_quarkus_start_dev: - default: false - type: "bool" - description: "Whether to start the service in development mode (start-dev)" - keycloak_quarkus_transaction_xa_enabled: - default: true - type: "bool" - description: "Enable or disable XA transactions which may not be supported by some DBMS" - keycloak_quarkus_hostname_strict: - default: true - type: "bool" - description: > - Disables dynamically resolving the hostname from request headers. Should always be set to true in production, unless - proxy verifies the Host header. - keycloak_quarkus_hostname_backchannel_dynamic: - default: false - type: "bool" - description: > - Enables dynamic resolving of backchannel URLs, including hostname, scheme, port and context path. - Set to true if your application accesses Keycloak via a private network. If set to true, hostname option needs to be specified as a full URL. - keycloak_quarkus_spi_sticky_session_encoder_infinispan_should_attach_route: - default: true - type: "bool" - description: > - If the route should be attached to cookies to reflect the node that owns a particular session. If false, route is not attached to cookies - and we rely on the session affinity capabilities from reverse proxy - keycloak_quarkus_ks_vault_enabled: - default: false - type: "bool" - description: "Whether to enable vault SPI" - keycloak_quarkus_ks_vault_file: - default: "{{ keycloak_quarkus_config_dir }}/keystore.p12" - type: "str" - description: "The keystore path for the vault SPI" - keycloak_quarkus_ks_vault_type: - default: "PKCS12" - type: "str" - description: "Type of the keystore used for the vault SPI" - keycloak_quarkus_ks_vault_pass: - required: false - type: "str" - description: "The password for accessing the keystore vault SPI" - keycloak_quarkus_systemd_wait_for_port: - description: 'Whether systemd unit should wait for keycloak port before returning' - default: "{{ keycloak_quarkus_ha_enabled }}" - type: "bool" - keycloak_quarkus_systemd_wait_for_port_number: - default: "{{ keycloak_quarkus_https_port }}" - description: "The port the systemd unit should wait for, by default the https port" - type: "int" - keycloak_quarkus_systemd_wait_for_log: - description: 'Whether systemd unit should wait for service to be up in logs' - default: false - type: "bool" - keycloak_quarkus_systemd_wait_for_timeout: - description: "How long to wait for service to be alive (seconds)" - default: 60 - type: 'int' - keycloak_quarkus_systemd_wait_for_delay: - description: "Activation delay for service systemd unit (seconds)" - default: 10 - type: 'int' - keycloak_quarkus_providers: - description: > - List of provider definition dicts: { 'id': str, 'spi': str, 'url': str, 'local_path': str, - 'maven': { - 'repository_url': str, 'group_id': str, 'artifact_id': str, 'version': str, 'username': str, optional, 'password': str, optional - }, - 'default': bool, - 'properties': list of key/value } - default: [] - type: "list" - keycloak_quarkus_supported_policy_types: - description: "List of str of supported policy types" - default: ['password-blacklists'] - type: "list" - keycloak_quarkus_policies: - description: "List of policy definition dicts: { 'name': str, 'url': str, 'type': str }" - default: [] - type: "list" - keycloak_quarkus_jdbc_download_url: - description: "Override the default Maven Central download URL for the JDBC driver" - type: "str" - keycloak_quarkus_jdbc_download_user: - description: "Set a username with which to authenticate when downloading JDBC drivers from an alternative location" - type: "str" - keycloak_quarkus_jdbc_download_pass: - description: > - Set a password with which to authenticate when downloading JDBC drivers from an alternative location - (requires `keycloak_quarkus_jdbc_download_user``) - type: "str" - keycloak_quarkus_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_quarkus_restart_health_check: - default: true - description: "Whether to wait for successful health check after restart" - type: "bool" - keycloak_quarkus_restart_strategy: - description: > - Strategy task file for restarting in HA, one of restart/[ 'serial', 'none', 'serial_then_parallel' ].yml, or path to - file when providing custom strategy; when keycloak_quarkus_ha_enabled and keycloak_quarkus_restart_health_check == true - default: "restart/serial.yml" - type: "str" - keycloak_quarkus_restart_pause: - description: "Seconds to wait between restarts in HA strategy" - default: 15 - type: int - keycloak_quarkus_restart_health_check_delay: - description: "Seconds to let pass before starting healch checks" - default: 10 - type: 'int' - keycloak_quarkus_restart_health_check_retries: - description: "Number of attempts for successful health check before failing" - default: 25 - type: 'int' - keycloak_quarkus_show_deprecation_warnings: - default: true - description: "Whether or not deprecation warnings should be shown" - type: "bool" - keycloak_quarkus_download_path: - description: "Path local to controller for offline/download of install archives" - default: "{{ lookup('env', 'PWD') }}" - type: "str" - keycloak_quarkus_cache_metrics_enabled: - description: 'Enable histograms for metrics for the embedded caches' - default: false - type: 'bool' - keycloak_quarkus_cache_embedded_authorization_max_count: - description: 'The maximum number of entries that can be stored in-memory by the authorization cache' - required: false - type: "int" - keycloak_quarkus_cache_embedded_client_sessions_max_count: - description: 'The maximum number of entries that can be stored in-memory by the clientSessions cache' - required: false - type: "int" - keycloak_quarkus_cache_embedded_crl_max_count: - description: 'The maximum number of entries that can be stored in-memory by the crl cache' - required: false - type: "int" - keycloak_quarkus_cache_embedded_keys_max_count: - description: 'The maximum number of entries that can be stored in-memory by the keys cache' - required: false - type: "int" - keycloak_quarkus_cache_embedded_offline_client_sessions_max_count: - description: 'The maximum number of entries that can be stored in-memory by the offlineClientSessions cache' - required: false - type: "int" - keycloak_quarkus_cache_embedded_offline_sessions_max_count: - description: 'The maximum number of entries that can be stored in-memory by the offlineSessions cache' - required: false - type: "int" - keycloak_quarkus_cache_embedded_realms_max_count: - description: 'The maximum number of entries that can be stored in-memory by the realms cache' - required: false - type: "int" - keycloak_quarkus_cache_embedded_sessions_max_count: - description: 'The maximum number of entries that can be stored in-memory by the sessions cache' - required: false - type: "int" - keycloak_quarkus_cache_embedded_users_max_count: - description: 'The maximum number of entries that can be stored in-memory by the users cache' - required: false - type: 'int' - keycloak_quarkus_cache_embedded_mtls_enabled: - description: 'Encrypts the network communication between Keycloak servers' - default: true - type: 'bool' - keycloak_quarkus_cache_embedded_mtls_key_store_file: - description: 'The Keystore file path' - default: "{{ keycloak.home }}/conf/cache_key_store.p12" - type: "str" - keycloak_quarkus_cache_embedded_mtls_key_store_password: - description: 'The password to access the Keystore' - default: '' - type: "str" - keycloak_quarkus_cache_embedded_mtls_rotation_interval_days: - description: 'Rotation period in days of automatic JGroups MTLS certificates' - default: 30 - type: 'int' - keycloak_quarkus_cache_embedded_mtls_trust_store_file: - description: 'The Truststore file path' - default: "{{ keycloak.home }}/conf/cache_trust_store.p12" - type: "str" - keycloak_quarkus_cache_embedded_mtls_trust_store_password: - description: 'The password to access the Truststore.' - default: '' - type: "str" - keycloak_quarkus_jgroups_port: - description: 'jgroups bind port' - default: 7800 - type: "int" - keycloak_quarkus_jgroups_bind_address: - description: 'jgroups bind address' - default: "{{ ansible_default_ipv4.address }}" - type: "str" - keycloak_quarkus_jgroups_external_addr: - description: 'IP address that other instances in the Keycloak should use to contact this node' - default: "{{ keycloak_quarkus_jgroups_bind_address }}" - type: "str" - keycloak_quarkus_jgroups_external_port: - description: 'Port that other instances in the Keycloak cluster should use to contact this node' - default: "{{ keycloak_quarkus_jgroups_port }}" - type: "int" - keycloak_quarkus_jgroups_opts: - description: "JVM arguments for jgroups configuration" - default: "-Djgroups.bind.address={{ keycloak_quarkus_jgroups_bind_address }} -Djgroups.external_port={{ keycloak_quarkus_jgroups_external_port }} -Djgroups.external_addr={{ keycloak_quarkus_jgroups_external_addr }}" - type: "str" - downstream: - options: - rhbk_version: - default: "26.2.4" - description: "Red Hat Build of Keycloak version" - type: "str" - rhbk_archive: - default: "rhbk-{{ rhbk_version }}.zip" - description: "Red Hat Build of Keycloak install archive filename" - type: "str" - rhbk_dest: - default: "/opt/rhbk" - description: "Root installation directory" - type: "str" - rhbk_installdir: - default: "{{ rhbk_dest }}/rhbk-{{ rhbk_version }}" - description: "Installation path for Red Hat Build of Keycloak" - type: "str" - rhbk_apply_patches: - default: false - description: "Install Red Hat Build of Keycloak most recent cumulative patch" - type: "bool" - rhbk_enable: - default: true - description: "Enable Red Hat Build of Keycloak installation" - type: "bool" - rhbk_offline_install: - default: false - description: "Perform an offline install" - type: "bool" - rhbk_service_name: - default: "rhbk" - description: "systemd service name for Red Hat Build of Keycloak" - type: "str" - rhbk_service_desc: - default: "Red Hat Build of Keycloak" - description: "systemd description for Red Hat Build of Keycloak" - type: "str" - rhbk_patch_version: - required: false - description: "Red Hat Build of Keycloak latest cumulative patch version to apply; defaults to latest version when rhbk_apply_patches is True" - type: "str" - rhbk_patch_bundle: - default: "rhbk-{{ rhbk_patch_version | default('[0-9]+[.][0-9]+[.][0-9]+') }}-patch.zip" - description: "Red Hat Build of Keycloak patch archive filename" - type: "str" - rhbk_product_category: - default: "rhbk" - description: "JBossNetwork API category for Red Hat Build of Keycloak" - type: "str" diff --git a/roles/keycloak_quarkus/meta/main.yml b/roles/keycloak_quarkus/meta/main.yml index 65b5e50..fd6a110 100644 --- a/roles/keycloak_quarkus/meta/main.yml +++ b/roles/keycloak_quarkus/meta/main.yml @@ -1,4 +1,6 @@ --- +collections: + galaxy_info: role_name: keycloak_quarkus namespace: middleware_automation @@ -8,17 +10,12 @@ galaxy_info: license: Apache License 2.0 - min_ansible_version: "2.16" + min_ansible_version: "2.9" platforms: - - name: EL - versions: - - "8" - - "9" - - name: Fedora - - name: Debian - - name: Ubuntu - + - name: EL + versions: + - 8 galaxy_tags: - keycloak @@ -29,5 +26,3 @@ galaxy_info: - authentication - identity - security - - rhbk - - debian diff --git a/roles/keycloak_quarkus/tasks/bootstrapped.yml b/roles/keycloak_quarkus/tasks/bootstrapped.yml deleted file mode 100644 index 3cbc5c4..0000000 --- a/roles/keycloak_quarkus/tasks/bootstrapped.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -- name: Save ansible custom facts - become: true - ansible.builtin.template: - src: keycloak.fact.j2 - dest: /etc/ansible/facts.d/keycloak.fact - mode: '0644' - vars: - bootstrapped: true - -- name: Refresh custom facts - ansible.builtin.setup: - filter: ansible_local - -- name: Ensure that `KEYCLOAK_ADMIN[_PASSWORD]` get purged - ansible.builtin.include_tasks: systemd.yml diff --git a/roles/keycloak_quarkus/tasks/config_store.yml b/roles/keycloak_quarkus/tasks/config_store.yml deleted file mode 100644 index 2d8b39e..0000000 --- a/roles/keycloak_quarkus/tasks/config_store.yml +++ /dev/null @@ -1,52 +0,0 @@ ---- -- name: "Initialize configuration key store variables to be written" - ansible.builtin.set_fact: - store_items: - - key: "kc.db-password" - value: "{{ keycloak_quarkus_db_pass }}" - -- name: "Initialize empty configuration key store" - become: true - # keytool doesn't allow creating an empty key store, so this is a hacky way around it - ansible.builtin.shell: | # noqa blocked_modules shell is necessary here - set -o nounset # abort on unbound variable - set -o pipefail # do not hide errors within pipes - set -o errexit # abort on nonzero exit status - - echo dummy | keytool -noprompt -importpass -alias dummy -keystore {{ keycloak_quarkus_config_key_store_file | quote }} -storepass {{ keycloak_quarkus_config_key_store_password | quote }} -storetype PKCS12 - keytool -delete -alias dummy -keystore {{ keycloak_quarkus_config_key_store_file | quote }} -storepass {{ keycloak_quarkus_config_key_store_password | quote }} - args: - creates: "{{ keycloak_quarkus_config_key_store_file }}" - -- name: "Set configuration key store using keytool" - ansible.builtin.shell: | # noqa blocked_modules shell is necessary here - set -o nounset # abort on unbound variable - set -o pipefail # do not hide errors within pipes - - keytool -list -alias {{ item.key | quote }} -keystore {{ keycloak_quarkus_config_key_store_file | quote }} -storepass {{ keycloak_quarkus_config_key_store_password | quote }} - retVal=$? - - set -o errexit # abort on nonzero exit status - - if [ $retVal -eq 0 ]; then - # value is already in keystore, but keytool has no replace function: delete and re-create instead - # note that we can not read whether the value has changed either[^1], so we need to override it - # [^1]: https://stackoverflow.com/a/37491400 - keytool -delete -alias {{ item.key | quote }} -keystore {{ keycloak_quarkus_config_key_store_file | quote }} -storepass {{ keycloak_quarkus_config_key_store_password | quote }} - fi - - echo {{ item.value | quote }} | keytool -noprompt -importpass -alias {{ item.key | quote }} -keystore {{ keycloak_quarkus_config_key_store_file | quote }} -storepass {{ keycloak_quarkus_config_key_store_password | quote }} -storetype PKCS12 - loop: "{{ store_items }}" - no_log: true - become: true - changed_when: true - notify: - - restart keycloak - -- name: "Set owner of configuration key store {{ keycloak_quarkus_config_key_store_file }}" - ansible.builtin.file: - path: "{{ keycloak_quarkus_config_key_store_file }}" - owner: "{{ keycloak.service_user }}" - group: "{{ keycloak.service_group }}" - mode: '0400' - become: true diff --git a/roles/keycloak_quarkus/tasks/debian.yml b/roles/keycloak_quarkus/tasks/debian.yml deleted file mode 100644 index 7e59204..0000000 --- a/roles/keycloak_quarkus/tasks/debian.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- name: Include firewall config tasks - ansible.builtin.include_tasks: - file: iptables.yml - apply: - tags: - - firewall - when: keycloak_quarkus_configure_iptables - tags: - - firewall diff --git a/roles/keycloak_quarkus/tasks/deprecations.yml b/roles/keycloak_quarkus/tasks/deprecations.yml deleted file mode 100644 index 0d370d5..0000000 --- a/roles/keycloak_quarkus/tasks/deprecations.yml +++ /dev/null @@ -1,162 +0,0 @@ ---- -- name: Check deprecation keycloak_quarkus_key_store -> keycloak_quarkus_http_key_store - delegate_to: localhost - run_once: true - when: - - keycloak_quarkus_https_key_store_enabled - block: - - name: Ensure backward compatibility for `keycloak_quarkus_key_store_file`, superseded by `keycloak_quarkus_https_key_store_file` - when: - - keycloak_quarkus_key_store_file is defined - - keycloak_quarkus_key_store_file != '' - - keycloak_quarkus_https_key_store_file == keycloak.home + "/conf/key_store.p12" # default value - changed_when: keycloak_quarkus_show_deprecation_warnings - ansible.builtin.set_fact: - keycloak_quarkus_https_key_store_file: "{{ keycloak_quarkus_key_store_file }}" - deprecated_variable: "keycloak_quarkus_key_store_file" # read in deprecation handler - notify: - - print deprecation warning - - - name: Flush handlers - ansible.builtin.meta: flush_handlers - - - name: Ensure backward compatibility for `keycloak_quarkus_key_store_password`, superseded by `keycloak_quarkus_https_key_store_password` - when: - - keycloak_quarkus_key_store_password is defined - - keycloak_quarkus_key_store_password != '' - - keycloak_quarkus_https_key_store_password == "" # default value - changed_when: keycloak_quarkus_show_deprecation_warnings - ansible.builtin.set_fact: - keycloak_quarkus_https_key_store_password: "{{ keycloak_quarkus_key_store_password }}" - deprecated_variable: "keycloak_quarkus_key_store_password" # read in deprecation handler - notify: - - print deprecation warning - - - name: Flush handlers - ansible.builtin.meta: flush_handlers - -# https://access.redhat.com/documentation/en-us/red_hat_build_of_keycloak/24.0/html-single/upgrading_guide/index#deprecated_literal_proxy_literal_option -- name: Check deprecation of keycloak_quarkus_proxy_mode - when: - - keycloak_quarkus_proxy_mode is defined - - keycloak_quarkus_proxy_headers is defined and keycloak_quarkus_proxy_headers | length == 0 - - keycloak_quarkus_version.split('.') | first | int >= 24 - delegate_to: localhost - run_once: true - changed_when: keycloak_quarkus_show_deprecation_warnings - ansible.builtin.set_fact: - deprecated_variable: "keycloak_quarkus_proxy_mode" # read in deprecation handler - notify: - - print deprecation warning - -# https://docs.redhat.com/en/documentation/red_hat_build_of_keycloak/26.0/html-single/upgrading_guide/index#new_hostname_options -- name: Check deprecation of keycloak_quarkus_frontend_url -> keycloak_quarkus_hostname - when: - - keycloak_quarkus_hostname is not defined - - keycloak_quarkus_frontend_url is defined - - keycloak_quarkus_frontend_url != '' - delegate_to: localhost - run_once: true - changed_when: keycloak_quarkus_show_deprecation_warnings - ansible.builtin.set_fact: - keycloak_quarkus_hostname: "{{ keycloak_quarkus_frontend_url }}" - deprecated_variable: "keycloak_quarkus_frontend_url" # read in deprecation handler - notify: - - print deprecation warning - -# https://docs.redhat.com/en/documentation/red_hat_build_of_keycloak/26.0/html-single/upgrading_guide/index#new_hostname_options -- name: Check deprecation of keycloak_quarkus_hostname_strict_https + keycloak_quarkus_host + keycloak_quarkus_port + keycloak_quarkus_path -> keycloak_quarkus_hostname - when: - - keycloak_quarkus_hostname is not defined - - keycloak_quarkus_hostname_strict_https is defined or keycloak_quarkus_frontend_url is defined or keycloak_quarkus_port is defined or keycloak_quarkus_path is defined - delegate_to: localhost - run_once: true - changed_when: keycloak_quarkus_show_deprecation_warnings - ansible.builtin.set_fact: - keycloak_quarkus_hostname: >- - {% set protocol = '' %} - {% if keycloak_quarkus_hostname_strict_https %} - {% set protocol = 'https://' %} - {% elif keycloak_quarkus_hostname_strict_https is defined and keycloak_quarkus_hostname_strict_https is False %} - {% set protocol = 'http://' %} - {% endif %} - {{ protocol }}{{ keycloak_quarkus_host }}:{{ keycloak_quarkus_port }}/{{ keycloak_quarkus_path }} - deprecated_variable: "keycloak_quarkus_hostname_strict_https or keycloak_quarkus_frontend_url or keycloak_quarkus_frontend_url or keycloak_quarkus_hostname" # read in deprecation handler - notify: - - print deprecation warning - -# https://docs.redhat.com/en/documentation/red_hat_build_of_keycloak/26.0/html-single/upgrading_guide/index#new_hostname_options -- name: Check deprecation of keycloak_quarkus_admin_url -> keycloak_quarkus_hostname_admin - when: - - keycloak_quarkus_hostname_admin is not defined - - keycloak_quarkus_admin_url is defined - - keycloak_quarkus_admin_url != '' - delegate_to: localhost - run_once: true - changed_when: keycloak_quarkus_show_deprecation_warnings - ansible.builtin.set_fact: - keycloak_quarkus_hostname_admin: "{{ keycloak_quarkus_admin_url }}" - deprecated_variable: "keycloak_quarkus_admin_url" # read in deprecation handler - notify: - - print deprecation warning - -# https://docs.redhat.com/en/documentation/red_hat_build_of_keycloak/26.0/html-single/upgrading_guide/index#new_hostname_options -- name: Check deprecation of keycloak_quarkus_hostname_strict_backchannel -> keycloak_quarkus_hostname_backchannel_dynamic - when: - - keycloak_quarkus_hostname_backchannel_dynamic is not defined - - keycloak_quarkus_hostname_strict_backchannel is defined - - keycloak_quarkus_hostname_strict_backchannel != '' - delegate_to: localhost - run_once: true - changed_when: keycloak_quarkus_show_deprecation_warnings - ansible.builtin.set_fact: - keycloak_quarkus_hostname_backchannel_dynamic: "{{ keycloak_quarkus_hostname_strict_backchannel == False }}" - deprecated_variable: "keycloak_quarkus_hostname_backchannel_dynamic" # read in deprecation handler - notify: - - print deprecation warning - -# https://github.com/keycloak/keycloak/issues/30009 -- name: Check deprecation of keycloak_quarkus_admin_user -> keycloak_quarkus_bootstrap_admin_user - when: - - keycloak_quarkus_bootstrap_admin_user is not defined - - keycloak_quarkus_admin_user is defined - - keycloak_quarkus_admin_user != '' - delegate_to: localhost - run_once: true - changed_when: keycloak_quarkus_show_deprecation_warnings - ansible.builtin.set_fact: - keycloak_quarkus_bootstrap_admin_user: "{{ keycloak_quarkus_admin_user }}" - deprecated_variable: "keycloak_quarkus_admin_user" # read in deprecation handler - notify: - - print deprecation warning - -# https://github.com/keycloak/keycloak/issues/30009 -- name: Check deprecation of keycloak_quarkus_admin_pass -> keycloak_quarkus_bootstrap_admin_password - when: - - keycloak_quarkus_bootstrap_admin_password is not defined - - keycloak_quarkus_admin_pass is defined - - keycloak_quarkus_admin_pass != '' - delegate_to: localhost - run_once: true - changed_when: keycloak_quarkus_show_deprecation_warnings - ansible.builtin.set_fact: - keycloak_quarkus_bootstrap_admin_user: "{{ keycloak_quarkus_admin_pass }}" - deprecated_variable: "keycloak_quarkus_admin_pass" # read in deprecation handler - notify: - - print deprecation warning - -- name: Check deprecation of keycloak_quarkus_bind_address -> keycloak_quarkus_http_host - when: - - keycloak_quarkus_bind_address is defined - - keycloak_quarkus_bind_address != '0.0.0.0' - delegate_to: localhost - run_once: true - changed_when: keycloak_quarkus_show_deprecation_warnings - ansible.builtin.set_fact: - keycloak_quarkus_http_host: "{{ keycloak_quarkus_bind_address }}" - deprecated_variable: "keycloak_quarkus_bind_address" # read in deprecation handler - notify: - - print deprecation warning - -- name: Flush handlers - ansible.builtin.meta: flush_handlers diff --git a/roles/keycloak_quarkus/tasks/fastpackages.yml b/roles/keycloak_quarkus/tasks/fastpackages.yml index 9dc1621..78bc556 100644 --- a/roles/keycloak_quarkus/tasks/fastpackages.yml +++ b/roles/keycloak_quarkus/tasks/fastpackages.yml @@ -1,31 +1,22 @@ --- -- name: "Check if packages are already installed" # noqa command-instead-of-module this runs faster - ansible.builtin.command: "rpm -q {{ packages_list | join(' ') }}" - register: rpm_info - changed_when: false - failed_when: false - when: ansible_facts.os_family == "RedHat" +- name: Check packages to be installed + block: + - name: "Check if packages are already installed" + ansible.builtin.command: "rpm -q {{ packages_list | join(' ') }}" + args: + warn: no + register: rpm_info + changed_when: rpm_info.failed -- name: "Add missing packages to the yum install list" - ansible.builtin.set_fact: - packages_to_install: "{{ packages_to_install | default([]) + rpm_info.stdout_lines | \ - map('regex_findall', 'package (.+) is not installed$') | default([]) | flatten }}" - when: ansible_facts.os_family == "RedHat" + rescue: + - name: "Add missing packages to the yum install list" + ansible.builtin.set_fact: + packages_to_install: "{{ packages_to_install | default([]) + rpm_info.stdout_lines | map('regex_findall', 'package (.+) is not installed$') | flatten }}" + when: rpm_info.failed - name: "Install packages: {{ packages_to_install }}" - become: true - ansible.builtin.dnf: + become: yes + ansible.builtin.yum: name: "{{ packages_to_install }}" state: present - when: - - packages_to_install | default([]) | length > 0 - - ansible_facts.os_family == "RedHat" - -- name: "Install packages: {{ packages_list }}" - become: true - ansible.builtin.package: - name: "{{ packages_list }}" - state: present - when: - - packages_list | default([]) | length > 0 - - ansible_facts.os_family == "Debian" + when: packages_to_install | default([]) | length > 0 \ No newline at end of file diff --git a/roles/keycloak_quarkus/tasks/firewalld.yml b/roles/keycloak_quarkus/tasks/firewalld.yml index 2d48124..772ba3c 100644 --- a/roles/keycloak_quarkus/tasks/firewalld.yml +++ b/roles/keycloak_quarkus/tasks/firewalld.yml @@ -6,31 +6,20 @@ - firewalld - name: Enable and start the firewalld service - become: true + become: yes ansible.builtin.systemd: name: firewalld - enabled: true + enabled: yes state: started -- name: "Configure firewall for {{ keycloak.service_name }} http port" - become: true - ansible.posix.firewalld: +- name: "Configure firewall for {{ keycloak.service_name }} ports" + become: yes + firewalld: port: "{{ item }}" permanent: true state: enabled - immediate: true + immediate: yes loop: - "{{ keycloak_quarkus_http_port }}/tcp" - when: keycloak_quarkus_http_enabled | bool - -- name: "Configure firewall for {{ keycloak.service_name }} ports" - become: true - ansible.posix.firewalld: - port: "{{ item }}" - permanent: true - state: enabled - immediate: true - loop: - "{{ keycloak_quarkus_https_port }}/tcp" - - "{{ keycloak_quarkus_http_management_port }}/tcp" - "{{ keycloak_quarkus_jgroups_port }}/tcp" diff --git a/roles/keycloak_quarkus/tasks/install.yml b/roles/keycloak_quarkus/tasks/install.yml index b188e6c..ba6ec40 100644 --- a/roles/keycloak_quarkus/tasks/install.yml +++ b/roles/keycloak_quarkus/tasks/install.yml @@ -8,38 +8,30 @@ - keycloak_quarkus_archive is defined - keycloak_quarkus_download_url is defined - keycloak_quarkus_version is defined - - local_path is defined quiet: true - name: Check for an existing deployment - become: true + become: yes ansible.builtin.stat: path: "{{ keycloak.home }}" register: existing_deploy - name: "Create {{ keycloak.service_name }} service user/group" - become: true + become: yes ansible.builtin.user: name: "{{ keycloak.service_user }}" home: /opt/keycloak - system: true - create_home: false + system: yes + create_home: no - name: "Create {{ keycloak.service_name }} install location" - become: true + become: yes ansible.builtin.file: dest: "{{ keycloak_quarkus_dest }}" state: directory owner: "{{ keycloak.service_user }}" group: "{{ keycloak.service_group }}" - mode: '0750' - -- name: Create directory for ansible custom facts - become: true - ansible.builtin.file: - state: directory - recurse: true - path: /etc/ansible/facts.d + mode: 0750 ## check remote archive - name: Set download archive path @@ -47,93 +39,35 @@ archive: "{{ keycloak_quarkus_dest }}/{{ keycloak.bundle }}" - name: Check download archive path - become: true + become: yes ansible.builtin.stat: path: "{{ archive }}" register: archive_path ## download to controller +- name: Check local download archive path + ansible.builtin.stat: + path: "{{ lookup('env', 'PWD') }}" + register: local_path + delegate_to: localhost + - name: Download keycloak archive ansible.builtin.get_url: # noqa risky-file-permissions delegated, uses controller host user url: "{{ keycloak_quarkus_download_url }}" dest: "{{ local_path.stat.path }}/{{ keycloak.bundle }}" - mode: '0640' + mode: 0640 delegate_to: localhost - become: false - run_once: true when: - archive_path is defined - archive_path.stat is defined - not archive_path.stat.exists - not keycloak.offline_install - - not rhbk_enable is defined or not rhbk_enable - -- name: Perform download from RHN using JBoss Network API - delegate_to: localhost - run_once: true - when: - - archive_path is defined - - archive_path.stat is defined - - not archive_path.stat.exists - - rhbk_enable is defined and rhbk_enable - - not keycloak.offline_install - block: - - name: Retrieve product download using JBoss Network API - middleware_automation.common.product_search: - client_id: "{{ rhn_username }}" - client_secret: "{{ rhn_password }}" - product_type: DISTRIBUTION - product_version: "{{ rhbk_version }}" - product_category: "{{ rhbk_product_category }}" - register: 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: - rhn_filtered_products: "{{ rhn_products.results | selectattr('file_path', 'match', '[^/]*/' + rhbk_archive + '$') }}" - delegate_to: localhost - run_once: true - - - name: Download Red Hat Build of Keycloak - middleware_automation.common.product_download: # noqa risky-file-permissions delegated, uses controller host user - client_id: "{{ rhn_username }}" - client_secret: "{{ rhn_password }}" - product_id: "{{ (rhn_filtered_products | first).id }}" - dest: "{{ local_path.stat.path }}/{{ keycloak.bundle }}" - mode: '0640' - no_log: "{{ omit_rhn_output | default(true) }}" - delegate_to: localhost - run_once: true - become: false - -- name: Perform download of RHBK from alternate download location - delegate_to: localhost - run_once: true - become: false - when: - - archive_path is defined - - archive_path.stat is defined - - not archive_path.stat.exists - - rhbk_enable is defined and rhbk_enable - - not keycloak.offline_install - - keycloak_quarkus_alternate_download_url is defined - ansible.builtin.get_url: # noqa risky-file-permissions delegated, uses controller host user - url: "{{ keycloak_quarkus_alternate_download_url }}" - dest: "{{ local_path.stat.path }}/{{ keycloak.bundle }}" - mode: '0640' - url_username: "{{ keycloak_quarkus_download_user | default(omit) }}" - url_password: "{{ keycloak_quarkus_download_pass | default(omit) }}" - validate_certs: "{{ keycloak_quarkus_download_validate_certs | default(omit) }}" - name: Check downloaded archive ansible.builtin.stat: path: "{{ local_path.stat.path }}/{{ keycloak.bundle }}" register: local_archive_path delegate_to: localhost - become: false - run_once: true ## copy and unpack - name: Copy archive to target nodes @@ -142,31 +76,31 @@ dest: "{{ archive }}" owner: "{{ keycloak.service_user }}" group: "{{ keycloak.service_group }}" - mode: '0640' + mode: 0750 register: new_version_downloaded when: - not archive_path.stat.exists - local_archive_path.stat is defined - local_archive_path.stat.exists - become: true + become: yes -- name: "Check target directory: {{ keycloak.home }}/bin/" +- name: "Check target directory: {{ keycloak.home }}" ansible.builtin.stat: - path: "{{ keycloak.home }}/bin/" + path: "{{ keycloak.home }}" register: path_to_workdir - become: true + become: yes -- name: "Extract Keycloak archive on target" # noqa no-handler need to run this here +- name: "Extract Keycloak archive on target" ansible.builtin.unarchive: - remote_src: true + remote_src: yes src: "{{ archive }}" dest: "{{ keycloak_quarkus_dest }}" - creates: "{{ keycloak.home }}/bin/" + creates: "{{ keycloak.home }}" owner: "{{ keycloak.service_user }}" group: "{{ keycloak.service_group }}" - become: true + become: yes when: - - (not path_to_workdir.stat.exists) or new_version_downloaded.changed + - new_version_downloaded.changed or not path_to_workdir.stat.exists notify: - restart keycloak @@ -174,113 +108,4 @@ ansible.builtin.debug: msg: "{{ keycloak.home }} already exists and version unchanged, skipping decompression" when: - - (not new_version_downloaded.changed) and path_to_workdir.stat.exists - -- name: "Copy private key to target" - ansible.builtin.copy: - content: "{{ keycloak_quarkus_key_content }}" - dest: "{{ keycloak_quarkus_key_file }}" - owner: "{{ keycloak.service_user }}" - group: "{{ keycloak.service_group }}" - mode: '0640' - become: true - when: - - keycloak_quarkus_https_key_file_enabled is defined and keycloak_quarkus_https_key_file_enabled - - keycloak_quarkus_key_file_copy_enabled is defined and keycloak_quarkus_key_file_copy_enabled - - keycloak_quarkus_key_content | length > 0 - -- name: "Copy certificate to target" - ansible.builtin.copy: - src: "{{ keycloak_quarkus_cert_file_src }}" - dest: "{{ keycloak_quarkus_cert_file }}" - owner: "{{ keycloak.service_user }}" - group: "{{ keycloak.service_group }}" - mode: '0644' - become: true - when: - - keycloak_quarkus_https_key_file_enabled is defined and keycloak_quarkus_https_key_file_enabled - - keycloak_quarkus_cert_file_copy_enabled is defined and keycloak_quarkus_cert_file_copy_enabled - - keycloak_quarkus_cert_file_src | length > 0 - -- name: "Install {{ keycloak_quarkus_db_engine }} JDBC driver" - ansible.builtin.include_tasks: jdbc_driver.yml - when: - - rhbk_enable is defined and rhbk_enable - - keycloak_quarkus_default_jdbc[keycloak_quarkus_db_engine].driver_jar_url is defined - -- name: "Download custom providers via http" - ansible.builtin.get_url: - url: "{{ item.url }}" - dest: "{{ keycloak.home }}/providers/{{ item.id }}.jar" - owner: "{{ keycloak.service_user }}" - group: "{{ keycloak.service_group }}" - mode: '0640' - checksum: "{{ item.checksum | default(omit) }}" - become: true - loop: "{{ keycloak_quarkus_providers }}" - when: item.url is defined and item.url | length > 0 - notify: "{{ ['invalidate keycloak theme cache', 'rebuild keycloak config', 'restart keycloak'] if not item.restart is defined or item.restart else [] }}" - -# this requires the `lxml` package to be installed; we redirect this step to localhost such that we do need to install it on the remote hosts -- name: "Download custom providers to localhost using maven" - middleware_automation.common.maven_artifact: - repository_url: "{{ item.maven.repository_url }}" - group_id: "{{ item.maven.group_id }}" - artifact_id: "{{ item.maven.artifact_id }}" - version: "{{ item.maven.version | default(omit) }}" - username: "{{ item.maven.username | default(omit) }}" - password: "{{ item.maven.password | default(omit) }}" - dest: "{{ local_path.stat.path }}/{{ item.id }}.jar" - delegate_to: "localhost" - run_once: true - loop: "{{ keycloak_quarkus_providers }}" - when: item.maven is defined - no_log: "{{ item.maven.password is defined and item.maven.password | length > 0 | default(false) }}" - -- name: "Copy maven providers" - ansible.builtin.copy: - src: "{{ local_path.stat.path }}/{{ item.id }}.jar" - dest: "{{ keycloak.home }}/providers/{{ item.id }}.jar" - owner: "{{ keycloak.service_user }}" - group: "{{ keycloak.service_group }}" - mode: '0640' - checksum: "{{ item.checksum | default(omit) }}" - become: true - loop: "{{ keycloak_quarkus_providers }}" - when: item.maven is defined - no_log: "{{ item.maven.password is defined and item.maven.password | length > 0 | default(false) }}" - notify: "{{ ['invalidate keycloak theme cache', 'rebuild keycloak config', 'restart keycloak'] if not item.restart is defined or item.restart else [] }}" - -- name: "Copy local providers" - ansible.builtin.copy: - src: "{{ item.local_path }}" - dest: "{{ keycloak.home }}/providers/{{ item.id }}.jar" - owner: "{{ keycloak.service_user }}" - group: "{{ keycloak.service_group }}" - mode: '0640' - become: true - loop: "{{ keycloak_quarkus_providers }}" - when: item.local_path is defined - notify: "{{ ['invalidate keycloak theme cache', 'rebuild keycloak config', 'restart keycloak'] if not item.restart is defined or item.restart else [] }}" - -- name: Ensure required folder structure for policies exists - ansible.builtin.file: - path: "{{ keycloak.home }}/data/{{ item | lower }}" - state: directory - owner: "{{ keycloak.service_user }}" - group: "{{ keycloak.service_group }}" - mode: '0750' - become: true - loop: "{{ keycloak_quarkus_supported_policy_types }}" - -- name: "Install custom policies" - ansible.builtin.get_url: - url: "{{ item.url }}" - dest: "{{ keycloak.home }}/data/{{ item.type | default(keycloak_quarkus_supported_policy_types | first) | lower }}/{{ item.name }}" - owner: "{{ keycloak.service_user }}" - group: "{{ keycloak.service_group }}" - mode: '0640' - become: true - loop: "{{ keycloak_quarkus_policies }}" - when: item.url is defined and item.url | length > 0 - notify: "restart keycloak" + - not new_version_downloaded.changed and path_to_workdir.stat.exists diff --git a/roles/keycloak_quarkus/tasks/invalidate_theme_cache.yml b/roles/keycloak_quarkus/tasks/invalidate_theme_cache.yml deleted file mode 100644 index 90ff67f..0000000 --- a/roles/keycloak_quarkus/tasks/invalidate_theme_cache.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -# From https://docs.redhat.com/en/documentation/red_hat_build_of_keycloak/24.0/html/server_developer_guide/themes#creating_a_theme: -# If you want to manually delete the content of the themes cache, -# you can do so by deleting the data/tmp/kc-gzip-cache directory of the server distribution -# It can be useful for instance if you redeployed custom providers or custom themes without -# disabling themes caching in the previous server executions. -- name: "Delete {{ keycloak.service_name }} theme cache directory" - ansible.builtin.file: - path: "{{ keycloak.home }}/data/tmp/kc-gzip-cache" - state: absent - become: true diff --git a/roles/keycloak_quarkus/tasks/iptables.yml b/roles/keycloak_quarkus/tasks/iptables.yml deleted file mode 100644 index b487b89..0000000 --- a/roles/keycloak_quarkus/tasks/iptables.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -- name: Ensure required package iptables are installed - ansible.builtin.include_tasks: fastpackages.yml - vars: - packages_list: - - iptables - -- name: "Configure firewall ports for {{ keycloak.service_name }}" - become: 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_quarkus_http_port }}" - - "{{ keycloak_quarkus_https_port }}" - - "{{ keycloak_quarkus_jgroups_port }}" diff --git a/roles/keycloak_quarkus/tasks/jdbc_driver.yml b/roles/keycloak_quarkus/tasks/jdbc_driver.yml deleted file mode 100644 index ba3f4b8..0000000 --- a/roles/keycloak_quarkus/tasks/jdbc_driver.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -- 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_quarkus_jdbc_download_user is undefined and keycloak_quarkus_jdbc_download_pass is not undefined) or - (keycloak_quarkus_jdbc_download_pass is undefined and keycloak_quarkus_jdbc_download_user is not undefined) - -- name: "Retrieve JDBC Driver from {{ keycloak_jdbc_download_url | default(keycloak_quarkus_default_jdbc[keycloak_quarkus_db_engine].driver_jar_url) }}" - ansible.builtin.get_url: - url: "{{ keycloak_quarkus_jdbc_download_url | default(keycloak_quarkus_default_jdbc[keycloak_quarkus_db_engine].driver_jar_url) }}" - dest: "{{ keycloak.home }}/providers" - owner: "{{ keycloak.service_user }}" - group: "{{ keycloak.service_group }}" - url_username: "{{ keycloak_quarkus_jdbc_download_user | default(omit) }}" - url_password: "{{ keycloak_quarkus_jdbc_download_pass | default(omit) }}" - validate_certs: "{{ keycloak_quarkus_jdbc_download_validate_certs | default(omit) }}" - mode: '0640' - become: true - notify: - - restart keycloak diff --git a/roles/keycloak_quarkus/tasks/main.yml b/roles/keycloak_quarkus/tasks/main.yml index 6a7a4b0..d350f6f 100644 --- a/roles/keycloak_quarkus/tasks/main.yml +++ b/roles/keycloak_quarkus/tasks/main.yml @@ -1,128 +1,41 @@ --- # tasks file for keycloak + - name: Check prerequisites - ansible.builtin.include_tasks: - file: prereqs.yml - apply: - tags: - - prereqs + ansible.builtin.include_tasks: prereqs.yml tags: - prereqs - - always -- name: Check for deprecations - ansible.builtin.include_tasks: - file: deprecations.yml - apply: - tags: - - always +- name: Include firewall config tasks + ansible.builtin.include_tasks: firewalld.yml + when: keycloak_quarkus_configure_firewalld tags: - - always - -- name: Distro specific tasks - ansible.builtin.include_tasks: - file: "{{ ansible_os_family | lower }}.yml" - apply: - tags: - - unbound - tags: - - unbound + - firewall - name: Include install tasks - ansible.builtin.include_tasks: - file: install.yml - apply: - tags: - - install + ansible.builtin.include_tasks: install.yml tags: - install - name: Include systemd tasks - ansible.builtin.include_tasks: - file: systemd.yml - apply: - tags: - - systemd + ansible.builtin.include_tasks: systemd.yml tags: - systemd -- name: Include configuration key store tasks - ansible.builtin.include_tasks: - file: config_store.yml - apply: - tags: - - install - when: keycloak.config_key_store_enabled - tags: - - install - -- name: Create tcpping cluster node list - ansible.builtin.set_fact: - keycloak_quarkus_cluster_nodes: > - {{ keycloak_quarkus_cluster_nodes | default([]) + [ - { - "name": item, - "address": 'jgroups-' + item, - "inventory_host": hostvars[item].keycloak_quarkus_jgroups_ip | default(item) + '[' + (keycloak_quarkus_jgroups_port | string) + ']', - "value": hostvars[item].keycloak_quarkus_jgroups_ip | default(item) - } - ] }} - loop: "{{ ansible_play_batch }}" - when: keycloak_quarkus_ha_enabled and keycloak_quarkus_ha_discovery == 'TCPPING' - -- name: "Configure config files for keycloak service" +- name: "Configure config for keycloak service" ansible.builtin.template: - src: "{{ item }}.j2" - dest: "{{ keycloak.home }}/conf/{{ item }}" + src: keycloak.conf.j2 + dest: "{{ keycloak.home }}/conf/keycloak.conf" owner: "{{ keycloak.service_user }}" group: "{{ keycloak.service_group }}" - mode: '0640' - become: true - loop: - - keycloak.conf - - quarkus.properties - - cache-ispn.xml + mode: 0644 notify: - - rebuild keycloak config - restart keycloak -- name: Ensure logdirectory exists - ansible.builtin.file: - state: directory - path: "{{ keycloak.log.file | dirname }}" - owner: "{{ keycloak.service_user }}" - group: "{{ keycloak.service_group }}" - mode: '0775' - become: true - -- name: Flush pending handlers - ansible.builtin.meta: flush_handlers - - name: "Start and wait for keycloak service" ansible.builtin.include_tasks: start.yml -- name: Link default logs directory - ansible.builtin.file: - state: link - src: "{{ keycloak.log.file | dirname }}" - dest: "{{ keycloak_quarkus_log_target }}" - force: true - become: true - - name: Check service status - ansible.builtin.systemd_service: - name: "{{ keycloak.service_name }}" + ansible.builtin.command: "systemctl status keycloak" register: keycloak_service_status - changed_when: false - -- name: "Notify to remove `keycloak_quarkus_bootstrap_admin_user[_password]` env vars" - when: - - not ansible_local.keycloak.general.bootstrapped | default(false) | bool # it was not bootstrapped prior to the current role's execution - - keycloak_service_status.status.ActiveState == "active" # but it is now - ansible.builtin.assert: { that: true, quiet: true } - changed_when: true - notify: - - bootstrapped - -- name: Flush pending handlers - ansible.builtin.meta: flush_handlers + changed_when: False \ No newline at end of file diff --git a/roles/keycloak_quarkus/tasks/prereqs.yml b/roles/keycloak_quarkus/tasks/prereqs.yml index 9b633f3..ea2b8f4 100644 --- a/roles/keycloak_quarkus/tasks/prereqs.yml +++ b/roles/keycloak_quarkus/tasks/prereqs.yml @@ -2,150 +2,33 @@ - name: Validate admin console password ansible.builtin.assert: that: - - keycloak_quarkus_bootstrap_admin_password | length > 12 - quiet: true - fail_msg: "The console administrator password is empty or invalid. Please set the keycloak_quarkus_bootstrap_admin_password to a 12+ char long string" + - keycloak_quarkus_admin_pass | length > 12 + quiet: True + fail_msg: "The console administrator password is empty or invalid. Please set the keycloak_quarkus_admin_pass variable to a 12+ char long string" success_msg: "{{ 'Console administrator password OK' }}" -- name: Validate http_relative_path - ansible.builtin.assert: - that: - - keycloak_quarkus_http_relative_path is regex('^/.*') - quiet: true - fail_msg: "The relative path for keycloak_quarkus_http_relative_path must begin with /" - success_msg: "{{ 'Relative path OK' }}" - -- name: Validate http_management_relative_path - ansible.builtin.assert: - that: - - keycloak_quarkus_http_management_relative_path is regex('^/.*') - quiet: true - fail_msg: "The relative path for keycloak_quarkus_http_management_relative_path must begin with /" - success_msg: "{{ 'Relative mgmt path OK' }}" - when: keycloak_quarkus_http_management_relative_path is defined - - name: Validate configuration ansible.builtin.assert: that: - - (keycloak_quarkus_ha_enabled and keycloak_quarkus_db_enabled) or - (not keycloak_quarkus_ha_enabled and keycloak_quarkus_db_enabled) or - (not keycloak_quarkus_ha_enabled and not keycloak_quarkus_db_enabled) - quiet: true - fail_msg: "HA setup requires a backend database service. Check keycloak_quarkus_ha_enabled and keycloak_quarkus_db_enabled" + - (keycloak_quarkus_ha_enabled and keycloak_quarkus_db_enabled) or (not keycloak_quarkus_ha_enabled and keycloak_quarkus_db_enabled) or (not keycloak_quarkus_ha_enabled and not keycloak_quarkus_db_enabled) + quiet: True + fail_msg: "Cannot install HA setup without a backend database service. Check keycloak_quarkus_ha_enabled and keycloak_quarkus_db_enabled" success_msg: "{{ 'Configuring HA' if keycloak_quarkus_ha_enabled else 'Configuring standalone' }}" -- 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: Validate credentials +# ansible.builtin.assert: +# that: +# - (rhn_username is defined and keycloak_rhsso_enable) or not keycloak_rhsso_enable or keycloak_offline_install +# - (rhn_password is defined and keycloak_rhsso_enable) or not keycloak_rhsso_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 Red Hat Single Sign-On' if keycloak_rhsso_enable else 'Installing keycloak.org' }}" - name: Ensure required packages are installed ansible.builtin.include_tasks: fastpackages.yml vars: - packages_list: "{{ keycloak_quarkus_prereq_package_list }}" - -- name: Check local download archive path - ansible.builtin.stat: - path: "{{ keycloak_quarkus_download_path }}" - register: local_path - delegate_to: localhost - run_once: true - become: false - -- name: Validate local download path - ansible.builtin.assert: - that: - - local_path.stat.exists - - local_path.stat.readable - - keycloak_quarkus_offline_install or local_path.stat.writeable - quiet: true - fail_msg: "Defined controller path for downloading resources is incorrect or unreadable: {{ keycloak_quarkus_download_path }}" - success_msg: "Will download resource to controller path: {{ keycloak_quarkus_download_path }}" - delegate_to: localhost - run_once: true - -- name: Check downloaded archive if offline - ansible.builtin.stat: - path: "{{ local_path.stat.path }}/{{ keycloak.bundle }}" - when: keycloak_quarkus_offline_install - register: local_archive_path_check - delegate_to: localhost - run_once: true - -- name: Validate local downloaded archive if offline - ansible.builtin.assert: - that: - - local_archive_path_check.stat.exists - - local_archive_path_check.stat.readable - quiet: true - fail_msg: "Configured for offline install but install archive not found at: {{ local_path.stat.path }}/{{ keycloak.bundle }}" - success_msg: "Will install offline with expected archive: {{ local_path.stat.path }}/{{ keycloak.bundle }}" - when: keycloak_quarkus_offline_install - delegate_to: localhost - run_once: true - -- name: "Validate keytool" - when: keycloak_quarkus_config_key_store_password | length > 0 - block: - - name: "Check run keytool" - changed_when: false - ansible.builtin.command: keytool -help - register: keytool_check - ignore_errors: true - - - name: "Fail when no keytool found" - when: keytool_check.rc != 0 - ansible.builtin.fail: - msg: "keytool NOT found in the PATH, but is required for setting up the configuration key store" - -- name: "Validate providers" - ansible.builtin.assert: - that: > - item.id is defined and item.id | length > 0 and - ( (item.spi is defined and item.spi | length > 0) or - (item.url is defined and item.url | length > 0) or - ( item.maven is defined and item.maven.repository_url is defined and item.maven.repository_url | length > 0 and - item.maven.group_id is defined and item.maven.group_id | length > 0 and - item.maven.artifact_id is defined and item.maven.artifact_id | length > 0) or - (item.local_path is defined and item.local_path | length > 0) - ) - quiet: true - fail_msg: > - Providers definition incorrect; `id` and one of `spi`, `url`, `local_path`, or `maven` are mandatory. `key` and `value` are mandatory for each property - loop: "{{ keycloak_quarkus_providers }}" - -- name: "Validate policies" - ansible.builtin.assert: - that: - - item.name is defined and item.name | length > 0 - - item.url is defined and item.url | length > 0 - - item.type is not defined or item.type | lower in keycloak_quarkus_supported_policy_types - quiet: true - fail_msg: > - Policy definition is incorrect: `name` and one of `url` are mandatory, `type` needs to be left empty or one of {{ keycloak_quarkus_supported_policy_types }}. - loop: "{{ keycloak_quarkus_policies }}" - -- name: "Validate additional env variables" - ansible.builtin.assert: - that: - - item.key is defined and item.key | length > 0 - - item.value is defined and item.value | length > 0 - quiet: true - fail_msg: "Additional env variable definition is incorrect: `key` and `value` are mandatory." - no_log: true - loop: "{{ keycloak_quarkus_additional_env_vars }}" - -- name: "Validate proxy-headers" - ansible.builtin.assert: - that: - - keycloak_quarkus_proxy_headers | lower in ['', 'forwarded', 'xforwarded'] - quiet: true - fail_msg: "keycloak_quarkus_proxy_headers must be either '', 'forwarded' or 'xforwarded'" + packages_list: + - "{{ keycloak_quarkus_jvm_package }}" + - unzip + - procps-ng + - initscripts \ No newline at end of file diff --git a/roles/keycloak_quarkus/tasks/rebuild_config.yml b/roles/keycloak_quarkus/tasks/rebuild_config.yml deleted file mode 100644 index 1d43127..0000000 --- a/roles/keycloak_quarkus/tasks/rebuild_config.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -# cf. https://www.keycloak.org/server/configuration#_optimize_the_keycloak_startup -- name: "Rebuild {{ keycloak.service_name }} config" - ansible.builtin.shell: | # noqa blocked_modules shell is necessary here - env -i bash -c "set -a ; source {{ keycloak_quarkus_sysconf_file }} ; {{ keycloak.home }}/bin/kc.sh build " - become: true - changed_when: true diff --git a/roles/keycloak_quarkus/tasks/redhat.yml b/roles/keycloak_quarkus/tasks/redhat.yml deleted file mode 100644 index 26d552b..0000000 --- a/roles/keycloak_quarkus/tasks/redhat.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- name: Include firewall config tasks - ansible.builtin.include_tasks: - file: firewalld.yml - apply: - tags: - - firewall - when: keycloak_quarkus_configure_firewalld - tags: - - firewall diff --git a/roles/keycloak_quarkus/tasks/restart.yml b/roles/keycloak_quarkus/tasks/restart.yml index 3aa97f6..eff9ddf 100644 --- a/roles/keycloak_quarkus/tasks/restart.yml +++ b/roles/keycloak_quarkus/tasks/restart.yml @@ -1,23 +1,7 @@ --- - name: "Restart and enable {{ keycloak.service_name }} service" ansible.builtin.systemd: - name: "{{ keycloak.service_name }}" - enabled: true + name: keycloak + enabled: yes state: restarted - daemon_reload: true - become: true - -- name: "Wait until {{ keycloak.service_name }} service becomes active {{ keycloak.health_url }}" - ansible.builtin.uri: - url: "{{ keycloak.health_url }}" - register: keycloak_status - until: keycloak_status.status == 200 - retries: "{{ keycloak_quarkus_restart_health_check_retries }}" - delay: "{{ keycloak_quarkus_restart_health_check_delay }}" - when: internal_force_health_check | default(keycloak_quarkus_restart_health_check) - -- name: Wait to give distributed ispn caches time to (re-)replicate back onto first host - ansible.builtin.pause: - seconds: "{{ keycloak_quarkus_restart_pause }}" - when: - - keycloak_quarkus_ha_enabled + become: yes diff --git a/roles/keycloak_quarkus/tasks/restart/none.yml b/roles/keycloak_quarkus/tasks/restart/none.yml deleted file mode 100644 index d048959..0000000 --- a/roles/keycloak_quarkus/tasks/restart/none.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -- name: "Display message" - ansible.builtin.debug: - msg: "keycloak_quarkus_restart_strategy is none, skipping restart" diff --git a/roles/keycloak_quarkus/tasks/restart/serial.yml b/roles/keycloak_quarkus/tasks/restart/serial.yml deleted file mode 100644 index 26397d3..0000000 --- a/roles/keycloak_quarkus/tasks/restart/serial.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -- name: "Restart services in serial, with optional healtch check (keycloak_quarkus_restart_health_check)" - throttle: 1 - block: - - name: "Restart and enable {{ keycloak.service_name }} service on {{ item }}" - ansible.builtin.include_tasks: - file: restart.yml - apply: - delegate_to: "{{ item }}" - run_once: true - loop: "{{ ansible_play_hosts }}" diff --git a/roles/keycloak_quarkus/tasks/restart/serial_then_parallel.yml b/roles/keycloak_quarkus/tasks/restart/serial_then_parallel.yml deleted file mode 100644 index d883ff1..0000000 --- a/roles/keycloak_quarkus/tasks/restart/serial_then_parallel.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -- name: Verify first restarted service with health URL, then rest restart in parallel - block: - - name: "Restart and enable {{ keycloak.service_name }} service on initial host" - ansible.builtin.include_tasks: - file: restart.yml - apply: - delegate_to: "{{ ansible_play_hosts | first }}" - run_once: true - vars: - internal_force_health_check: true - - - name: "Restart and enable {{ keycloak.service_name }} service on other hosts" - ansible.builtin.systemd: - name: "{{ keycloak.service_name }}" - enabled: true - state: restarted - daemon_reload: true - become: true - when: inventory_hostname != ansible_play_hosts | first diff --git a/roles/keycloak_quarkus/tasks/start.yml b/roles/keycloak_quarkus/tasks/start.yml index 5a3ad5f..bdf42f9 100644 --- a/roles/keycloak_quarkus/tasks/start.yml +++ b/roles/keycloak_quarkus/tasks/start.yml @@ -2,10 +2,9 @@ - name: "Start {{ keycloak.service_name }} service" ansible.builtin.systemd: name: keycloak - enabled: true + enabled: yes state: started - daemon_reload: true - become: true + become: yes - name: "Wait until {{ keycloak.service_name }} becomes active {{ keycloak.health_url }}" ansible.builtin.uri: @@ -13,5 +12,4 @@ register: keycloak_status until: keycloak_status.status == 200 retries: 25 - delay: 10 - when: internal_force_health_check | default(keycloak_quarkus_restart_health_check) + delay: 10 \ No newline at end of file diff --git a/roles/keycloak_quarkus/tasks/systemd.yml b/roles/keycloak_quarkus/tasks/systemd.yml index fda37f5..578802c 100644 --- a/roles/keycloak_quarkus/tasks/systemd.yml +++ b/roles/keycloak_quarkus/tasks/systemd.yml @@ -1,16 +1,13 @@ --- -- name: "Configure sysconfig file for {{ keycloak.service_name }} service" - become: true +- name: "Configure sysconfig file for keycloak service" + become: yes ansible.builtin.template: src: keycloak-sysconfig.j2 - dest: "{{ keycloak_quarkus_sysconf_file }}" + dest: /etc/sysconfig/keycloak owner: root group: root - mode: '0640' - vars: - keycloak_sys_pkg_java_home: "{{ keycloak_quarkus_pkg_java_home }}" + mode: 0644 notify: - - rebuild keycloak config - restart keycloak - name: "Configure systemd unit file for keycloak service" @@ -19,9 +16,14 @@ dest: /etc/systemd/system/keycloak.service owner: root group: root - mode: '0644' - become: true + mode: 0644 + become: yes register: systemdunit notify: - - rebuild keycloak config - restart keycloak + +- name: Reload systemd + become: yes + ansible.builtin.systemd: + daemon_reload: yes + when: systemdunit.changed diff --git a/roles/keycloak_quarkus/templates/cache-ispn.xml.j2 b/roles/keycloak_quarkus/templates/cache-ispn.xml.j2 deleted file mode 100644 index 2d745d5..0000000 --- a/roles/keycloak_quarkus/templates/cache-ispn.xml.j2 +++ /dev/null @@ -1,117 +0,0 @@ -{{ ansible_managed | comment('xml') }} - - - - -{% set stack_expression='' %} -{% if keycloak_quarkus_ha_enabled %} -{% if keycloak_quarkus_ha_discovery == 'TCPPING' %} -{% set stack_expression='stack="tcpping"' %} - - - - - - -{% elif keycloak_quarkus_ha_discovery == 'JDBCPING' %} -{% set stack_expression='stack="JDBC_PING2"' %} -{% endif %} -{% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/roles/keycloak_quarkus/templates/keycloak-sysconfig.j2 b/roles/keycloak_quarkus/templates/keycloak-sysconfig.j2 index 9efd068..13a589e 100644 --- a/roles/keycloak_quarkus/templates/keycloak-sysconfig.j2 +++ b/roles/keycloak_quarkus/templates/keycloak-sysconfig.j2 @@ -1,15 +1,3 @@ -{{ ansible_managed | comment }} -{% if not ansible_local.keycloak.general.bootstrapped | default(false) | bool %} -KC_BOOTSTRAP_ADMIN_USERNAME={{ keycloak_quarkus_bootstrap_admin_user }} -KC_BOOTSTRAP_ADMIN_PASSWORD='{{ keycloak_quarkus_bootstrap_admin_password }}' -{% else %} -{{ keycloak.bootstrap_mnemonic }} -{% endif %} -PATH="{{ keycloak_quarkus_java_home | default(keycloak_sys_pkg_java_home, true) }}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" -JAVA_HOME="{{ keycloak_quarkus_java_home | default(keycloak_sys_pkg_java_home, true) }}" -JAVA_OPTS="{{ keycloak_quarkus_java_opts }}" - -# Custom ENV variables -{% for env in keycloak_quarkus_additional_env_vars %} -{{ env.key }}={{ env.value }} -{% endfor %} +# {{ ansible_managed }} +KEYCLOAK_ADMIN={{ keycloak_quarkus_admin_user }} +KEYCLOAK_ADMIN_PASSWORD='{{ keycloak_quarkus_admin_pass }}' \ No newline at end of file diff --git a/roles/keycloak_quarkus/templates/keycloak.conf.j2 b/roles/keycloak_quarkus/templates/keycloak.conf.j2 index 7642715..59d82a2 100644 --- a/roles/keycloak_quarkus/templates/keycloak.conf.j2 +++ b/roles/keycloak_quarkus/templates/keycloak.conf.j2 @@ -1,107 +1,51 @@ -{{ ansible_managed | comment }} +# {{ ansible_managed }} -{% if keycloak_quarkus_db_enabled %} # Database -db={{ keycloak_quarkus_db_engine }} -db-url={{ keycloak_quarkus_db_url }} -db-username={{ keycloak_quarkus_db_user }} -{% if not keycloak.config_key_store_enabled %} -db-password={{ keycloak_quarkus_db_pass }} -{% endif %} -{% endif %} - -{% if keycloak.config_key_store_enabled %} -# Config store -config-keystore={{ keycloak_quarkus_config_key_store_file }} -config-keystore-password={{ keycloak_quarkus_config_key_store_password }} -{% endif %} +# Database vendor [dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres] +#db=postgres +# The username of the database user. +#db-username=keycloak +# The password of the database user. +#db-password=password +# The full database JDBC URL. If not provided, a default URL is set based on the selected database vendor. +#db-url=jdbc:postgresql://localhost/keycloak # Observability -metrics-enabled={{ keycloak_quarkus_metrics_enabled | lower }} -health-enabled={{ keycloak_quarkus_health_enabled | lower }} +# If the server should expose metrics and healthcheck endpoints. +#metrics-enabled=true # HTTP -http-enabled={{ keycloak_quarkus_http_enabled | lower }} -{% if keycloak_quarkus_http_enabled %} -http-port={{ keycloak_quarkus_http_port }} -{% endif %} -http-relative-path={{ keycloak_quarkus_http_relative_path }} -http-host={{ keycloak_quarkus_http_host }} +http-enabled=true +http-port=8080 +https-port=8443 +# The file path to a server certificate or certificate chain in PEM format. +#https-certificate-file=${kc.home.dir}conf/server.crt.pem +# The file path to a private key in PEM format. +#https-certificate-key-file=${kc.home.dir}conf/server.key.pem +# The proxy address forwarding mode if the server is behind a reverse proxy. +#proxy=reencrypt +# Do not attach route to cookies and rely on the session affinity capabilities from reverse proxy +#spi-sticky-session-encoder-infinispan-should-attach-route=false -# Management -http-management-port={{ keycloak_quarkus_http_management_port }} -{% if keycloak_quarkus_http_management_relative_path is defined and keycloak_quarkus_http_management_relative_path | length > 0 %} -http-management-relative-path={{ keycloak_quarkus_http_management_relative_path }} -{% endif %} - -# HTTPS -https-port={{ keycloak_quarkus_https_port }} -{% if keycloak_quarkus_https_key_file_enabled %} -https-certificate-file={{ keycloak_quarkus_cert_file}} -https-certificate-key-file={{ keycloak_quarkus_key_file }} -{% endif %} -{% if keycloak_quarkus_https_key_store_enabled %} -https-key-store-file={{ keycloak_quarkus_https_key_store_file }} -https-key-store-password={{ keycloak_quarkus_https_key_store_password }} -{% endif %} -{% if keycloak_quarkus_https_trust_store_enabled %} -https-trust-store-file={{ keycloak_quarkus_https_trust_store_file }} -https-trust-store-password={{ keycloak_quarkus_https_trust_store_password }} -{% endif %} - -# Client URL configuration -hostname={{ keycloak_quarkus_hostname }} -hostname-admin={{ keycloak_quarkus_hostname_admin }} -hostname-strict={{ keycloak_quarkus_hostname_strict | lower }} -hostname-backchannel-dynamic={{ keycloak_quarkus_hostname_backchannel_dynamic | lower }} +# Hostname for the Keycloak server. +hostname={{ keycloak_quarkus_host }} +hostname-path={{ keycloak_quarkus_http_relative_path }} # Cluster -{% if keycloak_quarkus_ha_enabled %} -cache=ispn -cache-config-file=cache-ispn.xml -{% if keycloak_quarkus_cache_remote %} -cache-remote-username={{ keycloak_quarkus_cache_remote_username }} -cache-remote-password={{ keycloak_quarkus_cache_remote_password }} -cache-remote-host={{ keycloak_quarkus_cache_remote_host }} -cache-remote-port={{ keycloak_quarkus_cache_remote_port }} -cache-remote-tls-enabled={{ keycloak_quarkus_cache_remote_tls_enabled | lower }} -{% endif %} -{% endif %} +#cache=ispn +#Defines the cache mechanism for high-availability. [local, ispn] +#cache-config-file=conf/cache-ispn.xml +#Defines the file from which cache configuration should be loaded from. +#cache-stack=tcp +#Define the default stack to use for cluster communication and node discovery. [tcp, udp, kubernetes, ec2, azure, google] -{% if keycloak_quarkus_proxy_headers | length > 0 %} -proxy-headers={{ keycloak_quarkus_proxy_headers | lower }} -{% elif keycloak_quarkus_proxy_mode is defined and keycloak_quarkus_proxy_mode != "none" %} -# Deprecated Proxy configuration -proxy={{ keycloak_quarkus_proxy_mode }} -{% endif %} - -spi-sticky-session-encoder-infinispan-should-attach-route={{ keycloak_quarkus_spi_sticky_session_encoder_infinispan_should_attach_route | d(true) | lower }} - -# Transaction -transaction-xa-enabled={{ keycloak_quarkus_transaction_xa_enabled | lower }} +# Proxy +# The proxy address forwarding mode if the server is behind a reverse proxy. [edge, reencrypt, passthrough] +#proxy= # Logging +# The format of log entries. #log-format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n -log={{ keycloak_quarkus_log }} -log-level={{ keycloak.log.level }} -log-file={{ keycloak.log.file }} -log-file-format={{ keycloak.log.format }} +# The log level of the root category or a comma-separated list of individual categories and their levels. +#log-level=info -# Vault -{% if keycloak_quarkus_ks_vault_enabled %} -vault=keystore -vault-file={{ keycloak_quarkus_ks_vault_file }} -vault-type={{ keycloak_quarkus_ks_vault_type }} -vault-pass={{ keycloak_quarkus_ks_vault_pass }} -{% endif %} - - -# Providers -{% for provider in keycloak_quarkus_providers %} -{% if provider.default is defined and provider.default %} -spi-{{ provider.spi }}-provider={{ provider.id }} -{% endif %} -{% if provider.properties is defined %}{% for property in provider.properties %} -spi-{{ provider.spi }}-{{ provider.id }}-{{ property.key }}={{ property.value }} -{% endfor %}{% endif %} -{% endfor %} diff --git a/roles/keycloak_quarkus/templates/keycloak.fact.j2 b/roles/keycloak_quarkus/templates/keycloak.fact.j2 deleted file mode 100644 index e035110..0000000 --- a/roles/keycloak_quarkus/templates/keycloak.fact.j2 +++ /dev/null @@ -1,2 +0,0 @@ -[general] -bootstrapped={{ bootstrapped | lower }} diff --git a/roles/keycloak_quarkus/templates/keycloak.service.j2 b/roles/keycloak_quarkus/templates/keycloak.service.j2 index 96207ed..a710ee2 100644 --- a/roles/keycloak_quarkus/templates/keycloak.service.j2 +++ b/roles/keycloak_quarkus/templates/keycloak.service.j2 @@ -1,33 +1,14 @@ -{{ ansible_managed | comment }} +# {{ ansible_managed }} [Unit] Description=Keycloak Server After=network.target [Service] -EnvironmentFile=-{{ keycloak_quarkus_sysconf_file }} -{% if keycloak_quarkus_start_dev %} -ExecStart={{ keycloak.home }}/bin/kc.sh start-dev -{% else %} -ExecStart={{ keycloak.home }}/bin/kc.sh start --optimized -{% endif %} -User={{ keycloak.service_user }} -Group={{ keycloak.service_group }} -SuccessExitStatus=0 143 -{% if keycloak_quarkus_service_restart_always %} -Restart=always -{% elif keycloak_quarkus_service_restart_on_failure %} -Restart=on-failure -{% endif %} -RestartSec={{ keycloak_quarkus_service_restartsec }} -{% if keycloak_quarkus_http_port | int < 1024 or keycloak_quarkus_https_port | int < 1024 %} -AmbientCapabilities=CAP_NET_BIND_SERVICE -{% endif %} -{% if keycloak_quarkus_systemd_wait_for_port %} -ExecStartPost=/usr/bin/timeout {{ keycloak_quarkus_systemd_wait_for_timeout }} sh -c 'while ! ss -H -t -l -n sport = :{{ keycloak_quarkus_systemd_wait_for_port_number }} | grep -q "^LISTEN.*:{{ keycloak_quarkus_systemd_wait_for_port_number }}"; do sleep 1; done && /bin/sleep {{ keycloak_quarkus_systemd_wait_for_delay }}' -{% endif %} -{% if keycloak_quarkus_systemd_wait_for_log %} -ExecStartPost=/usr/bin/timeout {{ keycloak_quarkus_systemd_wait_for_timeout }} sh -c 'cat {{ keycloak.log.file }} | sed "/Profile.*activated/ q" && /bin/sleep {{ keycloak_quarkus_systemd_wait_for_delay }}' -{% endif %} +Type=simple +EnvironmentFile=-/etc/sysconfig/keycloak +PIDFile={{ keycloak_quarkus_service_pidfile }} +ExecStart={{ keycloak.home }}/bin/kc.sh start +#--http-relative-path={{ keycloak_quarkus_http_relative_path }} [Install] WantedBy=multi-user.target diff --git a/roles/keycloak_quarkus/templates/quarkus.properties.j2 b/roles/keycloak_quarkus/templates/quarkus.properties.j2 deleted file mode 100644 index 06d9077..0000000 --- a/roles/keycloak_quarkus/templates/quarkus.properties.j2 +++ /dev/null @@ -1,29 +0,0 @@ -{{ ansible_managed | comment }} -{% if keycloak_quarkus_ha_enabled %} -{% if keycloak_quarkus_version.split('.')[0] | int < 22 %} -quarkus.infinispan-client.server-list={{ keycloak_quarkus_cache_remote_host }} -quarkus.infinispan-client.auth-username={{ keycloak_quarkus_cache_remote_username }} -quarkus.infinispan-client.auth-password={{ keycloak_quarkus_cache_remote_password }} -{% else %} -quarkus.infinispan-client.hosts={{ keycloak_quarkus_cache_remote_host }} -quarkus.infinispan-client.username={{ keycloak_quarkus_cache_remote_username }} -quarkus.infinispan-client.password={{ keycloak_quarkus_cache_remote_password }} -{% endif %} -quarkus.infinispan-client.client-intelligence=HASH_DISTRIBUTION_AWARE -quarkus.infinispan-client.use-auth=true -quarkus.infinispan-client.auth-realm=default -quarkus.infinispan-client.auth-server-name=infinispan -quarkus.infinispan-client.sasl-mechanism={{ keycloak_quarkus_cache_remote_sasl_mechanism }} -{% if keycloak_quarkus_cache_remote_tls_enabled %} -quarkus.infinispan-client.trust-store={{ keycloak_quarkus_https_trust_store_file }} -quarkus.infinispan-client.trust-store-password={{ keycloak_quarkus_https_trust_store_password }} -quarkus.infinispan-client.trust-store-type=jks -{% endif %} -#quarkus.infinispan-client.use-schema-registration=true -{% endif %} -quarkus.log.file.rotation.max-file-size={{ keycloak_quarkus_log_max_file_size }} -quarkus.log.file.rotation.max-backup-index={{ keycloak_quarkus_log_max_backup_index }} -quarkus.log.file.rotation.file-suffix={{ keycloak_quarkus_log_file_suffix }} -{% if keycloak_quarkus_db_enabled %} -quarkus.transaction-manager.enable-recovery=true -{% endif %} diff --git a/roles/keycloak_quarkus/vars/debian.yml b/roles/keycloak_quarkus/vars/debian.yml deleted file mode 100644 index 5391dda..0000000 --- a/roles/keycloak_quarkus/vars/debian.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -keycloak_quarkus_varjvm_package: "{{ keycloak_quarkus_jvm_package | default('openjdk-21-jdk-headless') }}" -keycloak_quarkus_prereq_package_list: - - "{{ keycloak_quarkus_varjvm_package }}" - - bash - - unzip - - procps - - apt - - tzdata -keycloak_quarkus_sysconf_file: /etc/default/keycloak -keycloak_quarkus_pkg_java_home: "/usr/lib/jvm/java-{{ keycloak_quarkus_varjvm_package | \ - regex_search('(?!:openjdk-)[0-9.]+') }}-openjdk-{{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}" diff --git a/roles/keycloak_quarkus/vars/main.yml b/roles/keycloak_quarkus/vars/main.yml index 997d7dc..bca7d88 100644 --- a/roles/keycloak_quarkus/vars/main.yml +++ b/roles/keycloak_quarkus/vars/main.yml @@ -1,17 +1,11 @@ --- -keycloak: # noqa var-naming this is an internal dict of interpolated values +keycloak: home: "{{ keycloak_quarkus_home }}" config_dir: "{{ keycloak_quarkus_config_dir }}" bundle: "{{ keycloak_quarkus_archive }}" service_name: "keycloak" - health_url: "{{ keycloak_quarkus_health_check_url | default(keycloak_quarkus_hostname ~ '/' ~ (keycloak_quarkus_health_check_url_path | default('realms/master/.well-known/openid-configuration'))) }}" + health_url: "http://localhost:8080/realms/master/.well-known/openid-configuration" cli_path: "{{ keycloak_quarkus_home }}/bin/kcadm.sh" service_user: "{{ keycloak_quarkus_service_user }}" service_group: "{{ keycloak_quarkus_service_group }}" - offline_install: "{{ keycloak_quarkus_offline_install }}" - config_key_store_enabled: "{{ keycloak_quarkus_config_key_store_password != '' }}" - log: - file: "{{ keycloak_quarkus_home }}/{{ keycloak_quarkus_log_file }}" - level: "{{ keycloak_quarkus_log_level }}" - format: "{{ keycloak_quarkus_log_format }}" - bootstrap_mnemonic: "# ansible-middleware/keycloak: bootstrapped" + offline_install: "{{ keycloak_quarkus_offline_install }}" \ No newline at end of file diff --git a/roles/keycloak_quarkus/vars/redhat.yml b/roles/keycloak_quarkus/vars/redhat.yml deleted file mode 100644 index 458c841..0000000 --- a/roles/keycloak_quarkus/vars/redhat.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -keycloak_quarkus_varjvm_package: "{{ keycloak_quarkus_jvm_package | default('java-21-openjdk-headless') }}" -keycloak_quarkus_prereq_package_list: - - "{{ keycloak_quarkus_varjvm_package }}" - - bash - - unzip - - procps-ng - - initscripts - - tzdata-java -keycloak_quarkus_sysconf_file: /etc/sysconfig/keycloak -keycloak_quarkus_pkg_java_home: "/etc/alternatives/jre_{{ keycloak_quarkus_varjvm_package | regex_search('(?<=java-)[0-9.]+') }}" diff --git a/roles/keycloak_realm/README.md b/roles/keycloak_realm/README.md index 179784e..cf098a7 100644 --- a/roles/keycloak_realm/README.md +++ b/roles/keycloak_realm/README.md @@ -1,25 +1,24 @@ keycloak_realm ============== - Create realms and clients in [keycloak](https://keycloak.org/) or [Red Hat Single Sign-On](https://access.redhat.com/products/red-hat-single-sign-on) services. - + Role Defaults ------------- | Variable | Description | Default | -|:---------|:------------|:--------| +|:---------|:------------|:---------| |`keycloak_admin_user`| Administration console user account | `admin` | |`keycloak_host`| hostname | `localhost` | -|`keycloak_context`| Context path for rest calls | `/auth` | |`keycloak_http_port`| HTTP port | `8080` | |`keycloak_https_port`| TLS HTTP port | `8443` | |`keycloak_auth_realm`| Name of the main authentication realm | `master` | +|`keycloak_rhsso_enable`| Define service is an upstream(Keycloak) or RHSSO | `master` | |`keycloak_management_http_port`| Management port | `9990` | |`keycloak_auth_client`| Authentication client for configuration REST calls | `admin-cli` | |`keycloak_client_public`| Configure a public realm client | `True` | -|`keycloak_client_web_origins`| Web origins for realm client | `/*` | +|`keycloak_client_web_origins`| Web origins for realm client | `+` | |`keycloak_url`| URL for configuration rest calls | `http://{{ keycloak_host }}:{{ keycloak_http_port }}` | |`keycloak_management_url`| URL for management console rest calls | `http://{{ keycloak_host }}:{{ keycloak_management_http_port }}` | @@ -72,8 +71,6 @@ Refer to [docs](https://docs.ansible.com/ansible/latest/collections/community/ge ```yaml - name: - id: - client_id: roles: realm: public_client: @@ -81,9 +78,6 @@ Refer to [docs](https://docs.ansible.com/ansible/latest/collections/community/ge users: ``` -`name` and either `id` or `client_id` are required. - - * `keycloak_client_users`, a list of: ```yaml @@ -137,4 +131,4 @@ Author Information ------------------ * [Guido Grazioli](https://github.com/guidograzioli) -* [Romain Pelisse](https://github.com/rpelisse) +* [Romain Pelisse](https://github.com/rpelisse) \ No newline at end of file diff --git a/roles/keycloak_realm/defaults/main.yml b/roles/keycloak_realm/defaults/main.yml index a294cbe..4975380 100644 --- a/roles/keycloak_realm/defaults/main.yml +++ b/roles/keycloak_realm/defaults/main.yml @@ -4,6 +4,7 @@ keycloak_host: localhost keycloak_http_port: 8080 keycloak_https_port: 8443 keycloak_management_http_port: 9990 +keycloak_rhsso_enable: False ### Keycloak administration console user keycloak_admin_user: admin @@ -26,24 +27,23 @@ keycloak_admin_password: '' # and users is a list of account, see below for the format definition # an empty name will skip the creation of the client # -# keycloak_clients: -# - name: '' -# roles: "{{ keycloak_client_default_roles }}" -# realm: "{{ keycloak_realm }}" -# public_client: "{{ keycloak_client_public }}" -# web_origins: "{{ keycloak_client_web_origins }}" -# redirect_uris: "{{ keycloak_client_redirect_uris }}" -# users: "{{ keycloak_client_users }}" +#keycloak_clients: +# - name: '' +# roles: "{{ keycloak_client_default_roles }}" +# realm: "{{ keycloak_realm }}" +# public_client: "{{ keycloak_client_public }}" +# web_origins: "{{ keycloak_client_web_origins }}" +# users: "{{ keycloak_client_users }}" keycloak_clients: [] # list of roles to create in the client keycloak_client_default_roles: [] # if True, create a public client; otherwise, a confidetial client -keycloak_client_public: true +keycloak_client_public: True # allowed web origins for the client -keycloak_client_web_origins: '/*' +keycloak_client_web_origins: '+' # list of user and role mappings to create in the client # Each user has the form: @@ -54,7 +54,3 @@ keycloak_client_users: [] ### List of Keycloak User Federation keycloak_user_federation: [] - -# other settings -keycloak_url: "http://{{ keycloak_host }}:{{ keycloak_http_port + (keycloak_jboss_port_offset | default(0)) }}" -keycloak_management_url: "http://{{ keycloak_host }}:{{ keycloak_management_http_port + (keycloak_jboss_port_offset | default(0)) }}" diff --git a/roles/keycloak_realm/meta/argument_specs.yml b/roles/keycloak_realm/meta/argument_specs.yml index 7c24a7c..45b5998 100644 --- a/roles/keycloak_realm/meta/argument_specs.yml +++ b/roles/keycloak_realm/meta/argument_specs.yml @@ -10,7 +10,7 @@ argument_specs: # line 5 of keycloak_realm/defaults/main.yml default: "/auth" description: "Context path for rest calls" - type: "str" + type: "str" keycloak_http_port: # line 4 of keycloak_realm/defaults/main.yml default: 8080 @@ -26,6 +26,11 @@ argument_specs: default: 9990 description: "Management port" type: "int" + keycloak_rhsso_enable: + # line 7 of keycloak_realm/defaults/main.yml + default: false + description: "Enable Red Hat Single Sign-on" + type: "bool" keycloak_admin_user: # line 10 of keycloak_realm/defaults/main.yml default: "admin" @@ -53,7 +58,7 @@ argument_specs: type: "bool" keycloak_client_web_origins: # line 42 of keycloak_realm/defaults/main.yml - default: "/*" + default: "+" description: "Web origins for realm client" type: "str" keycloak_client_users: @@ -83,53 +88,11 @@ argument_specs: type: "list" keycloak_url: # line 14 of keycloak_realm/vars/main.yml - default: "http://{{ keycloak_host }}:{{ keycloak_http_port + ( keycloak_jboss_port_offset | default(0) ) }}" + default: "http://{{ keycloak_host }}:{{ keycloak_http_port }}" description: "URL for configuration rest calls" type: "str" keycloak_management_url: # line 15 of keycloak_realm/vars/main.yml - default: "http://{{ keycloak_host }}:{{ keycloak_management_http_port + ( keycloak_jboss_port_offset | default(0) ) }}" + default: "http://{{ keycloak_host }}:{{ keycloak_management_http_port }}" description: "URL for management console rest calls" type: "str" - downstream: - options: - sso_version: - default: "7.6.0" - description: "Red Hat Single Sign-On version" - 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: "bool" - rhbk_version: - default: "22.0.6" - description: "Red Hat Build of Keycloak version" - type: "str" - rhbk_archive: - default: "rhbk-{{ rhbk_version }}.zip" - description: "Red Hat Build of Keycloak install archive filename" - type: "str" - rhbk_dest: - default: "/opt/rhbk" - description: "Root installation directory" - type: "str" - rhbk_installdir: - default: "{{ rhbk_dest }}/rhbk-{{ rhbk_version.split('.')[0] }}.{{ rhbk_version.split('.')[1] }}" - description: "Installation path for Red Hat Build of Keycloak" - type: "str" - rhbk_enable: - default: true - description: "Enable Red Hat Build of Keycloak installation" - type: "bool" diff --git a/roles/keycloak_realm/meta/main.yml b/roles/keycloak_realm/meta/main.yml index 97c69d2..4ce1b73 100644 --- a/roles/keycloak_realm/meta/main.yml +++ b/roles/keycloak_realm/meta/main.yml @@ -8,15 +8,16 @@ galaxy_info: license: Apache License 2.0 - min_ansible_version: "2.16" + min_ansible_version: "2.9" platforms: - - name: EL - versions: - - "8" + - name: EL + versions: + - 8 galaxy_tags: - keycloak - redhat - rhel + - rhn - sso diff --git a/roles/keycloak_realm/tasks/main.yml b/roles/keycloak_realm/tasks/main.yml index 7595ba3..2554958 100644 --- a/roles/keycloak_realm/tasks/main.yml +++ b/roles/keycloak_realm/tasks/main.yml @@ -4,8 +4,8 @@ url: "{{ keycloak_url }}{{ keycloak_context }}/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: false - no_log: "{{ keycloak_no_log | default('True') }}" + validate_certs: no + no_log: True register: keycloak_auth_response until: keycloak_auth_response.status == 200 retries: 5 @@ -15,7 +15,6 @@ ansible.builtin.uri: url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ keycloak_realm }}" method: GET - validate_certs: false status_code: - 200 - 404 @@ -28,8 +27,8 @@ ansible.builtin.uri: url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms" method: POST - body: "{{ lookup('template', 'realm.json.j2') }}" - validate_certs: false + body: "{{ lookup('template','realm.json.j2') }}" + validate_certs: no body_format: json headers: Authorization: "Bearer {{ keycloak_auth_response.json.access_token }}" @@ -37,42 +36,31 @@ when: keycloak_realm_exists.status == 404 - name: Create user federation - middleware_automation.keycloak.keycloak_user_federation: + community.general.keycloak_user_federation: auth_keycloak_url: "{{ keycloak_url }}{{ keycloak_context }}" auth_realm: "{{ keycloak_auth_realm }}" auth_username: "{{ keycloak_admin_user }}" auth_password: "{{ keycloak_admin_password }}" - realm: "{{ item.realm | default(keycloak_realm) }}" + realm: "{{ item.realm }}" name: "{{ item.name }}" state: present provider_id: "{{ item.provider_id }}" - provider_type: "{{ item.provider_type | default(org.keycloak.storage.UserStorageProvider) }}" + provider_type: "{{ item.provider_type | default(org.keycloak.storage.UserStorageProvider) }}" config: "{{ item.config }}" mappers: "{{ item.mappers | default(omit) }}" - no_log: "{{ keycloak_no_log | default('True') }}" + no_log: True register: create_user_federation_result loop: "{{ keycloak_user_federation | flatten }}" when: keycloak_user_federation is defined -- name: Validate Keycloak clients - ansible.builtin.assert: - that: - - item.name is defined and item.name | length > 0 - - (item.client_id is defined and item.client_id | length > 0) or (item.id is defined and item.id | length > 0) - fail_msg: "For each keycloak client, attributes `name` and either `id` or `client_id` is required" - quiet: true - loop: "{{ keycloak_clients | flatten }}" - loop_control: - label: "{{ item.name | default('unnamed client') }}" - - name: Create or update a Keycloak client - middleware_automation.keycloak.keycloak_client: + community.general.keycloak_client: auth_client_id: "{{ keycloak_auth_client }}" auth_keycloak_url: "{{ keycloak_url }}{{ keycloak_context }}" auth_realm: "{{ keycloak_auth_realm }}" auth_username: "{{ keycloak_admin_user }}" auth_password: "{{ keycloak_admin_password }}" - realm: "{{ item.realm | default(keycloak_realm) }}" + realm: "{{ item.realm }}" default_roles: "{{ item.roles | default(omit) }}" client_id: "{{ item.client_id | default(omit) }}" id: "{{ item.id | default(omit) }}" @@ -83,7 +71,7 @@ base_url: "{{ item.base_url | default('') }}" enabled: "{{ item.enabled | default(True) }}" redirect_uris: "{{ item.redirect_uris | default(omit) }}" - web_origins: "{{ item.web_origins | default(omit) }}" + web_origins: "{{ item.web_origins | default('+') }}" bearer_only: "{{ item.bearer_only | default(omit) }}" standard_flow_enabled: "{{ item.standard_flow_enabled | default(omit) }}" implicit_flow_enabled: "{{ item.implicit_flow_enabled | default(omit) }}" @@ -91,9 +79,8 @@ service_accounts_enabled: "{{ item.service_accounts_enabled | default(omit) }}" public_client: "{{ item.public_client | default(False) }}" protocol: "{{ item.protocol | default(omit) }}" - attributes: "{{ item.attributes | default(omit) }}" state: present - no_log: "{{ keycloak_no_log | default('false') }}" + no_log: True register: create_client_result loop: "{{ keycloak_clients | flatten }}" when: (item.name is defined and item.client_id is defined) or (item.name is defined and item.id is defined) @@ -110,7 +97,4 @@ loop: "{{ keycloak_clients | flatten }}" loop_control: loop_var: client - when: "'users' in client" - -- name: Provide Access token lifespan - ansible.builtin.include_tasks: manage_token_lifespan.yml + when: "'users' in client" \ No newline at end of file diff --git a/roles/keycloak_realm/tasks/manage_client_roles.yml b/roles/keycloak_realm/tasks/manage_client_roles.yml index fbb25ac..04cf2fa 100644 --- a/roles/keycloak_realm/tasks/manage_client_roles.yml +++ b/roles/keycloak_realm/tasks/manage_client_roles.yml @@ -1,7 +1,7 @@ - name: Create client roles - middleware_automation.keycloak.keycloak_role: + community.general.keycloak_role: name: "{{ item }}" - realm: "{{ client.realm | default(keycloak_realm) }}" + realm: "{{ client.realm }}" client_id: "{{ client.name }}" auth_client_id: "{{ keycloak_auth_client }}" auth_keycloak_url: "{{ keycloak_url }}{{ keycloak_context }}" @@ -10,4 +10,4 @@ auth_password: "{{ keycloak_admin_password }}" state: present loop: "{{ client.roles | flatten }}" - no_log: "{{ keycloak_no_log | default('True') }}" + no_log: True diff --git a/roles/keycloak_realm/tasks/manage_client_users.yml b/roles/keycloak_realm/tasks/manage_client_users.yml index 5234cb1..ed9fb03 100644 --- a/roles/keycloak_realm/tasks/manage_client_users.yml +++ b/roles/keycloak_realm/tasks/manage_client_users.yml @@ -10,4 +10,4 @@ loop: "{{ client.users | flatten }}" loop_control: loop_var: user - when: "'client_roles' in user" + when: "'client_roles' in user" \ No newline at end of file diff --git a/roles/keycloak_realm/tasks/manage_token_lifespan.yml b/roles/keycloak_realm/tasks/manage_token_lifespan.yml deleted file mode 100644 index f16b938..0000000 --- a/roles/keycloak_realm/tasks/manage_token_lifespan.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -- name: "Update Access token lifespan" - ansible.builtin.uri: - url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ keycloak_realm }}" - method: PUT - body: - accessTokenLifespan: 300 - validate_certs: false - body_format: json - status_code: - - 200 - - 204 - headers: - Authorization: "Bearer {{ keycloak_auth_response.json.access_token }}" diff --git a/roles/keycloak_realm/tasks/manage_user.yml b/roles/keycloak_realm/tasks/manage_user.yml index 1f9f7bd..840c738 100644 --- a/roles/keycloak_realm/tasks/manage_user.yml +++ b/roles/keycloak_realm/tasks/manage_user.yml @@ -2,7 +2,7 @@ - name: "Check if User Already Exists" ansible.builtin.uri: url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ keycloak_realm }}/users?username={{ user.username }}" - validate_certs: false + validate_certs: no headers: Authorization: "Bearer {{ keycloak_auth_response.json.access_token }}" register: keycloak_user_search_result @@ -18,7 +18,7 @@ email: "{{ user.email | default(omit) }}" firstName: "{{ user.firstName | default(omit) }}" lastName: "{{ user.lastName | default(omit) }}" - validate_certs: false + validate_certs: no body_format: json headers: Authorization: "Bearer {{ keycloak_auth_response.json.access_token }}" @@ -28,7 +28,7 @@ - name: "Get User" ansible.builtin.uri: url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ keycloak_realm }}/users?username={{ user.username }}" - validate_certs: false + validate_certs: no headers: Authorization: "Bearer {{ keycloak_auth_response.json.access_token }}" register: keycloak_user @@ -41,7 +41,7 @@ type: password temporary: false value: "{{ user.password }}" - validate_certs: false + validate_certs: no body_format: json status_code: - 200 diff --git a/roles/keycloak_realm/tasks/manage_user_client_roles.yml b/roles/keycloak_realm/tasks/manage_user_client_roles.yml index f9e0329..5369094 100644 --- a/roles/keycloak_realm/tasks/manage_user_client_roles.yml +++ b/roles/keycloak_realm/tasks/manage_user_client_roles.yml @@ -1,9 +1,8 @@ --- - name: "Get Realm for role" ansible.builtin.uri: - url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ client_role.realm | default(keycloak_realm) }}" + url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ client_role.realm }}" method: GET - validate_certs: false status_code: - 200 headers: @@ -13,11 +12,8 @@ - name: Check if Mapping is available ansible.builtin.uri: - url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ client_role.realm | \ - default(keycloak_realm) }}/users/{{ (keycloak_user.json | first).id }}/role-mappings/clients/{{ (create_client_result.results | \ - selectattr('end_state.clientId', 'equalto', client_role.client) | list | first).end_state.id }}/available" + url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ client_role.realm }}/users/{{ (keycloak_user.json | first).id }}/role-mappings/clients/{{ (create_client_result.results | selectattr('end_state.clientId', 'equalto', client_role.client) | list | first).end_state.id }}/available" method: GET - validate_certs: false status_code: - 200 headers: @@ -27,9 +23,7 @@ - name: "Create Role Mapping" ansible.builtin.uri: - url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ client_role.realm | \ - default(keycloak_realm) }}/users/{{ (keycloak_user.json | first).id }}/role-mappings/clients/{{ (create_client_result.results | \ - selectattr('end_state.clientId', 'equalto', client_role.client) | list | first).end_state.id }}" + url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ client_role.realm }}/users/{{ (keycloak_user.json | first).id }}/role-mappings/clients/{{ (create_client_result.results | selectattr('end_state.clientId', 'equalto', client_role.client) | list | first).end_state.id }}" method: POST body: - id: "{{ item.id }}" @@ -37,7 +31,7 @@ containerId: "{{ item.containerId }}" name: "{{ item.name }}" composite: "{{ item.composite }}" - validate_certs: false + validate_certs: False body_format: json headers: Authorization: "Bearer {{ keycloak_auth_response.json.access_token }}" diff --git a/roles/keycloak_realm/tasks/manage_user_roles.yml b/roles/keycloak_realm/tasks/manage_user_roles.yml index dc74477..e9d18b7 100644 --- a/roles/keycloak_realm/tasks/manage_user_roles.yml +++ b/roles/keycloak_realm/tasks/manage_user_roles.yml @@ -3,7 +3,7 @@ ansible.builtin.uri: url: "{{ keycloak_url }}{{ keycloak_context }}/admin/realms/{{ keycloak_realm }}/users?username={{ user.username }}" headers: - validate_certs: false + validate_certs: no Authorization: "Bearer {{ keycloak_auth_response.json.access_token }}" register: keycloak_user @@ -12,9 +12,9 @@ url: "{{ keycloak_url }}{{ keycloak_context }}/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: false + validate_certs: no register: keycloak_auth_response - no_log: "{{ keycloak_no_log | default('True') }}" + no_log: True until: keycloak_auth_response.status == 200 retries: 5 delay: 2 diff --git a/roles/keycloak_realm/vars/main.yml b/roles/keycloak_realm/vars/main.yml index ad9bd8e..076a8a9 100644 --- a/roles/keycloak_realm/vars/main.yml +++ b/roles/keycloak_realm/vars/main.yml @@ -3,3 +3,7 @@ # name of the realm to create, this is a required variable keycloak_realm: + +# other settings +keycloak_url: "http://{{ keycloak_host }}:{{ keycloak_http_port }}" +keycloak_management_url: "http://{{ keycloak_host }}:{{ keycloak_management_http_port }}"