Allow keycloak modules to take token as parameter for the auth. (#2250)

* Allow keycloak_group.py to take token as parameter for the authentification

* Fix some pep8 issues

* Add changelog fragment

* Refactor get_token to pass module.params + Documentation

* Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Fix unit test and add new one for token as param

* Fix identation

* Check base_url format also if token is given

* Update plugins/doc_fragments/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/identity/keycloak/keycloak_client.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/identity/keycloak/keycloak_clienttemplate.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Allow keycloak_group.py to take token as parameter for the authentification

* Refactor get_token to pass module.params + Documentation

* Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/identity/keycloak/keycloak_group.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Check if base_url is None before to check format

* Fix unit test: rename base_url parameter to auth_keycloak_url

* Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update changelogs/fragments/2250-allow-keycloak-modules-to-take-token-as-param.yml

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Update plugins/modules/identity/keycloak/keycloak_client.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Update plugins/modules/identity/keycloak/keycloak_client.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Update plugins/modules/identity/keycloak/keycloak_clienttemplate.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Update changelogs/fragments/2250-allow-keycloak-modules-to-take-token-as-param.yml

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Update plugins/modules/identity/keycloak/keycloak_clienttemplate.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Update plugins/modules/identity/keycloak/keycloak_group.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Update plugins/modules/identity/keycloak/keycloak_group.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Switch to modern syntax for the documentation (e.g. community.general.keycloak_client)

* Add check either creds or token as argument of all keyloak_* modules

* Update plugins/modules/identity/keycloak/keycloak_client.py

Co-authored-by: Felix Fontein <felix@fontein.de>
Co-authored-by: Amin Vakil <info@aminvakil.com>
pull/2286/head
Gaetan2907 2021-04-20 13:20:46 +02:00 committed by GitHub
parent 5b4fab80e2
commit 6ab9b05da3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 155 additions and 122 deletions

View File

@ -0,0 +1,5 @@
---
minor_changes:
- keycloak_* modules - allow the keycloak modules to use a token for the
authentication, the modules can take either a token or the credentials
(https://github.com/ansible-collections/community.general/pull/2250).

View File

@ -30,7 +30,6 @@ options:
description: description:
- Keycloak realm name to authenticate to for API access. - Keycloak realm name to authenticate to for API access.
type: str type: str
required: true
auth_client_secret: auth_client_secret:
description: description:
@ -41,7 +40,6 @@ options:
description: description:
- Username to authenticate for API access with. - Username to authenticate for API access with.
type: str type: str
required: true
aliases: aliases:
- username - username
@ -49,10 +47,15 @@ options:
description: description:
- Password to authenticate for API access with. - Password to authenticate for API access with.
type: str type: str
required: true
aliases: aliases:
- password - password
token:
description:
- Authentication token for Keycloak API.
type: str
version_added: 3.0.0
validate_certs: validate_certs:
description: description:
- Verify TLS certificates (do not disable this in production). - Verify TLS certificates (do not disable this in production).

View File

@ -57,11 +57,12 @@ def keycloak_argument_spec():
return dict( return dict(
auth_keycloak_url=dict(type='str', aliases=['url'], required=True, no_log=False), auth_keycloak_url=dict(type='str', aliases=['url'], required=True, no_log=False),
auth_client_id=dict(type='str', default='admin-cli'), auth_client_id=dict(type='str', default='admin-cli'),
auth_realm=dict(type='str', required=True), auth_realm=dict(type='str'),
auth_client_secret=dict(type='str', default=None, no_log=True), auth_client_secret=dict(type='str', default=None, no_log=True),
auth_username=dict(type='str', aliases=['username'], required=True), auth_username=dict(type='str', aliases=['username']),
auth_password=dict(type='str', aliases=['password'], required=True, no_log=True), auth_password=dict(type='str', aliases=['password'], no_log=True),
validate_certs=dict(type='bool', default=True) validate_certs=dict(type='bool', default=True),
token=dict(type='str', no_log=True),
) )
@ -73,10 +74,26 @@ class KeycloakError(Exception):
pass pass
def get_token(base_url, validate_certs, auth_realm, client_id, def get_token(module_params):
auth_username, auth_password, client_secret): """ 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')
if not base_url.lower().startswith(('http', 'https')): if not base_url.lower().startswith(('http', 'https')):
raise KeycloakError("auth_url '%s' should either start with 'http' or 'https'." % base_url) 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')
auth_url = URL_TOKEN.format(url=base_url, realm=auth_realm) auth_url = URL_TOKEN.format(url=base_url, realm=auth_realm)
temp_payload = { temp_payload = {
'grant_type': 'password', 'grant_type': 'password',
@ -101,13 +118,14 @@ def get_token(base_url, validate_certs, auth_realm, client_id,
% (auth_url, str(e))) % (auth_url, str(e)))
try: try:
return { token = r['access_token']
'Authorization': 'Bearer ' + r['access_token'],
'Content-Type': 'application/json'
}
except KeyError: except KeyError:
raise KeycloakError( raise KeycloakError(
'Could not obtain access token from %s' % auth_url) 'Could not obtain access token from %s' % auth_url)
return {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}
class KeycloakAPI(object): class KeycloakAPI(object):

View File

@ -511,20 +511,30 @@ author:
''' '''
EXAMPLES = ''' EXAMPLES = '''
- name: Create or update Keycloak client (minimal example) - name: Create or update Keycloak client (minimal example), authentication with credentials
local_action: community.general.keycloak_client:
module: keycloak_client
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth auth_keycloak_url: https://auth.example.com/auth
auth_realm: master auth_realm: master
auth_username: USERNAME auth_username: USERNAME
auth_password: PASSWORD auth_password: PASSWORD
client_id: test client_id: test
state: present state: present
delegate_to: localhost
- name: Create or update Keycloak client (minimal example), authentication with token
community.general.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 - name: Delete a Keycloak client
local_action: community.general.keycloak_client:
module: keycloak_client
auth_client_id: admin-cli auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth auth_keycloak_url: https://auth.example.com/auth
auth_realm: master auth_realm: master
@ -532,10 +542,11 @@ EXAMPLES = '''
auth_password: PASSWORD auth_password: PASSWORD
client_id: test client_id: test
state: absent state: absent
delegate_to: localhost
- name: Create or update a Keycloak client (with all the bells and whistles) - name: Create or update a Keycloak client (with all the bells and whistles)
local_action: community.general.keycloak_client:
module: keycloak_client
auth_client_id: admin-cli auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth auth_keycloak_url: https://auth.example.com/auth
auth_realm: master auth_realm: master
@ -619,6 +630,7 @@ EXAMPLES = '''
use.jwks.url: true use.jwks.url: true
jwks.url: JWKS_URL_FOR_CLIENT_AUTH_JWT jwks.url: JWKS_URL_FOR_CLIENT_AUTH_JWT
jwt.credential.certificate: JWT_CREDENTIAL_CERTIFICATE_FOR_CLIENT_AUTH jwt.credential.certificate: JWT_CREDENTIAL_CERTIFICATE_FOR_CLIENT_AUTH
delegate_to: localhost
''' '''
RETURN = ''' RETURN = '''
@ -740,21 +752,15 @@ def main():
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True, supports_check_mode=True,
required_one_of=([['client_id', 'id']])) 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={}) result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})
# Obtain access token, initialize API # Obtain access token, initialize API
try: try:
connection_header = get_token( connection_header = get_token(module.params)
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'),
)
except KeycloakError as e: except KeycloakError as e:
module.fail_json(msg=str(e)) module.fail_json(msg=str(e))

View File

@ -169,9 +169,8 @@ author:
''' '''
EXAMPLES = ''' EXAMPLES = '''
- name: Create or update Keycloak client template (minimal) - name: Create or update Keycloak client template (minimal), authentication with credentials
local_action: community.general.keycloak_client:
module: keycloak_clienttemplate
auth_client_id: admin-cli auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth auth_keycloak_url: https://auth.example.com/auth
auth_realm: master auth_realm: master
@ -179,10 +178,20 @@ EXAMPLES = '''
auth_password: PASSWORD auth_password: PASSWORD
realm: master realm: master
name: this_is_a_test name: this_is_a_test
delegate_to: localhost
- name: Create or update Keycloak client template (minimal), authentication with token
community.general.keycloak_clienttemplate:
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
token: TOKEN
realm: master
name: this_is_a_test
delegate_to: localhost
- name: Delete Keycloak client template - name: Delete Keycloak client template
local_action: community.general.keycloak_client:
module: keycloak_clienttemplate
auth_client_id: admin-cli auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth auth_keycloak_url: https://auth.example.com/auth
auth_realm: master auth_realm: master
@ -191,10 +200,10 @@ EXAMPLES = '''
realm: master realm: master
state: absent state: absent
name: test01 name: test01
delegate_to: localhost
- name: Create or update Keycloak client template (with a protocol mapper) - name: Create or update Keycloak client template (with a protocol mapper)
local_action: community.general.keycloak_client:
module: keycloak_clienttemplate
auth_client_id: admin-cli auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth auth_keycloak_url: https://auth.example.com/auth
auth_realm: master auth_realm: master
@ -217,6 +226,7 @@ EXAMPLES = '''
protocolMapper: oidc-usermodel-property-mapper protocolMapper: oidc-usermodel-property-mapper
full_scope_allowed: false full_scope_allowed: false
id: bce6f5e9-d7d3-4955-817e-c5b7f8d65b3f id: bce6f5e9-d7d3-4955-817e-c5b7f8d65b3f
delegate_to: localhost
''' '''
RETURN = ''' RETURN = '''
@ -296,21 +306,15 @@ def main():
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True, supports_check_mode=True,
required_one_of=([['id', 'name']])) 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={}) result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})
# Obtain access token, initialize API # Obtain access token, initialize API
try: try:
connection_header = get_token( connection_header = get_token(module.params)
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'),
)
except KeycloakError as e: except KeycloakError as e:
module.fail_json(msg=str(e)) module.fail_json(msg=str(e))
kc = KeycloakAPI(module, connection_header) kc = KeycloakAPI(module, connection_header)

View File

@ -81,7 +81,7 @@ author:
''' '''
EXAMPLES = ''' EXAMPLES = '''
- name: Create a Keycloak group - name: Create a Keycloak group, authentication with credentials
community.general.keycloak_group: community.general.keycloak_group:
name: my-new-kc-group name: my-new-kc-group
realm: MyCustomRealm realm: MyCustomRealm
@ -93,6 +93,16 @@ EXAMPLES = '''
auth_password: PASSWORD auth_password: PASSWORD
delegate_to: localhost delegate_to: localhost
- name: Create a Keycloak group, authentication with token
community.general.keycloak_group:
name: my-new-kc-group
realm: MyCustomRealm
state: present
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
token: TOKEN
delegate_to: localhost
- name: Delete a keycloak group - name: Delete a keycloak group
community.general.keycloak_group: community.general.keycloak_group:
id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd' id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd'
@ -217,30 +227,25 @@ def main():
realm=dict(default='master'), realm=dict(default='master'),
id=dict(type='str'), id=dict(type='str'),
name=dict(type='str'), name=dict(type='str'),
attributes=dict(type='dict') attributes=dict(type='dict'),
) )
argument_spec.update(meta_args) argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True, supports_check_mode=True,
required_one_of=([['id', 'name']])) 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={}, group='') result = dict(changed=False, msg='', diff={}, group='')
# Obtain access token, initialize API # Obtain access token, initialize API
try: try:
connection_header = get_token( connection_header = get_token(module.params)
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'),
)
except KeycloakError as e: except KeycloakError as e:
module.fail_json(msg=str(e)) module.fail_json(msg=str(e))
kc = KeycloakAPI(module, connection_header) kc = KeycloakAPI(module, connection_header)
realm = module.params.get('realm') realm = module.params.get('realm')

View File

@ -11,6 +11,16 @@ from ansible_collections.community.general.plugins.module_utils.identity.keycloa
from ansible.module_utils.six import StringIO from ansible.module_utils.six import StringIO
from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.urllib.error import HTTPError
module_params_creds = {
'auth_keycloak_url': 'http://keycloak.url/auth',
'validate_certs': True,
'auth_realm': 'master',
'client_id': 'admin-cli',
'auth_username': 'admin',
'auth_password': 'admin',
'client_secret': None,
}
def build_mocked_request(get_id_user_count, response_dict): def build_mocked_request(get_id_user_count, response_dict):
def _mocked_requests(*args, **kwargs): def _mocked_requests(*args, **kwargs):
@ -58,16 +68,22 @@ def mock_good_connection(mocker):
) )
def test_connect_to_keycloak(mock_good_connection): def test_connect_to_keycloak_with_creds(mock_good_connection):
keycloak_header = get_token( keycloak_header = get_token(module_params_creds)
base_url='http://keycloak.url/auth', assert keycloak_header == {
validate_certs=True, 'Authorization': 'Bearer alongtoken',
auth_realm='master', 'Content-Type': 'application/json'
client_id='admin-cli', }
auth_username='admin',
auth_password='admin',
client_secret=None def test_connect_to_keycloak_with_token(mock_good_connection):
) module_params_token = {
'auth_keycloak_url': 'http://keycloak.url/auth',
'validate_certs': True,
'client_id': 'admin-cli',
'token': "alongtoken"
}
keycloak_header = get_token(module_params_token)
assert keycloak_header == { assert keycloak_header == {
'Authorization': 'Bearer alongtoken', 'Authorization': 'Bearer alongtoken',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -87,15 +103,7 @@ def mock_bad_json_returned(mocker):
def test_bad_json_returned(mock_bad_json_returned): def test_bad_json_returned(mock_bad_json_returned):
with pytest.raises(KeycloakError) as raised_error: with pytest.raises(KeycloakError) as raised_error:
get_token( get_token(module_params_creds)
base_url='http://keycloak.url/auth',
validate_certs=True,
auth_realm='master',
client_id='admin-cli',
auth_username='admin',
auth_password='admin',
client_secret=None
)
# cannot check all the message, different errors message for the value # cannot check all the message, different errors message for the value
# error in python 2.6, 2.7 and 3.*. # error in python 2.6, 2.7 and 3.*.
assert ( assert (
@ -125,15 +133,7 @@ def mock_401_returned(mocker):
def test_error_returned(mock_401_returned): def test_error_returned(mock_401_returned):
with pytest.raises(KeycloakError) as raised_error: with pytest.raises(KeycloakError) as raised_error:
get_token( get_token(module_params_creds)
base_url='http://keycloak.url/auth',
validate_certs=True,
auth_realm='master',
client_id='admin-cli',
auth_username='notadminuser',
auth_password='notadminpassword',
client_secret=None
)
assert str(raised_error.value) == ( assert str(raised_error.value) == (
'Could not obtain access token from http://keycloak.url' 'Could not obtain access token from http://keycloak.url'
'/auth/realms/master/protocol/openid-connect/token: ' '/auth/realms/master/protocol/openid-connect/token: '
@ -154,15 +154,7 @@ def mock_json_without_token_returned(mocker):
def test_json_without_token_returned(mock_json_without_token_returned): def test_json_without_token_returned(mock_json_without_token_returned):
with pytest.raises(KeycloakError) as raised_error: with pytest.raises(KeycloakError) as raised_error:
get_token( get_token(module_params_creds)
base_url='http://keycloak.url/auth',
validate_certs=True,
auth_realm='master',
client_id='admin-cli',
auth_username='admin',
auth_password='admin',
client_secret=None
)
assert str(raised_error.value) == ( assert str(raised_error.value) == (
'Could not obtain access token from http://keycloak.url' 'Could not obtain access token from http://keycloak.url'
'/auth/realms/master/protocol/openid-connect/token' '/auth/realms/master/protocol/openid-connect/token'