diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 3e319e200df..0c2f1637b63 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -534,6 +534,8 @@ files: maintainers: elfelip Gaetan2907 $modules/identity/keycloak/keycloak_clientscope.py: maintainers: Gaetan2907 + $modules/identity/keycloak/keycloak_clientsecret.py: + maintainers: fynncfchen $modules/identity/keycloak/keycloak_client_rolemapping.py: maintainers: Gaetan2907 $modules/identity/keycloak/keycloak_group.py: diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 0ede2dc0bab..cac2a378bbb 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -67,6 +67,8 @@ URL_CLIENT_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/available" URL_CLIENT_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/composite" +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" @@ -877,6 +879,52 @@ def update_clientscope_protocolmappers(self, cid, mapper_rep, realm="master"): self.module.fail_json(msg='Could not update protocolmappers for clientscope %s in realm %s: %s' % (mapper_rep, realm, 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', headers=self.restheaders, + validate_certs=self.validate_certs).read())) + + except HTTPError as e: + if e.code == 404: + return None + else: + self.module.fail_json(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', headers=self.restheaders, + validate_certs=self.validate_certs).read())) + + except HTTPError as e: + if e.code == 404: + return None + else: + self.module.fail_json(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. diff --git a/plugins/modules/identity/keycloak/keycloak_clientsecret.py b/plugins/modules/identity/keycloak/keycloak_clientsecret.py new file mode 100644 index 00000000000..42c851fd56a --- /dev/null +++ b/plugins/modules/identity/keycloak/keycloak_clientsecret.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: keycloak_clientsecret + +short_description: Administration of Keycloak client secret via Keycloak API + +version_added: 4.4.0 + +description: + - This module allows you to get or generate new Keycloak client secret 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. + + - When generate a new client secret, where possible provide the client ID (not client_id) to the module. + This removes a lookup to the API to translate the client_id into the client ID. + + +options: + state: + description: + - State of the client secret. + - On C(present), get the current client secret. + - On C(absent), the new client secret will be generated. + default: 'present' + type: str + choices: + - present + - absent + + realm: + type: str + description: + - They Keycloak realm under which this client resides. + default: 'master' + + id: + description: + - The unique identifier for this client. + - This parameter is not required for getting or generating a client secret but + providing it will reduce the number of API calls required. + type: str + client_id: + description: + - The client_id of the client to lookup account client ID + aliases: + - clientId + type: str + + +extends_documentation_fragment: +- community.general.keycloak + +author: + - Fynn Chen (@fynncfchen) +''' + +EXAMPLES = ''' +- name: Get a Keycloak client secret, authentication with credentials + community.general.keycloak_clientsecret: + id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd' + 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: Generate a new Keycloak client secret, authentication with token + community.general.keycloak_user: + id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd' + realm: MyCustomRealm + state: absent + auth_client_id: admin-cli + auth_keycloak_url: https://auth.example.com/auth + token: TOKEN + delegate_to: localhost +''' + +RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + +end_state: + description: Representation of the client credential after module execution (sample is truncated). + returned: on success + type: complex + contains: + type: + description: Credential type. + type: str + returned: always + sample: secret + value: + description: Secret of the client. + type: str + returned: always + sample: cUGnX1EIeTtPPAkcyGMv0ncyqDPu68P1 + +clientsecret: + description: + - Representation of the client credential after module execution. + - Deprecated return value, it will be removed in community.general 6.0.0. Please use the return value I(end_state) instead. + returned: always + type: complex + contains: + type: + description: Credential type. + type: str + returned: always + sample: secret + value: + description: Secret of the client. + type: str + returned: always + sample: cUGnX1EIeTtPPAkcyGMv0ncyqDPu68P1 + +''' + +from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ + keycloak_argument_spec, get_token, KeycloakError +from ansible.module_utils.basic import AnsibleModule + + +def main(): + """ + Module execution + + :return: + """ + argument_spec = keycloak_argument_spec() + + meta_args = dict( + state=dict(default='present', choices=['present', 'absent']), + realm=dict(default='master'), + + id=dict(type='str'), + client_id=dict(type='str', aliases=['clientId']), + ) + + argument_spec.update(meta_args) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=([['id', 'client_id'], + ['token', 'auth_realm', 'auth_username', 'auth_password']]), + required_together=([['auth_realm', 'auth_username', 'auth_password']])) + + result = dict(changed=False, msg='', diff={}, clientsecret='') + + # 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') + id = module.params.get('id') + client_id = module.params.get('client_id') + + # only lookup the client_id if id isn't provided. + # in the case that both are provided, prefer the ID, since it's one + # less lookup. + if id is None and client_id is not None: + client = kc.get_client_by_clientid(client_id, realm=realm) + + if client is None: + raise Exception('Client does not exist {client_id}'.format(client_id=client_id)) + + id = client['id'] + + if state == 'present': + # Get secret + result['changed'] = False + + if module.check_mode: + module.exit_json(**result) + + # Create new secret + clientsecret = kc.get_clientsecret(id=id, realm=realm) + + result['clientsecret'] = clientsecret + result['end_state'] = clientsecret + result['msg'] = 'Get client secret successful for ID {id}'.format(id=id) + + module.exit_json(**result) + else: + if state == 'absent': + # Process a creation + result['changed'] = True + + if module.check_mode: + module.exit_json(**result) + + # Create new secret + clientsecret = kc.create_clientsecret(id=id, realm=realm) + + result['end_state'] = clientsecret + result['msg'] = 'New client secret has been generated for ID {id}'.format(id=id) + + module.exit_json(**result) + + # Do nothing and exit + result['changed'] = False + result['end_state'] = {} + result['msg'] = 'State not specified; doing nothing.' + module.exit_json(**result) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/keycloak_clientsecret.py b/plugins/modules/keycloak_clientsecret.py new file mode 120000 index 00000000000..775075dcd0f --- /dev/null +++ b/plugins/modules/keycloak_clientsecret.py @@ -0,0 +1 @@ +./identity/keycloak/keycloak_clientsecret.py \ No newline at end of file