1# -*- coding: utf-8 -*-
2# Copyright (c) 2017, Eike Frost <ei@kefro.st>
3#
4# This code is part of Ansible, but is an independent component.
5# This particular file snippet, and this file snippet only, is BSD licensed.
6# Modules you write using this snippet, which is embedded dynamically by Ansible
7# still belong to the author of the module, and may assign their own license
8# to the complete work.
9#
10# Redistribution and use in source and binary forms, with or without modification,
11# are permitted provided that the following conditions are met:
12#
13#    * Redistributions of source code must retain the above copyright
14#      notice, this list of conditions and the following disclaimer.
15#    * Redistributions in binary form must reproduce the above copyright notice,
16#      this list of conditions and the following disclaimer in the documentation
17#      and/or other materials provided with the distribution.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
27# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29from __future__ import absolute_import, division, print_function
30
31__metaclass__ = type
32
33import json
34import traceback
35
36from ansible.module_utils.urls import open_url
37from ansible.module_utils.six.moves.urllib.parse import urlencode, quote
38from ansible.module_utils.six.moves.urllib.error import HTTPError
39from ansible.module_utils.common.text.converters import to_native, to_text
40
41URL_REALMS = "{url}/admin/realms"
42URL_REALM = "{url}/admin/realms/{realm}"
43
44URL_TOKEN = "{url}/realms/{realm}/protocol/openid-connect/token"
45URL_CLIENT = "{url}/admin/realms/{realm}/clients/{id}"
46URL_CLIENTS = "{url}/admin/realms/{realm}/clients"
47
48URL_CLIENT_ROLES = "{url}/admin/realms/{realm}/clients/{id}/roles"
49URL_CLIENT_ROLE = "{url}/admin/realms/{realm}/clients/{id}/roles/{name}"
50URL_CLIENT_ROLE_COMPOSITES = "{url}/admin/realms/{realm}/clients/{id}/roles/{name}/composites"
51
52URL_REALM_ROLES = "{url}/admin/realms/{realm}/roles"
53URL_REALM_ROLE = "{url}/admin/realms/{realm}/roles/{name}"
54URL_REALM_ROLE_COMPOSITES = "{url}/admin/realms/{realm}/roles/{name}/composites"
55
56URL_CLIENTTEMPLATE = "{url}/admin/realms/{realm}/client-templates/{id}"
57URL_CLIENTTEMPLATES = "{url}/admin/realms/{realm}/client-templates"
58URL_GROUPS = "{url}/admin/realms/{realm}/groups"
59URL_GROUP = "{url}/admin/realms/{realm}/groups/{groupid}"
60
61URL_CLIENTSCOPES = "{url}/admin/realms/{realm}/client-scopes"
62URL_CLIENTSCOPE = "{url}/admin/realms/{realm}/client-scopes/{id}"
63URL_CLIENTSCOPE_PROTOCOLMAPPERS = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models"
64URL_CLIENTSCOPE_PROTOCOLMAPPER = "{url}/admin/realms/{realm}/client-scopes/{id}/protocol-mappers/models/{mapper_id}"
65
66URL_CLIENT_ROLEMAPPINGS = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}"
67URL_CLIENT_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/available"
68URL_CLIENT_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/composite"
69
70URL_AUTHENTICATION_FLOWS = "{url}/admin/realms/{realm}/authentication/flows"
71URL_AUTHENTICATION_FLOW = "{url}/admin/realms/{realm}/authentication/flows/{id}"
72URL_AUTHENTICATION_FLOW_COPY = "{url}/admin/realms/{realm}/authentication/flows/{copyfrom}/copy"
73URL_AUTHENTICATION_FLOW_EXECUTIONS = "{url}/admin/realms/{realm}/authentication/flows/{flowalias}/executions"
74URL_AUTHENTICATION_FLOW_EXECUTIONS_EXECUTION = "{url}/admin/realms/{realm}/authentication/flows/{flowalias}/executions/execution"
75URL_AUTHENTICATION_FLOW_EXECUTIONS_FLOW = "{url}/admin/realms/{realm}/authentication/flows/{flowalias}/executions/flow"
76URL_AUTHENTICATION_EXECUTION_CONFIG = "{url}/admin/realms/{realm}/authentication/executions/{id}/config"
77URL_AUTHENTICATION_EXECUTION_RAISE_PRIORITY = "{url}/admin/realms/{realm}/authentication/executions/{id}/raise-priority"
78URL_AUTHENTICATION_EXECUTION_LOWER_PRIORITY = "{url}/admin/realms/{realm}/authentication/executions/{id}/lower-priority"
79URL_AUTHENTICATION_CONFIG = "{url}/admin/realms/{realm}/authentication/config/{id}"
80
81URL_IDENTITY_PROVIDERS = "{url}/admin/realms/{realm}/identity-provider/instances"
82URL_IDENTITY_PROVIDER = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}"
83URL_IDENTITY_PROVIDER_MAPPERS = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}/mappers"
84URL_IDENTITY_PROVIDER_MAPPER = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}/mappers/{id}"
85
86URL_COMPONENTS = "{url}/admin/realms/{realm}/components"
87URL_COMPONENT = "{url}/admin/realms/{realm}/components/{id}"
88
89
90def keycloak_argument_spec():
91    """
92    Returns argument_spec of options common to keycloak_*-modules
93
94    :return: argument_spec dict
95    """
96    return dict(
97        auth_keycloak_url=dict(type='str', aliases=['url'], required=True, no_log=False),
98        auth_client_id=dict(type='str', default='admin-cli'),
99        auth_realm=dict(type='str'),
100        auth_client_secret=dict(type='str', default=None, no_log=True),
101        auth_username=dict(type='str', aliases=['username']),
102        auth_password=dict(type='str', aliases=['password'], no_log=True),
103        validate_certs=dict(type='bool', default=True),
104        token=dict(type='str', no_log=True),
105    )
106
107
108def camel(words):
109    return words.split('_')[0] + ''.join(x.capitalize() or '_' for x in words.split('_')[1:])
110
111
112class KeycloakError(Exception):
113    pass
114
115
116def get_token(module_params):
117    """ Obtains connection header with token for the authentication,
118        token already given or obtained from credentials
119        :param module_params: parameters of the module
120        :return: connection header
121    """
122    token = module_params.get('token')
123    base_url = module_params.get('auth_keycloak_url')
124
125    if not base_url.lower().startswith(('http', 'https')):
126        raise KeycloakError("auth_url '%s' should either start with 'http' or 'https'." % base_url)
127
128    if token is None:
129        base_url = module_params.get('auth_keycloak_url')
130        validate_certs = module_params.get('validate_certs')
131        auth_realm = module_params.get('auth_realm')
132        client_id = module_params.get('auth_client_id')
133        auth_username = module_params.get('auth_username')
134        auth_password = module_params.get('auth_password')
135        client_secret = module_params.get('auth_client_secret')
136        auth_url = URL_TOKEN.format(url=base_url, realm=auth_realm)
137        temp_payload = {
138            'grant_type': 'password',
139            'client_id': client_id,
140            'client_secret': client_secret,
141            'username': auth_username,
142            'password': auth_password,
143        }
144        # Remove empty items, for instance missing client_secret
145        payload = dict(
146            (k, v) for k, v in temp_payload.items() if v is not None)
147        try:
148            r = json.loads(to_native(open_url(auth_url, method='POST',
149                                              validate_certs=validate_certs,
150                                              data=urlencode(payload)).read()))
151        except ValueError as e:
152            raise KeycloakError(
153                'API returned invalid JSON when trying to obtain access token from %s: %s'
154                % (auth_url, str(e)))
155        except Exception as e:
156            raise KeycloakError('Could not obtain access token from %s: %s'
157                                % (auth_url, str(e)))
158
159        try:
160            token = r['access_token']
161        except KeyError:
162            raise KeycloakError(
163                'Could not obtain access token from %s' % auth_url)
164    return {
165        'Authorization': 'Bearer ' + token,
166        'Content-Type': 'application/json'
167    }
168
169
170def is_struct_included(struct1, struct2, exclude=None):
171    """
172    This function compare if the first parameter structure is included in the second.
173    The function use every elements of struct1 and validates they are present in the struct2 structure.
174    The two structure does not need to be equals for that function to return true.
175    Each elements are compared recursively.
176    :param struct1:
177        type:
178            dict for the initial call, can be dict, list, bool, int or str for recursive calls
179        description:
180            reference structure
181    :param struct2:
182        type:
183            dict for the initial call, can be dict, list, bool, int or str for recursive calls
184        description:
185            structure to compare with first parameter.
186    :param exclude:
187        type:
188            list
189        description:
190            Key to exclude from the comparison.
191        default: None
192    :return:
193        type:
194            bool
195        description:
196            Return True if all element of dict 1 are present in dict 2, return false otherwise.
197    """
198    if isinstance(struct1, list) and isinstance(struct2, list):
199        for item1 in struct1:
200            if isinstance(item1, (list, dict)):
201                for item2 in struct2:
202                    if not is_struct_included(item1, item2, exclude):
203                        return False
204            else:
205                if item1 not in struct2:
206                    return False
207        return True
208    elif isinstance(struct1, dict) and isinstance(struct2, dict):
209        try:
210            for key in struct1:
211                if not (exclude and key in exclude):
212                    if not is_struct_included(struct1[key], struct2[key], exclude):
213                        return False
214            return True
215        except KeyError:
216            return False
217    elif isinstance(struct1, bool) and isinstance(struct2, bool):
218        return struct1 == struct2
219    else:
220        return to_text(struct1, 'utf-8') == to_text(struct2, 'utf-8')
221
222
223class KeycloakAPI(object):
224    """ Keycloak API access; Keycloak uses OAuth 2.0 to protect its API, an access token for which
225        is obtained through OpenID connect
226    """
227    def __init__(self, module, connection_header):
228        self.module = module
229        self.baseurl = self.module.params.get('auth_keycloak_url')
230        self.validate_certs = self.module.params.get('validate_certs')
231        self.restheaders = connection_header
232
233    def get_realm_by_id(self, realm='master'):
234        """ Obtain realm representation by id
235
236        :param realm: realm id
237        :return: dict of real, representation or None if none matching exist
238        """
239        realm_url = URL_REALM.format(url=self.baseurl, realm=realm)
240
241        try:
242            return json.loads(to_native(open_url(realm_url, method='GET', headers=self.restheaders,
243                                                 validate_certs=self.validate_certs).read()))
244
245        except HTTPError as e:
246            if e.code == 404:
247                return None
248            else:
249                self.module.fail_json(msg='Could not obtain realm %s: %s' % (realm, str(e)),
250                                      exception=traceback.format_exc())
251        except ValueError as e:
252            self.module.fail_json(msg='API returned incorrect JSON when trying to obtain realm %s: %s' % (realm, str(e)),
253                                  exception=traceback.format_exc())
254        except Exception as e:
255            self.module.fail_json(msg='Could not obtain realm %s: %s' % (realm, str(e)),
256                                  exception=traceback.format_exc())
257
258    def update_realm(self, realmrep, realm="master"):
259        """ Update an existing realm
260        :param realmrep: corresponding (partial/full) realm representation with updates
261        :param realm: realm to be updated in Keycloak
262        :return: HTTPResponse object on success
263        """
264        realm_url = URL_REALM.format(url=self.baseurl, realm=realm)
265
266        try:
267            return open_url(realm_url, method='PUT', headers=self.restheaders,
268                            data=json.dumps(realmrep), validate_certs=self.validate_certs)
269        except Exception as e:
270            self.module.fail_json(msg='Could not update realm %s: %s' % (realm, str(e)),
271                                  exception=traceback.format_exc())
272
273    def create_realm(self, realmrep):
274        """ Create a realm in keycloak
275        :param realmrep: Realm representation of realm to be created.
276        :return: HTTPResponse object on success
277        """
278        realm_url = URL_REALMS.format(url=self.baseurl)
279
280        try:
281            return open_url(realm_url, method='POST', headers=self.restheaders,
282                            data=json.dumps(realmrep), validate_certs=self.validate_certs)
283        except Exception as e:
284            self.module.fail_json(msg='Could not create realm %s: %s' % (realmrep['id'], str(e)),
285                                  exception=traceback.format_exc())
286
287    def delete_realm(self, realm="master"):
288        """ Delete a realm from Keycloak
289
290        :param realm: realm to be deleted
291        :return: HTTPResponse object on success
292        """
293        realm_url = URL_REALM.format(url=self.baseurl, realm=realm)
294
295        try:
296            return open_url(realm_url, method='DELETE', headers=self.restheaders,
297                            validate_certs=self.validate_certs)
298        except Exception as e:
299            self.module.fail_json(msg='Could not delete realm %s: %s' % (realm, str(e)),
300                                  exception=traceback.format_exc())
301
302    def get_clients(self, realm='master', filter=None):
303        """ Obtains client representations for clients in a realm
304
305        :param realm: realm to be queried
306        :param filter: if defined, only the client with clientId specified in the filter is returned
307        :return: list of dicts of client representations
308        """
309        clientlist_url = URL_CLIENTS.format(url=self.baseurl, realm=realm)
310        if filter is not None:
311            clientlist_url += '?clientId=%s' % filter
312
313        try:
314            return json.loads(to_native(open_url(clientlist_url, method='GET', headers=self.restheaders,
315                                                 validate_certs=self.validate_certs).read()))
316        except ValueError as e:
317            self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of clients for realm %s: %s'
318                                      % (realm, str(e)))
319        except Exception as e:
320            self.module.fail_json(msg='Could not obtain list of clients for realm %s: %s'
321                                      % (realm, str(e)))
322
323    def get_client_by_clientid(self, client_id, realm='master'):
324        """ Get client representation by clientId
325        :param client_id: The clientId to be queried
326        :param realm: realm from which to obtain the client representation
327        :return: dict with a client representation or None if none matching exist
328        """
329        r = self.get_clients(realm=realm, filter=client_id)
330        if len(r) > 0:
331            return r[0]
332        else:
333            return None
334
335    def get_client_by_id(self, id, realm='master'):
336        """ Obtain client representation by id
337
338        :param id: id (not clientId) of client to be queried
339        :param realm: client from this realm
340        :return: dict of client representation or None if none matching exist
341        """
342        client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
343
344        try:
345            return json.loads(to_native(open_url(client_url, method='GET', headers=self.restheaders,
346                                                 validate_certs=self.validate_certs).read()))
347
348        except HTTPError as e:
349            if e.code == 404:
350                return None
351            else:
352                self.module.fail_json(msg='Could not obtain client %s for realm %s: %s'
353                                          % (id, realm, str(e)))
354        except ValueError as e:
355            self.module.fail_json(msg='API returned incorrect JSON when trying to obtain client %s for realm %s: %s'
356                                      % (id, realm, str(e)))
357        except Exception as e:
358            self.module.fail_json(msg='Could not obtain client %s for realm %s: %s'
359                                      % (id, realm, str(e)))
360
361    def get_client_id(self, client_id, realm='master'):
362        """ Obtain id of client by client_id
363
364        :param client_id: client_id of client to be queried
365        :param realm: client template from this realm
366        :return: id of client (usually a UUID)
367        """
368        result = self.get_client_by_clientid(client_id, realm)
369        if isinstance(result, dict) and 'id' in result:
370            return result['id']
371        else:
372            return None
373
374    def update_client(self, id, clientrep, realm="master"):
375        """ Update an existing client
376        :param id: id (not clientId) of client to be updated in Keycloak
377        :param clientrep: corresponding (partial/full) client representation with updates
378        :param realm: realm the client is in
379        :return: HTTPResponse object on success
380        """
381        client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
382
383        try:
384            return open_url(client_url, method='PUT', headers=self.restheaders,
385                            data=json.dumps(clientrep), validate_certs=self.validate_certs)
386        except Exception as e:
387            self.module.fail_json(msg='Could not update client %s in realm %s: %s'
388                                      % (id, realm, str(e)))
389
390    def create_client(self, clientrep, realm="master"):
391        """ Create a client in keycloak
392        :param clientrep: Client representation of client to be created. Must at least contain field clientId.
393        :param realm: realm for client to be created.
394        :return: HTTPResponse object on success
395        """
396        client_url = URL_CLIENTS.format(url=self.baseurl, realm=realm)
397
398        try:
399            return open_url(client_url, method='POST', headers=self.restheaders,
400                            data=json.dumps(clientrep), validate_certs=self.validate_certs)
401        except Exception as e:
402            self.module.fail_json(msg='Could not create client %s in realm %s: %s'
403                                      % (clientrep['clientId'], realm, str(e)))
404
405    def delete_client(self, id, realm="master"):
406        """ Delete a client from Keycloak
407
408        :param id: id (not clientId) of client to be deleted
409        :param realm: realm of client to be deleted
410        :return: HTTPResponse object on success
411        """
412        client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
413
414        try:
415            return open_url(client_url, method='DELETE', headers=self.restheaders,
416                            validate_certs=self.validate_certs)
417        except Exception as e:
418            self.module.fail_json(msg='Could not delete client %s in realm %s: %s'
419                                      % (id, realm, str(e)))
420
421    def get_client_roles_by_id(self, cid, realm="master"):
422        """ Fetch the roles of the a client on the Keycloak server.
423
424        :param cid: ID of the client from which to obtain the rolemappings.
425        :param realm: Realm from which to obtain the rolemappings.
426        :return: The rollemappings of specified group and client of the realm (default "master").
427        """
428        client_roles_url = URL_CLIENT_ROLES.format(url=self.baseurl, realm=realm, id=cid)
429        try:
430            return json.loads(to_native(open_url(client_roles_url, method="GET", headers=self.restheaders,
431                                                 validate_certs=self.validate_certs).read()))
432        except Exception as e:
433            self.module.fail_json(msg="Could not fetch rolemappings for client %s in realm %s: %s"
434                                      % (cid, realm, str(e)))
435
436    def get_client_role_by_name(self, gid, cid, name, realm="master"):
437        """ Get the role ID of a client.
438
439        :param gid: ID of the group from which to obtain the rolemappings.
440        :param cid: ID of the client from which to obtain the rolemappings.
441        :param name: Name of the role.
442        :param realm: Realm from which to obtain the rolemappings.
443        :return: The ID of the role, None if not found.
444        """
445        rolemappings = self.get_client_roles_by_id(cid, realm=realm)
446        for role in rolemappings:
447            if name == role['name']:
448                return role['id']
449        return None
450
451    def get_client_rolemapping_by_id(self, gid, cid, rid, realm='master'):
452        """ Obtain client representation by id
453
454        :param gid: ID of the group from which to obtain the rolemappings.
455        :param cid: ID of the client from which to obtain the rolemappings.
456        :param rid: ID of the role.
457        :param realm: client from this realm
458        :return: dict of rolemapping representation or None if none matching exist
459        """
460        rolemappings_url = URL_CLIENT_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid)
461        try:
462            rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", headers=self.restheaders,
463                                                         validate_certs=self.validate_certs).read()))
464            for role in rolemappings:
465                if rid == role['id']:
466                    return role
467        except Exception as e:
468            self.module.fail_json(msg="Could not fetch rolemappings for client %s in group %s, realm %s: %s"
469                                      % (cid, gid, realm, str(e)))
470        return None
471
472    def get_client_available_rolemappings(self, gid, cid, realm="master"):
473        """ Fetch the available role of a client in a specified goup on the Keycloak server.
474
475        :param gid: ID of the group from which to obtain the rolemappings.
476        :param cid: ID of the client from which to obtain the rolemappings.
477        :param realm: Realm from which to obtain the rolemappings.
478        :return: The rollemappings of specified group and client of the realm (default "master").
479        """
480        available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS_AVAILABLE.format(url=self.baseurl, realm=realm, id=gid, client=cid)
481        try:
482            return json.loads(to_native(open_url(available_rolemappings_url, method="GET", headers=self.restheaders,
483                                                 validate_certs=self.validate_certs).read()))
484        except Exception as e:
485            self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s"
486                                      % (cid, gid, realm, str(e)))
487
488    def get_client_composite_rolemappings(self, gid, cid, realm="master"):
489        """ Fetch the composite role of a client in a specified group on the Keycloak server.
490
491        :param gid: ID of the group from which to obtain the rolemappings.
492        :param cid: ID of the client from which to obtain the rolemappings.
493        :param realm: Realm from which to obtain the rolemappings.
494        :return: The rollemappings of specified group and client of the realm (default "master").
495        """
496        available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS_COMPOSITE.format(url=self.baseurl, realm=realm, id=gid, client=cid)
497        try:
498            return json.loads(to_native(open_url(available_rolemappings_url, method="GET", headers=self.restheaders,
499                                                 validate_certs=self.validate_certs).read()))
500        except Exception as e:
501            self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s"
502                                      % (cid, gid, realm, str(e)))
503
504    def add_group_rolemapping(self, gid, cid, role_rep, realm="master"):
505        """ Fetch the composite role of a client in a specified goup on the Keycloak server.
506
507        :param gid: ID of the group from which to obtain the rolemappings.
508        :param cid: ID of the client from which to obtain the rolemappings.
509        :param role_rep: Representation of the role to assign.
510        :param realm: Realm from which to obtain the rolemappings.
511        :return: None.
512        """
513        available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid)
514        try:
515            open_url(available_rolemappings_url, method="POST", headers=self.restheaders, data=json.dumps(role_rep), validate_certs=self.validate_certs)
516        except Exception as e:
517            self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s"
518                                      % (cid, gid, realm, str(e)))
519
520    def delete_group_rolemapping(self, gid, cid, role_rep, realm="master"):
521        """ Delete the rolemapping of a client in a specified group on the Keycloak server.
522
523        :param gid: ID of the group from which to obtain the rolemappings.
524        :param cid: ID of the client from which to obtain the rolemappings.
525        :param role_rep: Representation of the role to assign.
526        :param realm: Realm from which to obtain the rolemappings.
527        :return: None.
528        """
529        available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid)
530        try:
531            open_url(available_rolemappings_url, method="DELETE", headers=self.restheaders, validate_certs=self.validate_certs)
532        except Exception as e:
533            self.module.fail_json(msg="Could not delete available rolemappings for client %s in group %s, realm %s: %s"
534                                      % (cid, gid, realm, str(e)))
535
536    def get_client_templates(self, realm='master'):
537        """ Obtains client template representations for client templates in a realm
538
539        :param realm: realm to be queried
540        :return: list of dicts of client representations
541        """
542        url = URL_CLIENTTEMPLATES.format(url=self.baseurl, realm=realm)
543
544        try:
545            return json.loads(to_native(open_url(url, method='GET', headers=self.restheaders,
546                                                 validate_certs=self.validate_certs).read()))
547        except ValueError as e:
548            self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of client templates for realm %s: %s'
549                                      % (realm, str(e)))
550        except Exception as e:
551            self.module.fail_json(msg='Could not obtain list of client templates for realm %s: %s'
552                                      % (realm, str(e)))
553
554    def get_client_template_by_id(self, id, realm='master'):
555        """ Obtain client template representation by id
556
557        :param id: id (not name) of client template to be queried
558        :param realm: client template from this realm
559        :return: dict of client template representation or None if none matching exist
560        """
561        url = URL_CLIENTTEMPLATE.format(url=self.baseurl, id=id, realm=realm)
562
563        try:
564            return json.loads(to_native(open_url(url, method='GET', headers=self.restheaders,
565                                                 validate_certs=self.validate_certs).read()))
566        except ValueError as e:
567            self.module.fail_json(msg='API returned incorrect JSON when trying to obtain client templates %s for realm %s: %s'
568                                      % (id, realm, str(e)))
569        except Exception as e:
570            self.module.fail_json(msg='Could not obtain client template %s for realm %s: %s'
571                                      % (id, realm, str(e)))
572
573    def get_client_template_by_name(self, name, realm='master'):
574        """ Obtain client template representation by name
575
576        :param name: name of client template to be queried
577        :param realm: client template from this realm
578        :return: dict of client template representation or None if none matching exist
579        """
580        result = self.get_client_templates(realm)
581        if isinstance(result, list):
582            result = [x for x in result if x['name'] == name]
583            if len(result) > 0:
584                return result[0]
585        return None
586
587    def get_client_template_id(self, name, realm='master'):
588        """ Obtain client template id by name
589
590        :param name: name of client template to be queried
591        :param realm: client template from this realm
592        :return: client template id (usually a UUID)
593        """
594        result = self.get_client_template_by_name(name, realm)
595        if isinstance(result, dict) and 'id' in result:
596            return result['id']
597        else:
598            return None
599
600    def update_client_template(self, id, clienttrep, realm="master"):
601        """ Update an existing client template
602        :param id: id (not name) of client template to be updated in Keycloak
603        :param clienttrep: corresponding (partial/full) client template representation with updates
604        :param realm: realm the client template is in
605        :return: HTTPResponse object on success
606        """
607        url = URL_CLIENTTEMPLATE.format(url=self.baseurl, realm=realm, id=id)
608
609        try:
610            return open_url(url, method='PUT', headers=self.restheaders,
611                            data=json.dumps(clienttrep), validate_certs=self.validate_certs)
612        except Exception as e:
613            self.module.fail_json(msg='Could not update client template %s in realm %s: %s'
614                                      % (id, realm, str(e)))
615
616    def create_client_template(self, clienttrep, realm="master"):
617        """ Create a client in keycloak
618        :param clienttrep: Client template representation of client template to be created. Must at least contain field name
619        :param realm: realm for client template to be created in
620        :return: HTTPResponse object on success
621        """
622        url = URL_CLIENTTEMPLATES.format(url=self.baseurl, realm=realm)
623
624        try:
625            return open_url(url, method='POST', headers=self.restheaders,
626                            data=json.dumps(clienttrep), validate_certs=self.validate_certs)
627        except Exception as e:
628            self.module.fail_json(msg='Could not create client template %s in realm %s: %s'
629                                      % (clienttrep['clientId'], realm, str(e)))
630
631    def delete_client_template(self, id, realm="master"):
632        """ Delete a client template from Keycloak
633
634        :param id: id (not name) of client to be deleted
635        :param realm: realm of client template to be deleted
636        :return: HTTPResponse object on success
637        """
638        url = URL_CLIENTTEMPLATE.format(url=self.baseurl, realm=realm, id=id)
639
640        try:
641            return open_url(url, method='DELETE', headers=self.restheaders,
642                            validate_certs=self.validate_certs)
643        except Exception as e:
644            self.module.fail_json(msg='Could not delete client template %s in realm %s: %s'
645                                      % (id, realm, str(e)))
646
647    def get_clientscopes(self, realm="master"):
648        """ Fetch the name and ID of all clientscopes on the Keycloak server.
649
650        To fetch the full data of the group, make a subsequent call to
651        get_clientscope_by_clientscopeid, passing in the ID of the group you wish to return.
652
653        :param realm: Realm in which the clientscope resides; default 'master'.
654        :return The clientscopes of this realm (default "master")
655        """
656        clientscopes_url = URL_CLIENTSCOPES.format(url=self.baseurl, realm=realm)
657        try:
658            return json.loads(to_native(open_url(clientscopes_url, method="GET", headers=self.restheaders,
659                                                 validate_certs=self.validate_certs).read()))
660        except Exception as e:
661            self.module.fail_json(msg="Could not fetch list of clientscopes in realm %s: %s"
662                                      % (realm, str(e)))
663
664    def get_clientscope_by_clientscopeid(self, cid, realm="master"):
665        """ Fetch a keycloak clientscope from the provided realm using the clientscope's unique ID.
666
667        If the clientscope does not exist, None is returned.
668
669        gid is a UUID provided by the Keycloak API
670        :param cid: UUID of the clientscope to be returned
671        :param realm: Realm in which the clientscope resides; default 'master'.
672        """
673        clientscope_url = URL_CLIENTSCOPE.format(url=self.baseurl, realm=realm, id=cid)
674        try:
675            return json.loads(to_native(open_url(clientscope_url, method="GET", headers=self.restheaders,
676                                                 validate_certs=self.validate_certs).read()))
677
678        except HTTPError as e:
679            if e.code == 404:
680                return None
681            else:
682                self.module.fail_json(msg="Could not fetch clientscope %s in realm %s: %s"
683                                          % (cid, realm, str(e)))
684        except Exception as e:
685            self.module.fail_json(msg="Could not clientscope group %s in realm %s: %s"
686                                      % (cid, realm, str(e)))
687
688    def get_clientscope_by_name(self, name, realm="master"):
689        """ Fetch a keycloak clientscope within a realm based on its name.
690
691        The Keycloak API does not allow filtering of the clientscopes resource by name.
692        As a result, this method first retrieves the entire list of clientscopes - name and ID -
693        then performs a second query to fetch the group.
694
695        If the clientscope does not exist, None is returned.
696        :param name: Name of the clientscope to fetch.
697        :param realm: Realm in which the clientscope resides; default 'master'
698        """
699        try:
700            all_clientscopes = self.get_clientscopes(realm=realm)
701
702            for clientscope in all_clientscopes:
703                if clientscope['name'] == name:
704                    return self.get_clientscope_by_clientscopeid(clientscope['id'], realm=realm)
705
706            return None
707
708        except Exception as e:
709            self.module.fail_json(msg="Could not fetch clientscope %s in realm %s: %s"
710                                      % (name, realm, str(e)))
711
712    def create_clientscope(self, clientscoperep, realm="master"):
713        """ Create a Keycloak clientscope.
714
715        :param clientscoperep: a ClientScopeRepresentation of the clientscope to be created. Must contain at minimum the field name.
716        :return: HTTPResponse object on success
717        """
718        clientscopes_url = URL_CLIENTSCOPES.format(url=self.baseurl, realm=realm)
719        try:
720            return open_url(clientscopes_url, method='POST', headers=self.restheaders,
721                            data=json.dumps(clientscoperep), validate_certs=self.validate_certs)
722        except Exception as e:
723            self.module.fail_json(msg="Could not create clientscope %s in realm %s: %s"
724                                      % (clientscoperep['name'], realm, str(e)))
725
726    def update_clientscope(self, clientscoperep, realm="master"):
727        """ Update an existing clientscope.
728
729        :param grouprep: A GroupRepresentation of the updated group.
730        :return HTTPResponse object on success
731        """
732        clientscope_url = URL_CLIENTSCOPE.format(url=self.baseurl, realm=realm, id=clientscoperep['id'])
733
734        try:
735            return open_url(clientscope_url, method='PUT', headers=self.restheaders,
736                            data=json.dumps(clientscoperep), validate_certs=self.validate_certs)
737
738        except Exception as e:
739            self.module.fail_json(msg='Could not update clientscope %s in realm %s: %s'
740                                      % (clientscoperep['name'], realm, str(e)))
741
742    def delete_clientscope(self, name=None, cid=None, realm="master"):
743        """ Delete a clientscope. One of name or cid must be provided.
744
745        Providing the clientscope ID is preferred as it avoids a second lookup to
746        convert a clientscope name to an ID.
747
748        :param name: The name of the clientscope. A lookup will be performed to retrieve the clientscope ID.
749        :param cid: The ID of the clientscope (preferred to name).
750        :param realm: The realm in which this group resides, default "master".
751        """
752
753        if cid is None and name is None:
754            # prefer an exception since this is almost certainly a programming error in the module itself.
755            raise Exception("Unable to delete group - one of group ID or name must be provided.")
756
757        # only lookup the name if cid isn't provided.
758        # in the case that both are provided, prefer the ID, since it's one
759        # less lookup.
760        if cid is None and name is not None:
761            for clientscope in self.get_clientscopes(realm=realm):
762                if clientscope['name'] == name:
763                    cid = clientscope['id']
764                    break
765
766        # if the group doesn't exist - no problem, nothing to delete.
767        if cid is None:
768            return None
769
770        # should have a good cid by here.
771        clientscope_url = URL_CLIENTSCOPE.format(realm=realm, id=cid, url=self.baseurl)
772        try:
773            return open_url(clientscope_url, method='DELETE', headers=self.restheaders,
774                            validate_certs=self.validate_certs)
775
776        except Exception as e:
777            self.module.fail_json(msg="Unable to delete clientscope %s: %s" % (cid, str(e)))
778
779    def get_clientscope_protocolmappers(self, cid, realm="master"):
780        """ Fetch the name and ID of all clientscopes on the Keycloak server.
781
782        To fetch the full data of the group, make a subsequent call to
783        get_clientscope_by_clientscopeid, passing in the ID of the group you wish to return.
784
785        :param cid: id of clientscope (not name).
786        :param realm: Realm in which the clientscope resides; default 'master'.
787        :return The protocolmappers of this realm (default "master")
788        """
789        protocolmappers_url = URL_CLIENTSCOPE_PROTOCOLMAPPERS.format(id=cid, url=self.baseurl, realm=realm)
790        try:
791            return json.loads(to_native(open_url(protocolmappers_url, method="GET", headers=self.restheaders,
792                                                 validate_certs=self.validate_certs).read()))
793        except Exception as e:
794            self.module.fail_json(msg="Could not fetch list of protocolmappers in realm %s: %s"
795                                      % (realm, str(e)))
796
797    def get_clientscope_protocolmapper_by_protocolmapperid(self, pid, cid, realm="master"):
798        """ Fetch a keycloak clientscope from the provided realm using the clientscope's unique ID.
799
800        If the clientscope does not exist, None is returned.
801
802        gid is a UUID provided by the Keycloak API
803
804        :param cid: UUID of the protocolmapper to be returned
805        :param cid: UUID of the clientscope to be returned
806        :param realm: Realm in which the clientscope resides; default 'master'.
807        """
808        protocolmapper_url = URL_CLIENTSCOPE_PROTOCOLMAPPER.format(url=self.baseurl, realm=realm, id=cid, mapper_id=pid)
809        try:
810            return json.loads(to_native(open_url(protocolmapper_url, method="GET", headers=self.restheaders,
811                                                 validate_certs=self.validate_certs).read()))
812
813        except HTTPError as e:
814            if e.code == 404:
815                return None
816            else:
817                self.module.fail_json(msg="Could not fetch protocolmapper %s in realm %s: %s"
818                                          % (pid, realm, str(e)))
819        except Exception as e:
820            self.module.fail_json(msg="Could not fetch protocolmapper %s in realm %s: %s"
821                                      % (cid, realm, str(e)))
822
823    def get_clientscope_protocolmapper_by_name(self, cid, name, realm="master"):
824        """ Fetch a keycloak clientscope within a realm based on its name.
825
826        The Keycloak API does not allow filtering of the clientscopes resource by name.
827        As a result, this method first retrieves the entire list of clientscopes - name and ID -
828        then performs a second query to fetch the group.
829
830        If the clientscope does not exist, None is returned.
831        :param cid: Id of the clientscope (not name).
832        :param name: Name of the protocolmapper to fetch.
833        :param realm: Realm in which the clientscope resides; default 'master'
834        """
835        try:
836            all_protocolmappers = self.get_clientscope_protocolmappers(cid, realm=realm)
837
838            for protocolmapper in all_protocolmappers:
839                if protocolmapper['name'] == name:
840                    return self.get_clientscope_protocolmapper_by_protocolmapperid(protocolmapper['id'], cid, realm=realm)
841
842            return None
843
844        except Exception as e:
845            self.module.fail_json(msg="Could not fetch protocolmapper %s in realm %s: %s"
846                                      % (name, realm, str(e)))
847
848    def create_clientscope_protocolmapper(self, cid, mapper_rep, realm="master"):
849        """ Create a Keycloak clientscope protocolmapper.
850
851        :param cid: Id of the clientscope.
852        :param mapper_rep: a ProtocolMapperRepresentation of the protocolmapper to be created. Must contain at minimum the field name.
853        :return: HTTPResponse object on success
854        """
855        protocolmappers_url = URL_CLIENTSCOPE_PROTOCOLMAPPERS.format(url=self.baseurl, id=cid, realm=realm)
856        try:
857            return open_url(protocolmappers_url, method='POST', headers=self.restheaders,
858                            data=json.dumps(mapper_rep), validate_certs=self.validate_certs)
859        except Exception as e:
860            self.module.fail_json(msg="Could not create protocolmapper %s in realm %s: %s"
861                                      % (mapper_rep['name'], realm, str(e)))
862
863    def update_clientscope_protocolmappers(self, cid, mapper_rep, realm="master"):
864        """ Update an existing clientscope.
865
866        :param cid: Id of the clientscope.
867        :param mapper_rep: A ProtocolMapperRepresentation of the updated protocolmapper.
868        :return HTTPResponse object on success
869        """
870        protocolmapper_url = URL_CLIENTSCOPE_PROTOCOLMAPPER.format(url=self.baseurl, realm=realm, id=cid, mapper_id=mapper_rep['id'])
871
872        try:
873            return open_url(protocolmapper_url, method='PUT', headers=self.restheaders,
874                            data=json.dumps(mapper_rep), validate_certs=self.validate_certs)
875
876        except Exception as e:
877            self.module.fail_json(msg='Could not update protocolmappers for clientscope %s in realm %s: %s'
878                                      % (mapper_rep, realm, str(e)))
879
880    def get_groups(self, realm="master"):
881        """ Fetch the name and ID of all groups on the Keycloak server.
882
883        To fetch the full data of the group, make a subsequent call to
884        get_group_by_groupid, passing in the ID of the group you wish to return.
885
886        :param realm: Return the groups of this realm (default "master").
887        """
888        groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm)
889        try:
890            return json.loads(to_native(open_url(groups_url, method="GET", headers=self.restheaders,
891                                                 validate_certs=self.validate_certs).read()))
892        except Exception as e:
893            self.module.fail_json(msg="Could not fetch list of groups in realm %s: %s"
894                                      % (realm, str(e)))
895
896    def get_group_by_groupid(self, gid, realm="master"):
897        """ Fetch a keycloak group from the provided realm using the group's unique ID.
898
899        If the group does not exist, None is returned.
900
901        gid is a UUID provided by the Keycloak API
902        :param gid: UUID of the group to be returned
903        :param realm: Realm in which the group resides; default 'master'.
904        """
905        groups_url = URL_GROUP.format(url=self.baseurl, realm=realm, groupid=gid)
906        try:
907            return json.loads(to_native(open_url(groups_url, method="GET", headers=self.restheaders,
908                                                 validate_certs=self.validate_certs).read()))
909
910        except HTTPError as e:
911            if e.code == 404:
912                return None
913            else:
914                self.module.fail_json(msg="Could not fetch group %s in realm %s: %s"
915                                          % (gid, realm, str(e)))
916        except Exception as e:
917            self.module.fail_json(msg="Could not fetch group %s in realm %s: %s"
918                                      % (gid, realm, str(e)))
919
920    def get_group_by_name(self, name, realm="master"):
921        """ Fetch a keycloak group within a realm based on its name.
922
923        The Keycloak API does not allow filtering of the Groups resource by name.
924        As a result, this method first retrieves the entire list of groups - name and ID -
925        then performs a second query to fetch the group.
926
927        If the group does not exist, None is returned.
928        :param name: Name of the group to fetch.
929        :param realm: Realm in which the group resides; default 'master'
930        """
931        groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm)
932        try:
933            all_groups = self.get_groups(realm=realm)
934
935            for group in all_groups:
936                if group['name'] == name:
937                    return self.get_group_by_groupid(group['id'], realm=realm)
938
939            return None
940
941        except Exception as e:
942            self.module.fail_json(msg="Could not fetch group %s in realm %s: %s"
943                                      % (name, realm, str(e)))
944
945    def create_group(self, grouprep, realm="master"):
946        """ Create a Keycloak group.
947
948        :param grouprep: a GroupRepresentation of the group to be created. Must contain at minimum the field name.
949        :return: HTTPResponse object on success
950        """
951        groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm)
952        try:
953            return open_url(groups_url, method='POST', headers=self.restheaders,
954                            data=json.dumps(grouprep), validate_certs=self.validate_certs)
955        except Exception as e:
956            self.module.fail_json(msg="Could not create group %s in realm %s: %s"
957                                      % (grouprep['name'], realm, str(e)))
958
959    def update_group(self, grouprep, realm="master"):
960        """ Update an existing group.
961
962        :param grouprep: A GroupRepresentation of the updated group.
963        :return HTTPResponse object on success
964        """
965        group_url = URL_GROUP.format(url=self.baseurl, realm=realm, groupid=grouprep['id'])
966
967        try:
968            return open_url(group_url, method='PUT', headers=self.restheaders,
969                            data=json.dumps(grouprep), validate_certs=self.validate_certs)
970        except Exception as e:
971            self.module.fail_json(msg='Could not update group %s in realm %s: %s'
972                                      % (grouprep['name'], realm, str(e)))
973
974    def delete_group(self, name=None, groupid=None, realm="master"):
975        """ Delete a group. One of name or groupid must be provided.
976
977        Providing the group ID is preferred as it avoids a second lookup to
978        convert a group name to an ID.
979
980        :param name: The name of the group. A lookup will be performed to retrieve the group ID.
981        :param groupid: The ID of the group (preferred to name).
982        :param realm: The realm in which this group resides, default "master".
983        """
984
985        if groupid is None and name is None:
986            # prefer an exception since this is almost certainly a programming error in the module itself.
987            raise Exception("Unable to delete group - one of group ID or name must be provided.")
988
989        # only lookup the name if groupid isn't provided.
990        # in the case that both are provided, prefer the ID, since it's one
991        # less lookup.
992        if groupid is None and name is not None:
993            for group in self.get_groups(realm=realm):
994                if group['name'] == name:
995                    groupid = group['id']
996                    break
997
998        # if the group doesn't exist - no problem, nothing to delete.
999        if groupid is None:
1000            return None
1001
1002        # should have a good groupid by here.
1003        group_url = URL_GROUP.format(realm=realm, groupid=groupid, url=self.baseurl)
1004        try:
1005            return open_url(group_url, method='DELETE', headers=self.restheaders,
1006                            validate_certs=self.validate_certs)
1007        except Exception as e:
1008            self.module.fail_json(msg="Unable to delete group %s: %s" % (groupid, str(e)))
1009
1010    def get_realm_roles(self, realm='master'):
1011        """ Obtains role representations for roles in a realm
1012
1013        :param realm: realm to be queried
1014        :return: list of dicts of role representations
1015        """
1016        rolelist_url = URL_REALM_ROLES.format(url=self.baseurl, realm=realm)
1017        try:
1018            return json.loads(to_native(open_url(rolelist_url, method='GET', headers=self.restheaders,
1019                                                 validate_certs=self.validate_certs).read()))
1020        except ValueError as e:
1021            self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of roles for realm %s: %s'
1022                                      % (realm, str(e)))
1023        except Exception as e:
1024            self.module.fail_json(msg='Could not obtain list of roles for realm %s: %s'
1025                                      % (realm, str(e)))
1026
1027    def get_realm_role(self, name, realm='master'):
1028        """ Fetch a keycloak role from the provided realm using the role's name.
1029
1030        If the role does not exist, None is returned.
1031        :param name: Name of the role to fetch.
1032        :param realm: Realm in which the role resides; default 'master'.
1033        """
1034        role_url = URL_REALM_ROLE.format(url=self.baseurl, realm=realm, name=quote(name))
1035        try:
1036            return json.loads(to_native(open_url(role_url, method="GET", headers=self.restheaders,
1037                                                 validate_certs=self.validate_certs).read()))
1038        except HTTPError as e:
1039            if e.code == 404:
1040                return None
1041            else:
1042                self.module.fail_json(msg='Could not fetch role %s in realm %s: %s'
1043                                          % (name, realm, str(e)))
1044        except Exception as e:
1045            self.module.fail_json(msg='Could not fetch role %s in realm %s: %s'
1046                                      % (name, realm, str(e)))
1047
1048    def create_realm_role(self, rolerep, realm='master'):
1049        """ Create a Keycloak realm role.
1050
1051        :param rolerep: a RoleRepresentation of the role to be created. Must contain at minimum the field name.
1052        :return: HTTPResponse object on success
1053        """
1054        roles_url = URL_REALM_ROLES.format(url=self.baseurl, realm=realm)
1055        try:
1056            return open_url(roles_url, method='POST', headers=self.restheaders,
1057                            data=json.dumps(rolerep), validate_certs=self.validate_certs)
1058        except Exception as e:
1059            self.module.fail_json(msg='Could not create role %s in realm %s: %s'
1060                                      % (rolerep['name'], realm, str(e)))
1061
1062    def update_realm_role(self, rolerep, realm='master'):
1063        """ Update an existing realm role.
1064
1065        :param rolerep: A RoleRepresentation of the updated role.
1066        :return HTTPResponse object on success
1067        """
1068        role_url = URL_REALM_ROLE.format(url=self.baseurl, realm=realm, name=quote(rolerep['name']))
1069        try:
1070            return open_url(role_url, method='PUT', headers=self.restheaders,
1071                            data=json.dumps(rolerep), validate_certs=self.validate_certs)
1072        except Exception as e:
1073            self.module.fail_json(msg='Could not update role %s in realm %s: %s'
1074                                      % (rolerep['name'], realm, str(e)))
1075
1076    def delete_realm_role(self, name, realm='master'):
1077        """ Delete a realm role.
1078
1079        :param name: The name of the role.
1080        :param realm: The realm in which this role resides, default "master".
1081        """
1082        role_url = URL_REALM_ROLE.format(url=self.baseurl, realm=realm, name=quote(name))
1083        try:
1084            return open_url(role_url, method='DELETE', headers=self.restheaders,
1085                            validate_certs=self.validate_certs)
1086        except Exception as e:
1087            self.module.fail_json(msg='Unable to delete role %s in realm %s: %s'
1088                                      % (name, realm, str(e)))
1089
1090    def get_client_roles(self, clientid, realm='master'):
1091        """ Obtains role representations for client roles in a specific client
1092
1093        :param clientid: Client id to be queried
1094        :param realm: Realm to be queried
1095        :return: List of dicts of role representations
1096        """
1097        cid = self.get_client_id(clientid, realm=realm)
1098        if cid is None:
1099            self.module.fail_json(msg='Could not find client %s in realm %s'
1100                                      % (clientid, realm))
1101        rolelist_url = URL_CLIENT_ROLES.format(url=self.baseurl, realm=realm, id=cid)
1102        try:
1103            return json.loads(to_native(open_url(rolelist_url, method='GET', headers=self.restheaders,
1104                                                 validate_certs=self.validate_certs).read()))
1105        except ValueError as e:
1106            self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of roles for client %s in realm %s: %s'
1107                                      % (clientid, realm, str(e)))
1108        except Exception as e:
1109            self.module.fail_json(msg='Could not obtain list of roles for client %s in realm %s: %s'
1110                                      % (clientid, realm, str(e)))
1111
1112    def get_client_role(self, name, clientid, realm='master'):
1113        """ Fetch a keycloak client role from the provided realm using the role's name.
1114
1115        :param name: Name of the role to fetch.
1116        :param clientid: Client id for the client role
1117        :param realm: Realm in which the role resides
1118        :return: Dict of role representation
1119        If the role does not exist, None is returned.
1120        """
1121        cid = self.get_client_id(clientid, realm=realm)
1122        if cid is None:
1123            self.module.fail_json(msg='Could not find client %s in realm %s'
1124                                      % (clientid, realm))
1125        role_url = URL_CLIENT_ROLE.format(url=self.baseurl, realm=realm, id=cid, name=quote(name))
1126        try:
1127            return json.loads(to_native(open_url(role_url, method="GET", headers=self.restheaders,
1128                                                 validate_certs=self.validate_certs).read()))
1129        except HTTPError as e:
1130            if e.code == 404:
1131                return None
1132            else:
1133                self.module.fail_json(msg='Could not fetch role %s in client %s of realm %s: %s'
1134                                          % (name, clientid, realm, str(e)))
1135        except Exception as e:
1136            self.module.fail_json(msg='Could not fetch role %s for client %s in realm %s: %s'
1137                                      % (name, clientid, realm, str(e)))
1138
1139    def create_client_role(self, rolerep, clientid, realm='master'):
1140        """ Create a Keycloak client role.
1141
1142        :param rolerep: a RoleRepresentation of the role to be created. Must contain at minimum the field name.
1143        :param clientid: Client id for the client role
1144        :param realm: Realm in which the role resides
1145        :return: HTTPResponse object on success
1146        """
1147        cid = self.get_client_id(clientid, realm=realm)
1148        if cid is None:
1149            self.module.fail_json(msg='Could not find client %s in realm %s'
1150                                      % (clientid, realm))
1151        roles_url = URL_CLIENT_ROLES.format(url=self.baseurl, realm=realm, id=cid)
1152        try:
1153            return open_url(roles_url, method='POST', headers=self.restheaders,
1154                            data=json.dumps(rolerep), validate_certs=self.validate_certs)
1155        except Exception as e:
1156            self.module.fail_json(msg='Could not create role %s for client %s in realm %s: %s'
1157                                      % (rolerep['name'], clientid, realm, str(e)))
1158
1159    def update_client_role(self, rolerep, clientid, realm="master"):
1160        """ Update an existing client role.
1161
1162        :param rolerep: A RoleRepresentation of the updated role.
1163        :param clientid: Client id for the client role
1164        :param realm: Realm in which the role resides
1165        :return HTTPResponse object on success
1166        """
1167        cid = self.get_client_id(clientid, realm=realm)
1168        if cid is None:
1169            self.module.fail_json(msg='Could not find client %s in realm %s'
1170                                      % (clientid, realm))
1171        role_url = URL_CLIENT_ROLE.format(url=self.baseurl, realm=realm, id=cid, name=quote(rolerep['name']))
1172        try:
1173            return open_url(role_url, method='PUT', headers=self.restheaders,
1174                            data=json.dumps(rolerep), validate_certs=self.validate_certs)
1175        except Exception as e:
1176            self.module.fail_json(msg='Could not update role %s for client %s in realm %s: %s'
1177                                      % (rolerep['name'], clientid, realm, str(e)))
1178
1179    def delete_client_role(self, name, clientid, realm="master"):
1180        """ Delete a role. One of name or roleid must be provided.
1181
1182        :param name: The name of the role.
1183        :param clientid: Client id for the client role
1184        :param realm: Realm in which the role resides
1185        """
1186        cid = self.get_client_id(clientid, realm=realm)
1187        if cid is None:
1188            self.module.fail_json(msg='Could not find client %s in realm %s'
1189                                      % (clientid, realm))
1190        role_url = URL_CLIENT_ROLE.format(url=self.baseurl, realm=realm, id=cid, name=quote(name))
1191        try:
1192            return open_url(role_url, method='DELETE', headers=self.restheaders,
1193                            validate_certs=self.validate_certs)
1194        except Exception as e:
1195            self.module.fail_json(msg='Unable to delete role %s for client %s in realm %s: %s'
1196                                      % (name, clientid, realm, str(e)))
1197
1198    def get_authentication_flow_by_alias(self, alias, realm='master'):
1199        """
1200        Get an authentication flow by it's alias
1201        :param alias: Alias of the authentication flow to get.
1202        :param realm: Realm.
1203        :return: Authentication flow representation.
1204        """
1205        try:
1206            authentication_flow = {}
1207            # Check if the authentication flow exists on the Keycloak serveraders
1208            authentications = json.load(open_url(URL_AUTHENTICATION_FLOWS.format(url=self.baseurl, realm=realm), method='GET', headers=self.restheaders))
1209            for authentication in authentications:
1210                if authentication["alias"] == alias:
1211                    authentication_flow = authentication
1212                    break
1213            return authentication_flow
1214        except Exception as e:
1215            self.module.fail_json(msg="Unable get authentication flow %s: %s" % (alias, str(e)))
1216
1217    def delete_authentication_flow_by_id(self, id, realm='master'):
1218        """
1219        Delete an authentication flow from Keycloak
1220        :param id: id of authentication flow to be deleted
1221        :param realm: realm of client to be deleted
1222        :return: HTTPResponse object on success
1223        """
1224        flow_url = URL_AUTHENTICATION_FLOW.format(url=self.baseurl, realm=realm, id=id)
1225
1226        try:
1227            return open_url(flow_url, method='DELETE', headers=self.restheaders,
1228                            validate_certs=self.validate_certs)
1229        except Exception as e:
1230            self.module.fail_json(msg='Could not delete authentication flow %s in realm %s: %s'
1231                                  % (id, realm, str(e)))
1232
1233    def copy_auth_flow(self, config, realm='master'):
1234        """
1235        Create a new authentication flow from a copy of another.
1236        :param config: Representation of the authentication flow to create.
1237        :param realm: Realm.
1238        :return: Representation of the new authentication flow.
1239        """
1240        try:
1241            new_name = dict(
1242                newName=config["alias"]
1243            )
1244            open_url(
1245                URL_AUTHENTICATION_FLOW_COPY.format(
1246                    url=self.baseurl,
1247                    realm=realm,
1248                    copyfrom=quote(config["copyFrom"])),
1249                method='POST',
1250                headers=self.restheaders,
1251                data=json.dumps(new_name))
1252            flow_list = json.load(
1253                open_url(
1254                    URL_AUTHENTICATION_FLOWS.format(url=self.baseurl,
1255                                                    realm=realm),
1256                    method='GET',
1257                    headers=self.restheaders))
1258            for flow in flow_list:
1259                if flow["alias"] == config["alias"]:
1260                    return flow
1261            return None
1262        except Exception as e:
1263            self.module.fail_json(msg='Could not copy authentication flow %s in realm %s: %s'
1264                                  % (config["alias"], realm, str(e)))
1265
1266    def create_empty_auth_flow(self, config, realm='master'):
1267        """
1268        Create a new empty authentication flow.
1269        :param config: Representation of the authentication flow to create.
1270        :param realm: Realm.
1271        :return: Representation of the new authentication flow.
1272        """
1273        try:
1274            new_flow = dict(
1275                alias=config["alias"],
1276                providerId=config["providerId"],
1277                description=config["description"],
1278                topLevel=True
1279            )
1280            open_url(
1281                URL_AUTHENTICATION_FLOWS.format(
1282                    url=self.baseurl,
1283                    realm=realm),
1284                method='POST',
1285                headers=self.restheaders,
1286                data=json.dumps(new_flow))
1287            flow_list = json.load(
1288                open_url(
1289                    URL_AUTHENTICATION_FLOWS.format(
1290                        url=self.baseurl,
1291                        realm=realm),
1292                    method='GET',
1293                    headers=self.restheaders))
1294            for flow in flow_list:
1295                if flow["alias"] == config["alias"]:
1296                    return flow
1297            return None
1298        except Exception as e:
1299            self.module.fail_json(msg='Could not create empty authentication flow %s in realm %s: %s'
1300                                  % (config["alias"], realm, str(e)))
1301
1302    def update_authentication_executions(self, flowAlias, updatedExec, realm='master'):
1303        """ Update authentication executions
1304
1305        :param flowAlias: name of the parent flow
1306        :param updatedExec: JSON containing updated execution
1307        :return: HTTPResponse object on success
1308        """
1309        try:
1310            open_url(
1311                URL_AUTHENTICATION_FLOW_EXECUTIONS.format(
1312                    url=self.baseurl,
1313                    realm=realm,
1314                    flowalias=quote(flowAlias)),
1315                method='PUT',
1316                headers=self.restheaders,
1317                data=json.dumps(updatedExec))
1318        except Exception as e:
1319            self.module.fail_json(msg="Unable to update executions %s: %s" % (updatedExec, str(e)))
1320
1321    def add_authenticationConfig_to_execution(self, executionId, authenticationConfig, realm='master'):
1322        """ Add autenticatorConfig to the execution
1323
1324        :param executionId: id of execution
1325        :param authenticationConfig: config to add to the execution
1326        :return: HTTPResponse object on success
1327        """
1328        try:
1329            open_url(
1330                URL_AUTHENTICATION_EXECUTION_CONFIG.format(
1331                    url=self.baseurl,
1332                    realm=realm,
1333                    id=executionId),
1334                method='POST',
1335                headers=self.restheaders,
1336                data=json.dumps(authenticationConfig))
1337        except Exception as e:
1338            self.module.fail_json(msg="Unable to add authenticationConfig %s: %s" % (executionId, str(e)))
1339
1340    def create_subflow(self, subflowName, flowAlias, realm='master'):
1341        """ Create new sublow on the flow
1342
1343        :param subflowName: name of the subflow to create
1344        :param flowAlias: name of the parent flow
1345        :return: HTTPResponse object on success
1346        """
1347        try:
1348            newSubFlow = {}
1349            newSubFlow["alias"] = subflowName
1350            newSubFlow["provider"] = "registration-page-form"
1351            newSubFlow["type"] = "basic-flow"
1352            open_url(
1353                URL_AUTHENTICATION_FLOW_EXECUTIONS_FLOW.format(
1354                    url=self.baseurl,
1355                    realm=realm,
1356                    flowalias=quote(flowAlias)),
1357                method='POST',
1358                headers=self.restheaders,
1359                data=json.dumps(newSubFlow))
1360        except Exception as e:
1361            self.module.fail_json(msg="Unable to create new subflow %s: %s" % (subflowName, str(e)))
1362
1363    def create_execution(self, execution, flowAlias, realm='master'):
1364        """ Create new execution on the flow
1365
1366        :param execution: name of execution to create
1367        :param flowAlias: name of the parent flow
1368        :return: HTTPResponse object on success
1369        """
1370        try:
1371            newExec = {}
1372            newExec["provider"] = execution["providerId"]
1373            newExec["requirement"] = execution["requirement"]
1374            open_url(
1375                URL_AUTHENTICATION_FLOW_EXECUTIONS_EXECUTION.format(
1376                    url=self.baseurl,
1377                    realm=realm,
1378                    flowalias=quote(flowAlias)),
1379                method='POST',
1380                headers=self.restheaders,
1381                data=json.dumps(newExec))
1382        except Exception as e:
1383            self.module.fail_json(msg="Unable to create new execution %s: %s" % (execution["provider"], str(e)))
1384
1385    def change_execution_priority(self, executionId, diff, realm='master'):
1386        """ Raise or lower execution priority of diff time
1387
1388        :param executionId: id of execution to lower priority
1389        :param realm: realm the client is in
1390        :param diff: Integer number, raise of diff time if positive lower of diff time if negative
1391        :return: HTTPResponse object on success
1392        """
1393        try:
1394            if diff > 0:
1395                for i in range(diff):
1396                    open_url(
1397                        URL_AUTHENTICATION_EXECUTION_RAISE_PRIORITY.format(
1398                            url=self.baseurl,
1399                            realm=realm,
1400                            id=executionId),
1401                        method='POST',
1402                        headers=self.restheaders)
1403            elif diff < 0:
1404                for i in range(-diff):
1405                    open_url(
1406                        URL_AUTHENTICATION_EXECUTION_LOWER_PRIORITY.format(
1407                            url=self.baseurl,
1408                            realm=realm,
1409                            id=executionId),
1410                        method='POST',
1411                        headers=self.restheaders)
1412        except Exception as e:
1413            self.module.fail_json(msg="Unable to change execution priority %s: %s" % (executionId, str(e)))
1414
1415    def get_executions_representation(self, config, realm='master'):
1416        """
1417        Get a representation of the executions for an authentication flow.
1418        :param config: Representation of the authentication flow
1419        :param realm: Realm
1420        :return: Representation of the executions
1421        """
1422        try:
1423            # Get executions created
1424            executions = json.load(
1425                open_url(
1426                    URL_AUTHENTICATION_FLOW_EXECUTIONS.format(
1427                        url=self.baseurl,
1428                        realm=realm,
1429                        flowalias=quote(config["alias"])),
1430                    method='GET',
1431                    headers=self.restheaders))
1432            for execution in executions:
1433                if "authenticationConfig" in execution:
1434                    execConfigId = execution["authenticationConfig"]
1435                    execConfig = json.load(
1436                        open_url(
1437                            URL_AUTHENTICATION_CONFIG.format(
1438                                url=self.baseurl,
1439                                realm=realm,
1440                                id=execConfigId),
1441                            method='GET',
1442                            headers=self.restheaders))
1443                    execution["authenticationConfig"] = execConfig
1444            return executions
1445        except Exception as e:
1446            self.module.fail_json(msg='Could not get executions for authentication flow %s in realm %s: %s'
1447                                  % (config["alias"], realm, str(e)))
1448
1449    def get_identity_providers(self, realm='master'):
1450        """ Fetch representations for identity providers in a realm
1451        :param realm: realm to be queried
1452        :return: list of representations for identity providers
1453        """
1454        idps_url = URL_IDENTITY_PROVIDERS.format(url=self.baseurl, realm=realm)
1455        try:
1456            return json.loads(to_native(open_url(idps_url, method='GET', headers=self.restheaders,
1457                                                 validate_certs=self.validate_certs).read()))
1458        except ValueError as e:
1459            self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of identity providers for realm %s: %s'
1460                                      % (realm, str(e)))
1461        except Exception as e:
1462            self.module.fail_json(msg='Could not obtain list of identity providers for realm %s: %s'
1463                                      % (realm, str(e)))
1464
1465    def get_identity_provider(self, alias, realm='master'):
1466        """ Fetch identity provider representation from a realm using the idp's alias.
1467        If the identity provider does not exist, None is returned.
1468        :param alias: Alias of the identity provider to fetch.
1469        :param realm: Realm in which the identity provider resides; default 'master'.
1470        """
1471        idp_url = URL_IDENTITY_PROVIDER.format(url=self.baseurl, realm=realm, alias=alias)
1472        try:
1473            return json.loads(to_native(open_url(idp_url, method="GET", headers=self.restheaders,
1474                                                 validate_certs=self.validate_certs).read()))
1475        except HTTPError as e:
1476            if e.code == 404:
1477                return None
1478            else:
1479                self.module.fail_json(msg='Could not fetch identity provider %s in realm %s: %s'
1480                                          % (alias, realm, str(e)))
1481        except Exception as e:
1482            self.module.fail_json(msg='Could not fetch identity provider %s in realm %s: %s'
1483                                      % (alias, realm, str(e)))
1484
1485    def create_identity_provider(self, idprep, realm='master'):
1486        """ Create an identity provider.
1487        :param idprep: Identity provider representation of the idp to be created.
1488        :param realm: Realm in which this identity provider resides, default "master".
1489        :return: HTTPResponse object on success
1490        """
1491        idps_url = URL_IDENTITY_PROVIDERS.format(url=self.baseurl, realm=realm)
1492        try:
1493            return open_url(idps_url, method='POST', headers=self.restheaders,
1494                            data=json.dumps(idprep), validate_certs=self.validate_certs)
1495        except Exception as e:
1496            self.module.fail_json(msg='Could not create identity provider %s in realm %s: %s'
1497                                      % (idprep['alias'], realm, str(e)))
1498
1499    def update_identity_provider(self, idprep, realm='master'):
1500        """ Update an existing identity provider.
1501        :param idprep: Identity provider representation of the idp to be updated.
1502        :param realm: Realm in which this identity provider resides, default "master".
1503        :return HTTPResponse object on success
1504        """
1505        idp_url = URL_IDENTITY_PROVIDER.format(url=self.baseurl, realm=realm, alias=idprep['alias'])
1506        try:
1507            return open_url(idp_url, method='PUT', headers=self.restheaders,
1508                            data=json.dumps(idprep), validate_certs=self.validate_certs)
1509        except Exception as e:
1510            self.module.fail_json(msg='Could not update identity provider %s in realm %s: %s'
1511                                      % (idprep['alias'], realm, str(e)))
1512
1513    def delete_identity_provider(self, alias, realm='master'):
1514        """ Delete an identity provider.
1515        :param alias: Alias of the identity provider.
1516        :param realm: Realm in which this identity provider resides, default "master".
1517        """
1518        idp_url = URL_IDENTITY_PROVIDER.format(url=self.baseurl, realm=realm, alias=alias)
1519        try:
1520            return open_url(idp_url, method='DELETE', headers=self.restheaders,
1521                            validate_certs=self.validate_certs)
1522        except Exception as e:
1523            self.module.fail_json(msg='Unable to delete identity provider %s in realm %s: %s'
1524                                      % (alias, realm, str(e)))
1525
1526    def get_identity_provider_mappers(self, alias, realm='master'):
1527        """ Fetch representations for identity provider mappers
1528        :param alias: Alias of the identity provider.
1529        :param realm: realm to be queried
1530        :return: list of representations for identity provider mappers
1531        """
1532        mappers_url = URL_IDENTITY_PROVIDER_MAPPERS.format(url=self.baseurl, realm=realm, alias=alias)
1533        try:
1534            return json.loads(to_native(open_url(mappers_url, method='GET', headers=self.restheaders,
1535                                                 validate_certs=self.validate_certs).read()))
1536        except ValueError as e:
1537            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'
1538                                      % (alias, realm, str(e)))
1539        except Exception as e:
1540            self.module.fail_json(msg='Could not obtain list of identity provider mappers for idp %s in realm %s: %s'
1541                                      % (alias, realm, str(e)))
1542
1543    def get_identity_provider_mapper(self, mid, alias, realm='master'):
1544        """ Fetch identity provider representation from a realm using the idp's alias.
1545        If the identity provider does not exist, None is returned.
1546        :param mid: Unique ID of the mapper to fetch.
1547        :param alias: Alias of the identity provider.
1548        :param realm: Realm in which the identity provider resides; default 'master'.
1549        """
1550        mapper_url = URL_IDENTITY_PROVIDER_MAPPER.format(url=self.baseurl, realm=realm, alias=alias, id=mid)
1551        try:
1552            return json.loads(to_native(open_url(mapper_url, method="GET", headers=self.restheaders,
1553                                                 validate_certs=self.validate_certs).read()))
1554        except HTTPError as e:
1555            if e.code == 404:
1556                return None
1557            else:
1558                self.module.fail_json(msg='Could not fetch mapper %s for identity provider %s in realm %s: %s'
1559                                          % (mid, alias, realm, str(e)))
1560        except Exception as e:
1561            self.module.fail_json(msg='Could not fetch mapper %s for identity provider %s in realm %s: %s'
1562                                      % (mid, alias, realm, str(e)))
1563
1564    def create_identity_provider_mapper(self, mapper, alias, realm='master'):
1565        """ Create an identity provider mapper.
1566        :param mapper: IdentityProviderMapperRepresentation of the mapper to be created.
1567        :param alias: Alias of the identity provider.
1568        :param realm: Realm in which this identity provider resides, default "master".
1569        :return: HTTPResponse object on success
1570        """
1571        mappers_url = URL_IDENTITY_PROVIDER_MAPPERS.format(url=self.baseurl, realm=realm, alias=alias)
1572        try:
1573            return open_url(mappers_url, method='POST', headers=self.restheaders,
1574                            data=json.dumps(mapper), validate_certs=self.validate_certs)
1575        except Exception as e:
1576            self.module.fail_json(msg='Could not create identity provider mapper %s for idp %s in realm %s: %s'
1577                                      % (mapper['name'], alias, realm, str(e)))
1578
1579    def update_identity_provider_mapper(self, mapper, alias, realm='master'):
1580        """ Update an existing identity provider.
1581        :param mapper: IdentityProviderMapperRepresentation of the mapper to be updated.
1582        :param alias: Alias of the identity provider.
1583        :param realm: Realm in which this identity provider resides, default "master".
1584        :return HTTPResponse object on success
1585        """
1586        mapper_url = URL_IDENTITY_PROVIDER_MAPPER.format(url=self.baseurl, realm=realm, alias=alias, id=mapper['id'])
1587        try:
1588            return open_url(mapper_url, method='PUT', headers=self.restheaders,
1589                            data=json.dumps(mapper), validate_certs=self.validate_certs)
1590        except Exception as e:
1591            self.module.fail_json(msg='Could not update mapper %s for identity provider %s in realm %s: %s'
1592                                      % (mapper['id'], alias, realm, str(e)))
1593
1594    def delete_identity_provider_mapper(self, mid, alias, realm='master'):
1595        """ Delete an identity provider.
1596        :param mid: Unique ID of the mapper to delete.
1597        :param alias: Alias of the identity provider.
1598        :param realm: Realm in which this identity provider resides, default "master".
1599        """
1600        mapper_url = URL_IDENTITY_PROVIDER_MAPPER.format(url=self.baseurl, realm=realm, alias=alias, id=mid)
1601        try:
1602            return open_url(mapper_url, method='DELETE', headers=self.restheaders,
1603                            validate_certs=self.validate_certs)
1604        except Exception as e:
1605            self.module.fail_json(msg='Unable to delete mapper %s for identity provider %s in realm %s: %s'
1606                                      % (mid, alias, realm, str(e)))
1607
1608    def get_components(self, filter=None, realm='master'):
1609        """ Fetch representations for components in a realm
1610        :param realm: realm to be queried
1611        :param filter: search filter
1612        :return: list of representations for components
1613        """
1614        comps_url = URL_COMPONENTS.format(url=self.baseurl, realm=realm)
1615        if filter is not None:
1616            comps_url += '?%s' % filter
1617
1618        try:
1619            return json.loads(to_native(open_url(comps_url, method='GET', headers=self.restheaders,
1620                                                 validate_certs=self.validate_certs).read()))
1621        except ValueError as e:
1622            self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of components for realm %s: %s'
1623                                      % (realm, str(e)))
1624        except Exception as e:
1625            self.module.fail_json(msg='Could not obtain list of components for realm %s: %s'
1626                                      % (realm, str(e)))
1627
1628    def get_component(self, cid, realm='master'):
1629        """ Fetch component representation from a realm using its cid.
1630        If the component does not exist, None is returned.
1631        :param cid: Unique ID of the component to fetch.
1632        :param realm: Realm in which the component resides; default 'master'.
1633        """
1634        comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid)
1635        try:
1636            return json.loads(to_native(open_url(comp_url, method="GET", headers=self.restheaders,
1637                                                 validate_certs=self.validate_certs).read()))
1638        except HTTPError as e:
1639            if e.code == 404:
1640                return None
1641            else:
1642                self.module.fail_json(msg='Could not fetch component %s in realm %s: %s'
1643                                          % (cid, realm, str(e)))
1644        except Exception as e:
1645            self.module.fail_json(msg='Could not fetch component %s in realm %s: %s'
1646                                      % (cid, realm, str(e)))
1647
1648    def create_component(self, comprep, realm='master'):
1649        """ Create an component.
1650        :param comprep: Component representation of the component to be created.
1651        :param realm: Realm in which this component resides, default "master".
1652        :return: Component representation of the created component
1653        """
1654        comps_url = URL_COMPONENTS.format(url=self.baseurl, realm=realm)
1655        try:
1656            resp = open_url(comps_url, method='POST', headers=self.restheaders,
1657                            data=json.dumps(comprep), validate_certs=self.validate_certs)
1658            comp_url = resp.getheader('Location')
1659            if comp_url is None:
1660                self.module.fail_json(msg='Could not create component in realm %s: %s'
1661                                          % (realm, 'unexpected response'))
1662            return json.loads(to_native(open_url(comp_url, method="GET", headers=self.restheaders,
1663                                                 validate_certs=self.validate_certs).read()))
1664        except Exception as e:
1665            self.module.fail_json(msg='Could not create component in realm %s: %s'
1666                                      % (realm, str(e)))
1667
1668    def update_component(self, comprep, realm='master'):
1669        """ Update an existing component.
1670        :param comprep: Component representation of the component to be updated.
1671        :param realm: Realm in which this component resides, default "master".
1672        :return HTTPResponse object on success
1673        """
1674        cid = comprep.get('id')
1675        if cid is None:
1676            self.module.fail_json(msg='Cannot update component without id')
1677        comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid)
1678        try:
1679            return open_url(comp_url, method='PUT', headers=self.restheaders,
1680                            data=json.dumps(comprep), validate_certs=self.validate_certs)
1681        except Exception as e:
1682            self.module.fail_json(msg='Could not update component %s in realm %s: %s'
1683                                      % (cid, realm, str(e)))
1684
1685    def delete_component(self, cid, realm='master'):
1686        """ Delete an component.
1687        :param cid: Unique ID of the component.
1688        :param realm: Realm in which this component resides, default "master".
1689        """
1690        comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid)
1691        try:
1692            return open_url(comp_url, method='DELETE', headers=self.restheaders,
1693                            validate_certs=self.validate_certs)
1694        except Exception as e:
1695            self.module.fail_json(msg='Unable to delete component %s in realm %s: %s'
1696                                      % (cid, realm, str(e)))
1697