1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Copyright (c) 2019, Adam Goossens <adam.goossens@gmail.com> 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10ANSIBLE_METADATA = { 11 'metadata_version': '1.1', 12 'status': ['preview'], 13 'supported_by': 'community' 14} 15 16DOCUMENTATION = ''' 17--- 18module: keycloak_group 19 20short_description: Allows administration of Keycloak groups via Keycloak API 21 22description: 23 - This module allows you to add, remove or modify Keycloak groups via the Keycloak REST API. 24 It requires access to the REST API via OpenID Connect; the user connecting and the client being 25 used must have the requisite access rights. In a default Keycloak installation, admin-cli 26 and an admin user would work, as would a separate client definition with the scope tailored 27 to your needs and a user having the expected roles. 28 29 - The names of module options are snake_cased versions of the camelCase ones found in the 30 Keycloak API and its documentation at U(http://www.keycloak.org/docs-api/3.3/rest-api/). 31 32 - Attributes are multi-valued in the Keycloak API. All attributes are lists of individual values and will 33 be returned that way by this module. You may pass single values for attributes when calling the module, 34 and this will be translated into a list suitable for the API. 35 36 - When updating a group, where possible provide the group ID to the module. This removes a lookup 37 to the API to translate the name into the group ID. 38 39version_added: "2.8" 40 41options: 42 state: 43 description: 44 - State of the group. 45 - On C(present), the group will be created if it does not yet exist, or updated with the parameters you provide. 46 - On C(absent), the group will be removed if it exists. 47 required: true 48 default: 'present' 49 type: str 50 choices: 51 - present 52 - absent 53 54 name: 55 type: str 56 description: 57 - Name of the group. 58 - This parameter is required only when creating or updating the group. 59 60 realm: 61 type: str 62 description: 63 - They Keycloak realm under which this group resides. 64 default: 'master' 65 66 id: 67 type: str 68 description: 69 - The unique identifier for this group. 70 - This parameter is not required for updating or deleting a group but 71 providing it will reduce the number of API calls required. 72 73 attributes: 74 type: dict 75 description: 76 - A dict of key/value pairs to set as custom attributes for the group. 77 - Values may be single values (e.g. a string) or a list of strings. 78 79notes: 80 - Presently, the I(realmRoles), I(clientRoles) and I(access) attributes returned by the Keycloak API 81 are read-only for groups. This limitation will be removed in a later version of this module. 82 83extends_documentation_fragment: 84 - keycloak 85 86author: 87 - Adam Goossens (@adamgoossens) 88''' 89 90EXAMPLES = ''' 91- name: Create a Keycloak group 92 keycloak_group: 93 name: my-new-kc-group 94 realm: MyCustomRealm 95 state: present 96 auth_client_id: admin-cli 97 auth_keycloak_url: https://auth.example.com/auth 98 auth_realm: master 99 auth_username: USERNAME 100 auth_password: PASSWORD 101 delegate_to: localhost 102 103- name: Delete a keycloak group 104 keycloak_group: 105 id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd' 106 state: absent 107 realm: MyCustomRealm 108 auth_client_id: admin-cli 109 auth_keycloak_url: https://auth.example.com/auth 110 auth_realm: master 111 auth_username: USERNAME 112 auth_password: PASSWORD 113 delegate_to: localhost 114 115- name: Delete a Keycloak group based on name 116 keycloak_group: 117 name: my-group-for-deletion 118 state: absent 119 auth_client_id: admin-cli 120 auth_keycloak_url: https://auth.example.com/auth 121 auth_realm: master 122 auth_username: USERNAME 123 auth_password: PASSWORD 124 delegate_to: localhost 125 126- name: Update the name of a Keycloak group 127 keycloak_group: 128 id: '9d59aa76-2755-48c6-b1af-beb70a82c3cd' 129 name: an-updated-kc-group-name 130 state: present 131 auth_client_id: admin-cli 132 auth_keycloak_url: https://auth.example.com/auth 133 auth_realm: master 134 auth_username: USERNAME 135 auth_password: PASSWORD 136 delegate_to: localhost 137 138- name: Create a keycloak group with some custom attributes 139 keycloak_group: 140 auth_client_id: admin-cli 141 auth_keycloak_url: https://auth.example.com/auth 142 auth_realm: master 143 auth_username: USERNAME 144 auth_password: PASSWORD 145 name: my-new_group 146 attributes: 147 attrib1: value1 148 attrib2: value2 149 attrib3: 150 - with 151 - numerous 152 - individual 153 - list 154 - items 155 delegate_to: localhost 156''' 157 158RETURN = ''' 159group: 160 description: Group representation of the group after module execution (sample is truncated). 161 returned: always 162 type: complex 163 contains: 164 id: 165 description: GUID that identifies the group 166 type: str 167 returned: always 168 sample: 23f38145-3195-462c-97e7-97041ccea73e 169 name: 170 description: Name of the group 171 type: str 172 returned: always 173 sample: grp-test-123 174 attributes: 175 description: Attributes applied to this group 176 type: dict 177 returned: always 178 sample: 179 attr1: ["val1", "val2", "val3"] 180 path: 181 description: URI path to the group 182 type: str 183 returned: always 184 sample: /grp-test-123 185 realmRoles: 186 description: An array of the realm-level roles granted to this group 187 type: list 188 returned: always 189 sample: [] 190 subGroups: 191 description: A list of groups that are children of this group. These groups will have the same parameters as 192 documented here. 193 type: list 194 returned: always 195 clientRoles: 196 description: A list of client-level roles granted to this group 197 type: list 198 returned: always 199 sample: [] 200 access: 201 description: A dict describing the accesses you have to this group based on the credentials used. 202 type: dict 203 returned: always 204 sample: 205 manage: true 206 manageMembership: true 207 view: true 208''' 209 210from ansible.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ 211 keycloak_argument_spec, get_token, KeycloakError 212from ansible.module_utils.basic import AnsibleModule 213 214 215def main(): 216 """ 217 Module execution 218 219 :return: 220 """ 221 argument_spec = keycloak_argument_spec() 222 meta_args = dict( 223 state=dict(default='present', choices=['present', 'absent']), 224 realm=dict(default='master'), 225 id=dict(type='str'), 226 name=dict(type='str'), 227 attributes=dict(type='dict') 228 ) 229 230 argument_spec.update(meta_args) 231 232 module = AnsibleModule(argument_spec=argument_spec, 233 supports_check_mode=True, 234 required_one_of=([['id', 'name']])) 235 236 result = dict(changed=False, msg='', diff={}, group='') 237 238 # Obtain access token, initialize API 239 try: 240 connection_header = get_token( 241 base_url=module.params.get('auth_keycloak_url'), 242 validate_certs=module.params.get('validate_certs'), 243 auth_realm=module.params.get('auth_realm'), 244 client_id=module.params.get('auth_client_id'), 245 auth_username=module.params.get('auth_username'), 246 auth_password=module.params.get('auth_password'), 247 client_secret=module.params.get('auth_client_secret'), 248 ) 249 except KeycloakError as e: 250 module.fail_json(msg=str(e)) 251 kc = KeycloakAPI(module, connection_header) 252 253 realm = module.params.get('realm') 254 state = module.params.get('state') 255 gid = module.params.get('id') 256 name = module.params.get('name') 257 attributes = module.params.get('attributes') 258 259 before_group = None # current state of the group, for merging. 260 261 # does the group already exist? 262 if gid is None: 263 before_group = kc.get_group_by_name(name, realm=realm) 264 else: 265 before_group = kc.get_group_by_groupid(gid, realm=realm) 266 267 before_group = {} if before_group is None else before_group 268 269 # attributes in Keycloak have their values returned as lists 270 # via the API. attributes is a dict, so we'll transparently convert 271 # the values to lists. 272 if attributes is not None: 273 for key, val in module.params['attributes'].items(): 274 module.params['attributes'][key] = [val] if not isinstance(val, list) else val 275 276 group_params = [x for x in module.params 277 if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and 278 module.params.get(x) is not None] 279 280 # build a changeset 281 changeset = {} 282 for param in group_params: 283 new_param_value = module.params.get(param) 284 old_value = before_group[param] if param in before_group else None 285 if new_param_value != old_value: 286 changeset[camel(param)] = new_param_value 287 288 # prepare the new group 289 updated_group = before_group.copy() 290 updated_group.update(changeset) 291 292 # if before_group is none, the group doesn't exist. 293 if before_group == {}: 294 if state == 'absent': 295 # nothing to do. 296 if module._diff: 297 result['diff'] = dict(before='', after='') 298 result['msg'] = 'Group does not exist; doing nothing.' 299 result['group'] = dict() 300 module.exit_json(**result) 301 302 # for 'present', create a new group. 303 result['changed'] = True 304 if name is None: 305 module.fail_json(msg='name must be specified when creating a new group') 306 307 if module._diff: 308 result['diff'] = dict(before='', after=updated_group) 309 310 if module.check_mode: 311 module.exit_json(**result) 312 313 # do it for real! 314 kc.create_group(updated_group, realm=realm) 315 after_group = kc.get_group_by_name(name, realm) 316 317 result['group'] = after_group 318 result['msg'] = 'Group {name} has been created with ID {id}'.format(name=after_group['name'], 319 id=after_group['id']) 320 321 else: 322 if state == 'present': 323 # no changes 324 if updated_group == before_group: 325 result['changed'] = False 326 result['group'] = updated_group 327 result['msg'] = "No changes required to group {name}.".format(name=before_group['name']) 328 module.exit_json(**result) 329 330 # update the existing group 331 result['changed'] = True 332 333 if module._diff: 334 result['diff'] = dict(before=before_group, after=updated_group) 335 336 if module.check_mode: 337 module.exit_json(**result) 338 339 # do the update 340 kc.update_group(updated_group, realm=realm) 341 342 after_group = kc.get_group_by_groupid(updated_group['id'], realm=realm) 343 344 result['group'] = after_group 345 result['msg'] = "Group {id} has been updated".format(id=after_group['id']) 346 347 module.exit_json(**result) 348 349 elif state == 'absent': 350 result['group'] = dict() 351 352 if module._diff: 353 result['diff'] = dict(before=before_group, after='') 354 355 if module.check_mode: 356 module.exit_json(**result) 357 358 # delete for real 359 gid = before_group['id'] 360 kc.delete_group(groupid=gid, realm=realm) 361 362 result['changed'] = True 363 result['msg'] = "Group {name} has been deleted".format(name=before_group['name']) 364 365 module.exit_json(**result) 366 367 module.exit_json(**result) 368 369 370if __name__ == '__main__': 371 main() 372