#!/usr/bin/python # coding: utf-8 -*- # (c) 2017, Wayne Witzel III # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: role author: "Wayne Witzel III (@wwitzel3)" short_description: grant or revoke an Automation Platform Controller role. description: - Roles are used for access control, this module is for managing user access to server resources. - Grant or revoke Automation Platform Controller roles to users. See U(https://www.ansible.com/tower) for an overview. options: user: description: - User name, ID, or named URL that receives the permissions specified by the role. - Deprecated, use 'users'. type: str users: description: - User names, IDs, or named URLs that receive the permissions specified by the role. type: list elements: str team: description: - Team name, ID, or named URL that receives the permissions specified by the role. - Deprecated, use 'teams'. type: str teams: description: - Team names, IDs, or named URLs that receive the permissions specified by the role. type: list elements: str role: description: - The role type to grant/revoke. required: True choices: ["admin", "read", "member", "execute", "adhoc", "update", "use", "approval", "auditor", "project_admin", "inventory_admin", "credential_admin", "workflow_admin", "notification_admin", "job_template_admin", "execution_environment_admin"] type: str target_team: description: - Team name, ID, or named URL that the role acts on. - For example, make someone a member or an admin of a team. - Members of a team implicitly receive the permissions that the team has. - Deprecated, use 'target_teams'. type: str target_teams: description: - Team names, IDs, or named URLs that the role acts on. - For example, make someone a member or an admin of a team. - Members of a team implicitly receive the permissions that the team has. type: list elements: str inventory: description: - Inventory name, ID, or named URL the role acts on. - Deprecated, use 'inventories'. type: str inventories: description: - Inventory names, IDs, or named URLs the role acts on. type: list elements: str job_template: description: - The job template name, ID, or named URL the role acts on. - Deprecated, use 'job_templates'. type: str job_templates: description: - The job template names, IDs, or named URLs the role acts on. type: list elements: str workflow: description: - The workflow job template name, ID, or named URL the role acts on. - Deprecated, use 'workflows'. type: str workflows: description: - The workflow job template names, IDs, or named URLs the role acts on. type: list elements: str credential: description: - Credential name, ID, or named URL the role acts on. - Deprecated, use 'credentials'. type: str credentials: description: - Credential names, IDs, or named URLs the role acts on. type: list elements: str organization: description: - Organization name, ID, or named URL the role acts on. - Deprecated, use 'organizations'. type: str organizations: description: - Organization names, IDs, or named URLs the role acts on. type: list elements: str lookup_organization: description: - Organization name, ID, or named URL the inventories, job templates, projects, or workflows the items exists in. - Used to help lookup the object, for organization roles see organization. - If not provided, will lookup by name only, which does not work with duplicates. type: str project: description: - Project name, ID, or named URL the role acts on. - Deprecated, use 'projects'. type: str projects: description: - Project names, IDs, or named URLs the role acts on. type: list elements: str instance_groups: description: - Instance Group names, IDs, or named URLs the role acts on. type: list elements: str state: description: - Desired state. - State of present indicates the user should have the role. - State of absent indicates the user should have the role taken away, if they have it. default: "present" choices: ["present", "absent"] type: str extends_documentation_fragment: awx.awx.auth ''' EXAMPLES = ''' - name: Add jdoe to the member role of My Team role: user: jdoe target_team: "My Team" role: member state: present - name: Add Joe to multiple job templates and a workflow role: user: joe role: execute workflows: - test-role-workflow job_templates: - jt1 - jt2 state: present ''' from ..module_utils.controller_api import ControllerAPIModule def main(): argument_spec = dict( user=dict(), users=dict(type='list', elements='str'), team=dict(), teams=dict(type='list', elements='str'), role=dict( choices=[ "admin", "read", "member", "execute", "adhoc", "update", "use", "approval", "auditor", "project_admin", "inventory_admin", "credential_admin", "workflow_admin", "notification_admin", "job_template_admin", "execution_environment_admin", ], required=True, ), target_team=dict(), target_teams=dict(type='list', elements='str'), inventory=dict(), inventories=dict(type='list', elements='str'), job_template=dict(), job_templates=dict(type='list', elements='str'), workflow=dict(), workflows=dict(type='list', elements='str'), credential=dict(), credentials=dict(type='list', elements='str'), organization=dict(), organizations=dict(type='list', elements='str'), lookup_organization=dict(), project=dict(), projects=dict(type='list', elements='str'), instance_groups=dict(type='list', elements='str'), state=dict(choices=['present', 'absent'], default='present'), ) module = ControllerAPIModule(argument_spec=argument_spec) role_type = module.params.pop('role') role_field = role_type + '_role' state = module.params.pop('state') module.json_output['role'] = role_type # Deal with legacy parameters resource_list_param_keys = { 'credentials': 'credential', 'inventories': 'inventory', 'job_templates': 'job_template', 'organizations': 'organization', 'projects': 'project', 'target_teams': 'target_team', 'workflows': 'workflow', 'users': 'user', 'teams': 'team', 'instance_groups': 'instance_group', } resources = {} for resource_group, old_name in resource_list_param_keys.items(): if module.params.get(resource_group) is not None: resources.setdefault(resource_group, []).extend(module.params.get(resource_group)) if module.params.get(old_name) is not None: resources.setdefault(resource_group, []).append(module.params.get(old_name)) if module.params.get('lookup_organization') is not None: resources['lookup_organization'] = module.params.get('lookup_organization') if module.params.get('instance_groups') is not None: resources['instance_groups'] = module.params.get('instance_groups') # Change workflows to its endpoint name. if 'workflows' in resources: resources['workflow_job_templates'] = resources.pop('workflows') # Set lookup data to use lookup_data = {} if 'lookup_organization' in resources: lookup_data['organization'] = module.resolve_name_to_id('organizations', resources['lookup_organization']) resources.pop('lookup_organization') # Lookup actor data # separate actors from resources actor_data = {} missing_items = [] # Lookup Resources resource_data = {} for key, value in resources.items(): for resource in value: # Attempt to look up project based on the provided name, ID, or named URL and lookup data lookup_key = key if key == 'organizations' or key == 'users' or key == 'teams': lookup_data_populated = {} else: lookup_data_populated = lookup_data if key == 'target_teams': lookup_key = 'teams' data = module.get_one(lookup_key, name_or_id=resource, data=lookup_data_populated) if data is None: missing_items.append(resource) else: if key == 'users' or key == 'teams': actor_data.setdefault(key, []).append(data) elif key == 'target_teams': resource_data.setdefault('teams', []).append(data) else: resource_data.setdefault(key, []).append(data) if len(missing_items) > 0: module.fail_json( msg='There were {0} missing items, missing items: {1}'.format(len(missing_items), missing_items), changed=False ) # build association agenda associations = {} for actor_type, actors in actor_data.items(): for key, value in resource_data.items(): for resource in value: resource_roles = resource['summary_fields']['object_roles'] if role_field not in resource_roles: available_roles = ', '.join(list(resource_roles.keys())) module.fail_json( msg='Resource {0} has no role {1}, available roles: {2}'.format(resource['url'], role_field, available_roles), changed=False ) role_data = resource_roles[role_field] endpoint = '/roles/{0}/{1}/'.format(role_data['id'], actor_type) associations.setdefault(endpoint, []) for actor in actors: associations[endpoint].append(actor['id']) # perform associations for association_endpoint, new_association_list in associations.items(): response = module.get_all_endpoint(association_endpoint) existing_associated_ids = [association['id'] for association in response['json']['results']] if state == 'present': for an_id in list(set(new_association_list) - set(existing_associated_ids)): response = module.post_endpoint(association_endpoint, **{'data': {'id': int(an_id)}}) if response['status_code'] == 204: module.json_output['changed'] = True else: module.fail_json(msg="Failed to grant role. {0}".format(response['json'].get('detail', response['json'].get('msg', 'unknown')))) else: for an_id in list(set(existing_associated_ids) & set(new_association_list)): response = module.post_endpoint(association_endpoint, **{'data': {'id': int(an_id), 'disassociate': True}}) if response['status_code'] == 204: module.json_output['changed'] = True else: module.fail_json(msg="Failed to revoke role. {0}".format(response['json'].get('detail', response['json'].get('msg', 'unknown')))) module.exit_json(**module.json_output) if __name__ == '__main__': main()