Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keycloak modules retry request on authentication error, support refresh token parameter #9494

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
31e3791
feat: begin refactor to support refresh token in keycloak modules
armkeh Sep 13, 2024
3fe444a
chore: add start of tests for shared token usage
armkeh Sep 14, 2024
c7341db
feat: progress towards supporting refresh token; token introspection …
armkeh Sep 28, 2024
670db1a
chore: reset to main branch previous state; a different approach is n…
armkeh Oct 16, 2024
016f5fe
Merge remote-tracking branch 'upstream/main'
armkeh Oct 16, 2024
b7fd1e8
feat: add request methods to keycloak class, which will be expanded w…
armkeh Nov 1, 2024
70813f3
chore: Merge remote-tracking branch 'upstream/main' [8857]
armkeh Nov 1, 2024
b73c82b
feat: all requests to keycloak use request methods instead of open_ur…
armkeh Nov 6, 2024
bfe339a
chore: Merge remote-tracking branch 'upstream/main' [8857]
armkeh Nov 9, 2024
d6a2730
fix: data argument is optional in keycloak request methods [8857]
armkeh Nov 9, 2024
8c7684a
feat: add integration test for keycloak module authentication methods…
armkeh Nov 9, 2024
08650d0
chore: refactor get token logic to separate logic using username/pass…
armkeh Nov 9, 2024
ccea7aa
chore: refactor token request logic further to isolate request logic …
armkeh Nov 9, 2024
bd31be4
chore: fix minor lint issues [8857]
armkeh Dec 30, 2024
817c085
test: add (currently failing) test for request with invalid auth toke…
armkeh Dec 30, 2024
6770085
chore: allow realm to be provided to role module with refresh_token, …
armkeh Dec 30, 2024
3b90af5
feat: add retry logic to requests in keycloak module utils [8857]
armkeh Dec 30, 2024
76a95ed
chore: rename keycloak module fail_open_url method to fail_request [8…
armkeh Dec 30, 2024
53eec38
chore: merge remote-tracking branch 'upstream/main' [8857]
armkeh Dec 30, 2024
5b23f10
chore: update all keycloak modules to support refresh token param [8857]
armkeh Dec 30, 2024
a782e11
chore: merge remote-tracking branch 'upstream/main' [8857]
armkeh Dec 30, 2024
a58ea2b
chore: add refresh_token param to keycloak doc_fragments [8857]
armkeh Dec 31, 2024
85296f9
chore: restore dependency between auth_realm and auth_username,auth_p…
armkeh Dec 31, 2024
42dfe5d
chore: rearrange module param checks to reduce future pr size [8857]
armkeh Dec 31, 2024
c7a9207
chore: remove extra comma [8857]
armkeh Dec 31, 2024
28fc381
chore: update version added for refresh token param [8857]
armkeh Jan 1, 2025
1145ae3
chore: merge remote-tracking branch 'upstream/main' [8857]
armkeh Jan 1, 2025
24dbd85
chore: add changelog fragment [8857]
armkeh Jan 3, 2025
f183ab5
chore: merge remote-tracking branch 'upstream/main' [8857]
armkeh Jan 3, 2025
3ebc757
chore: re-add fail_open_url to keycloak module utils for backward com…
armkeh Jan 3, 2025
ef79f26
fix: do not make a new request to keycloak without reauth when refres…
armkeh Jan 23, 2025
ab26910
fix: only make final auth attempt if username/pass provided, and retu…
armkeh Jan 23, 2025
e62e04b
fix: make re-auth and retry code more consistent, ensure final except…
armkeh Jan 23, 2025
f37d7cf
test: fix arguments for invalid token, valid refresh token test (#8857)
armkeh Jan 23, 2025
57270fb
feat: catch invalid refresh token errors during re-auth attempt (#8857)
armkeh Jan 23, 2025
a10f37b
test: improve test coverage, including some unhappy path tests for au…
armkeh Jan 23, 2025
b8b79b1
chore: store auth errors from token request in backwards compatible w…
armkeh Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
major_changes:
- keycloak_* modules - ``refresh_token`` parameter added. When multiple authentication parameters are provided (``token``, ``refresh_token``, and ``auth_username``/``auth_password``), modules will now automatically retry requests upon authentication errors (401), using in order the token, refresh token, and username/password (https://github.com/ansible-collections/community.general/pull/9494).
6 changes: 6 additions & 0 deletions plugins/doc_fragments/keycloak.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ class ModuleDocFragment(object):
type: str
version_added: 3.0.0
refresh_token:
description:
- Authentication refresh token for Keycloak API.
type: str
version_added: 10.3.0
validate_certs:
description:
- Verify TLS certificates (do not disable this in production).
Expand Down
1,130 changes: 497 additions & 633 deletions plugins/module_utils/identity/keycloak/keycloak.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion plugins/modules/keycloak_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,8 @@ def main():
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']])
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', flow={})
Expand Down
3 changes: 2 additions & 1 deletion plugins/modules/keycloak_authentication_required_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@ def main():
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']])
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', end_state={}, diff=dict(before={}, after={}))
Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_authz_authorization_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ def main():
supports_check_mode=True,
required_one_of=(
[['token', 'auth_realm', 'auth_username', 'auth_password']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', end_state={}, diff=dict(before={}, after={}))

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_authz_custom_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ def main():
supports_check_mode=True,
required_one_of=(
[['token', 'auth_realm', 'auth_username', 'auth_password']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_authz_permission.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,9 @@ def main():
supports_check_mode=True,
required_one_of=(
[['token', 'auth_realm', 'auth_username', 'auth_password']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

# Convenience variables
state = module.params.get('state')
Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_authz_permission_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ def main():
supports_check_mode=True,
required_one_of=(
[['token', 'auth_realm', 'auth_username', 'auth_password']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

# Convenience variables
name = module.params.get('name')
Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,9 @@ def main():
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']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_client_rolemapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@ def main():
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']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_clientscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,9 @@ def main():
supports_check_mode=True,
required_one_of=([['id', 'name'],
['token', 'auth_realm', 'auth_username', 'auth_password']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_clientscope_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@ def keycloak_clientscope_type_module():
['default_clientscopes', 'optional_clientscopes']
]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
mutually_exclusive=[
['token', 'auth_realm'],
['token', 'auth_username'],
['token', 'auth_password']
])
],
)

return module

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_clienttemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,9 @@ def main():
supports_check_mode=True,
required_one_of=([['id', 'name'],
['token', 'auth_realm', 'auth_username', 'auth_password']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ def main():
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']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', end_state={}, diff=dict(before={}, after={}))

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,9 @@ def main():
supports_check_mode=True,
required_one_of=([['id', 'name'],
['token', 'auth_realm', 'auth_username', 'auth_password']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, group='')

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_identity_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,9 @@ def main():
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']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_realm.py
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,9 @@ def main():
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']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_realm_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ def main():
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']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

# Initialize the result object. Only "changed" seems to have special
# meaning for Ansible.
Expand Down
3 changes: 2 additions & 1 deletion plugins/modules/keycloak_realm_keys_metadata_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ def main():
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"]]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg="", keys_metadata="")
Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_realm_rolemapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,9 @@ def main():
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']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_role.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@ def main():
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']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,9 @@ def main():
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']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_user_federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,9 @@ def main():
supports_check_mode=True,
required_one_of=([['id', 'name'],
['token', 'auth_realm', 'auth_username', 'auth_password']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_user_rolemapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,9 @@ def main():
supports_check_mode=True,
required_one_of=([['token', 'auth_realm', 'auth_username', 'auth_password'],
['uid', 'target_username', 'service_account_user_client_id']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})

Expand Down
4 changes: 3 additions & 1 deletion plugins/modules/keycloak_userprofile.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,9 @@ def main():
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']]))
required_together=([['auth_realm', 'auth_username', 'auth_password']]),
required_by={'refresh_token': 'auth_realm'},
)

# Initialize the result object. Only "changed" seems to have special
# meaning for Ansible.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!--
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 module authentication integration test

To run the Keycloak module authentication integration test, start a keycloak server using Docker or Podman:

```sh
podman|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
```

Source Ansible env-setup from ansible github repository.

Run the integration tests:

```sh
ansible-test integration keycloak_role --python 3.10 --allow-unsupported
```

To cleanup, run:

```sh
podman|docker stop mykeycloak
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 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

unsupported
Loading
Loading