mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-08-04 21:24:24 -07:00
Keycloak idp well known url support (#10527)
* first commit * add and fixe test * add example * fragment and sanity * sanity * sanity * Update plugins/modules/keycloak_identity_provider.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update changelogs/fragments/10527-keycloak-idp-well-known-url-support.yml --------- Co-authored-by: Andre Desrosiers <andre.desrosiers@ssss.gouv.qc.ca> Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
5bdd82fbf5
commit
7ffeaaa16d
6 changed files with 182 additions and 1 deletions
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- keycloak_identity_provider – add support for ``fromUrl`` to automatically fetch OIDC endpoints from the well-known discovery URL, simplifying identity provider configuration (https://github.com/ansible-collections/community.general/pull/10527).
|
|
@ -104,6 +104,7 @@ URL_IDENTITY_PROVIDERS = "{url}/admin/realms/{realm}/identity-provider/instances
|
||||||
URL_IDENTITY_PROVIDER = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}"
|
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_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_IDENTITY_PROVIDER_MAPPER = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}/mappers/{id}"
|
||||||
|
URL_IDENTITY_PROVIDER_IMPORT = "{url}/admin/realms/{realm}/identity-provider/import-config"
|
||||||
|
|
||||||
URL_COMPONENTS = "{url}/admin/realms/{realm}/components"
|
URL_COMPONENTS = "{url}/admin/realms/{realm}/components"
|
||||||
URL_COMPONENT = "{url}/admin/realms/{realm}/components/{id}"
|
URL_COMPONENT = "{url}/admin/realms/{realm}/components/{id}"
|
||||||
|
@ -2580,6 +2581,23 @@ class KeycloakAPI(object):
|
||||||
self.fail_request(e, msg='Could not obtain list of identity provider mappers for idp %s in realm %s: %s'
|
self.fail_request(e, msg='Could not obtain list of identity provider mappers for idp %s in realm %s: %s'
|
||||||
% (alias, realm, str(e)))
|
% (alias, realm, str(e)))
|
||||||
|
|
||||||
|
def fetch_idp_endpoints_import_config_url(self, fromUrl, providerId='oidc', realm='master'):
|
||||||
|
""" Import an identity provider configuration through Keycloak server from a well-known URL.
|
||||||
|
:param fromUrl: URL to import the identity provider configuration from.
|
||||||
|
"param providerId: Provider ID of the identity provider to import, default 'oidc'.
|
||||||
|
:param realm: Realm
|
||||||
|
:return: IDP endpoins.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"providerId": providerId,
|
||||||
|
"fromUrl": fromUrl
|
||||||
|
}
|
||||||
|
idps_url = URL_IDENTITY_PROVIDER_IMPORT.format(url=self.baseurl, realm=realm)
|
||||||
|
return self._request_and_deserialize(idps_url, method='POST', data=json.dumps(payload))
|
||||||
|
except Exception as e:
|
||||||
|
self.fail_request(e, msg='Could not import the IdP config in realm %s: %s' % (realm, str(e)))
|
||||||
|
|
||||||
def get_identity_provider_mapper(self, mid, alias, realm='master'):
|
def get_identity_provider_mapper(self, mid, alias, realm='master'):
|
||||||
""" Fetch identity provider representation from a realm using the idp's alias.
|
""" Fetch identity provider representation from a realm using the idp's alias.
|
||||||
If the identity provider does not exist, None is returned.
|
If the identity provider does not exist, None is returned.
|
||||||
|
|
|
@ -242,6 +242,15 @@ options:
|
||||||
- Way to identify and track external users from the assertion.
|
- Way to identify and track external users from the assertion.
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
|
fromUrl:
|
||||||
|
description:
|
||||||
|
- IDP well-known OpenID Connect configuration URL.
|
||||||
|
- Support only O(provider_id=oidc).
|
||||||
|
- O(config.fromUrl) is mutually exclusive with O(config.userInfoUrl), O(config.authorizationUrl),
|
||||||
|
O(config.tokenUrl), O(config.logoutUrl), O(config.issuer) and O(config.jwksUrl).
|
||||||
|
type: str
|
||||||
|
version_added: '11.2.0'
|
||||||
|
|
||||||
mappers:
|
mappers:
|
||||||
description:
|
description:
|
||||||
- A list of dicts defining mappers associated with this Identity Provider.
|
- A list of dicts defining mappers associated with this Identity Provider.
|
||||||
|
@ -318,6 +327,24 @@ EXAMPLES = r"""
|
||||||
user.attribute: last_name
|
user.attribute: last_name
|
||||||
syncMode: INHERIT
|
syncMode: INHERIT
|
||||||
|
|
||||||
|
- name: Create OIDC identity provider, with well-known configuration URL
|
||||||
|
community.general.keycloak_identity_provider:
|
||||||
|
state: present
|
||||||
|
auth_keycloak_url: https://auth.example.com/auth
|
||||||
|
auth_realm: master
|
||||||
|
auth_username: admin
|
||||||
|
auth_password: admin
|
||||||
|
realm: myrealm
|
||||||
|
alias: oidc-idp
|
||||||
|
display_name: OpenID Connect IdP
|
||||||
|
enabled: true
|
||||||
|
provider_id: oidc
|
||||||
|
config:
|
||||||
|
fromUrl: https://the-idp.example.com/auth/realms/idprealm/.well-known/openid-configuration
|
||||||
|
clientAuthMethod: client_secret_post
|
||||||
|
clientId: my-client
|
||||||
|
clientSecret: secret
|
||||||
|
|
||||||
- name: Create SAML identity provider, authentication with credentials
|
- name: Create SAML identity provider, authentication with credentials
|
||||||
community.general.keycloak_identity_provider:
|
community.general.keycloak_identity_provider:
|
||||||
state: present
|
state: present
|
||||||
|
@ -461,6 +488,29 @@ def get_identity_provider_with_mappers(kc, alias, realm):
|
||||||
return idp
|
return idp
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_identity_provider_wellknown_config(kc, config):
|
||||||
|
"""
|
||||||
|
Fetches OpenID Connect well-known configuration from a given URL and updates the config dict with discovered endpoints.
|
||||||
|
Support for oidc providers only.
|
||||||
|
:param kc: KeycloakAPI instance used to fetch endpoints and handle errors.
|
||||||
|
:param config: Dictionary containing identity provider configuration, must include 'fromUrl' key to trigger fetch.
|
||||||
|
:return: None. The config dict is updated in-place.
|
||||||
|
"""
|
||||||
|
if config and 'fromUrl' in config :
|
||||||
|
if 'providerId' in config and config['providerId'] != 'oidc':
|
||||||
|
kc.module.fail_json(msg="Only 'oidc' provider_id is supported when using 'fromUrl'.")
|
||||||
|
endpoints = ['userInfoUrl', 'authorizationUrl', 'tokenUrl', 'logoutUrl', 'issuer', 'jwksUrl']
|
||||||
|
if any(k in config for k in endpoints):
|
||||||
|
kc.module.fail_json(msg="Cannot specify both 'fromUrl' and 'userInfoUrl', 'authorizationUrl', 'tokenUrl', 'logoutUrl', 'issuer' or 'jwksUrl'.")
|
||||||
|
openIdConfig = kc.fetch_idp_endpoints_import_config_url(
|
||||||
|
fromUrl=config['fromUrl'],
|
||||||
|
realm=kc.module.params.get('realm', 'master'))
|
||||||
|
for k in endpoints:
|
||||||
|
if k in openIdConfig:
|
||||||
|
config[k] = openIdConfig[k]
|
||||||
|
del config['fromUrl']
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""
|
"""
|
||||||
Module execution
|
Module execution
|
||||||
|
@ -517,6 +567,9 @@ def main():
|
||||||
realm = module.params.get('realm')
|
realm = module.params.get('realm')
|
||||||
alias = module.params.get('alias')
|
alias = module.params.get('alias')
|
||||||
state = module.params.get('state')
|
state = module.params.get('state')
|
||||||
|
config = module.params.get('config')
|
||||||
|
|
||||||
|
fetch_identity_provider_wellknown_config(kc, config)
|
||||||
|
|
||||||
# Filter and map the parameters names that apply to the identity provider.
|
# Filter and map the parameters names that apply to the identity provider.
|
||||||
idp_params = [x for x in module.params
|
idp_params = [x for x in module.params
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!--
|
||||||
|
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
|
||||||
|
-->
|
||||||
|
# Running keycloak_identity_provider module integration test
|
||||||
|
|
||||||
|
To run Keycloak component info module's integration test, start a keycloak server using Docker:
|
||||||
|
|
||||||
|
docker run -d --rm --name mykeycloak -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=password quay.io/keycloak/keycloak:latest start-dev --http-relative-path /auth
|
||||||
|
|
||||||
|
Run integration tests:
|
||||||
|
|
||||||
|
ansible-test integration -v keycloak_identity_provider --allow-unsupported --docker fedora35 --docker-network host
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
|
||||||
|
docker stop mykeycloak
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,15 @@
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# 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
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
- name: Delete realm if exists
|
||||||
|
community.general.keycloak_realm:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
- name: Create realm
|
- name: Create realm
|
||||||
community.general.keycloak_realm:
|
community.general.keycloak_realm:
|
||||||
auth_keycloak_url: "{{ url }}"
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
@ -62,7 +71,7 @@
|
||||||
- result.existing == {}
|
- result.existing == {}
|
||||||
- result.end_state.alias == "{{ idp }}"
|
- result.end_state.alias == "{{ idp }}"
|
||||||
- result.end_state.mappers != []
|
- result.end_state.mappers != []
|
||||||
- result.end_state.config.client_secret = "**********"
|
- result.end_state.config.clientSecret == "**********"
|
||||||
|
|
||||||
- name: Update existing identity provider (no change)
|
- name: Update existing identity provider (no change)
|
||||||
community.general.keycloak_identity_provider:
|
community.general.keycloak_identity_provider:
|
||||||
|
@ -277,3 +286,79 @@
|
||||||
that:
|
that:
|
||||||
- result is not changed
|
- result is not changed
|
||||||
- result.end_state == {}
|
- result.end_state == {}
|
||||||
|
|
||||||
|
- name: Create IDP realm
|
||||||
|
community.general.keycloak_realm:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
id: "{{ idp_realm }}"
|
||||||
|
realm: "{{ idp_realm }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create new identity provider with fromUrl
|
||||||
|
community.general.keycloak_identity_provider:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
alias: "{{ idp_fromurl }}"
|
||||||
|
display_name: OpenID Connect IdP from url
|
||||||
|
enabled: true
|
||||||
|
provider_id: oidc
|
||||||
|
config:
|
||||||
|
fromUrl: "{{ url }}/realms/{{ idp_realm }}/.well-known/openid-configuration"
|
||||||
|
clientAuthMethod: client_secret_post
|
||||||
|
clientId: clientid
|
||||||
|
clientSecret: clientsecret
|
||||||
|
syncMode: FORCE
|
||||||
|
state: present
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Debug
|
||||||
|
debug:
|
||||||
|
var: result
|
||||||
|
|
||||||
|
- name: Assert identity provider created with IDP endpoints
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state.config.authorizationUrl == "{{ url }}/realms/{{ idp_realm }}/protocol/openid-connect/auth"
|
||||||
|
- result.end_state.config.issuer == "{{ url }}/realms/{{ idp_realm }}"
|
||||||
|
- result.end_state.config.jwksUrl == "{{ url }}/realms/{{ idp_realm }}/protocol/openid-connect/certs"
|
||||||
|
- result.end_state.config.logoutUrl == "{{ url }}/realms/{{ idp_realm }}/protocol/openid-connect/logout"
|
||||||
|
- result.end_state.config.tokenUrl == "{{ url }}/realms/{{ idp_realm }}/protocol/openid-connect/token"
|
||||||
|
- result.end_state.config.userInfoUrl == "{{ url }}/realms/{{ idp_realm }}/protocol/openid-connect/userinfo"
|
||||||
|
|
||||||
|
- name: Create new identity provider with fromUrl and exclusion should fail
|
||||||
|
community.general.keycloak_identity_provider:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
alias: "mustfail"
|
||||||
|
display_name: Failed OpenID Connect IdP from url
|
||||||
|
enabled: true
|
||||||
|
provider_id: oidc
|
||||||
|
config: "{{ config | combine(endpoint) }}"
|
||||||
|
state: present
|
||||||
|
vars:
|
||||||
|
config:
|
||||||
|
fromUrl: "{{ url }}/realms/{{ idp_realm }}/.well-known/openid-configuration"
|
||||||
|
clientAuthMethod: client_secret_post
|
||||||
|
clientId: clientid
|
||||||
|
clientSecret: clientsecret
|
||||||
|
endpoint: "{{ '{\"' + item + '\": \"' + url + '/realms/' + idp_realm + '/protocol/openid-connect/' + item + '\"}' }}"
|
||||||
|
with_items: ['userInfoUrl', 'authorizationUrl', 'tokenUrl', 'logoutUrl', 'issuer', 'jwksUrl']
|
||||||
|
register: result
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Check failure of identity provider creation with fromUrl and userInfoUrl
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result is failed
|
||||||
|
- result.results | selectattr('failed', 'equalto', false) | list | length == 0
|
||||||
|
|
|
@ -9,3 +9,6 @@ admin_user: admin
|
||||||
admin_password: password
|
admin_password: password
|
||||||
realm: myrealm
|
realm: myrealm
|
||||||
idp: myidp
|
idp: myidp
|
||||||
|
|
||||||
|
idp_realm: myidprealm
|
||||||
|
idp_fromurl: myidpfromurl
|
Loading…
Add table
Add a link
Reference in a new issue