From 27948aa4e179349c94a1ef5cf23e568476262d41 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 20 Mar 2020 21:16:09 -0400 Subject: [PATCH] Convert tower_role to no longer use tower-cli --- .../plugins/module_utils/tower_api.py | 9 ++ awx_collection/plugins/modules/tower_role.py | 139 ++++++++++-------- awx_collection/test/awx/test_role.py | 39 ++++- 3 files changed, 120 insertions(+), 67 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 0725dc6fe8..555564b1ae 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -190,6 +190,15 @@ class TowerModule(AnsibleModule): else: setattr(self, honorred_setting, config_data[honorred_setting]) + @staticmethod + def param_to_endpoint(name): + exceptions = { + 'inventory': 'inventories', + 'target_team': 'teams', + 'workflow': 'workflow_job_templates' + } + return exceptions.get(name, '{0}s'.format(name)) + def head_endpoint(self, endpoint, *args, **kwargs): return self.make_request('HEAD', endpoint, **kwargs) diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index 80f3c5370a..b7cf2c7e2e 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -18,10 +18,10 @@ DOCUMENTATION = ''' module: tower_role version_added: "2.3" author: "Wayne Witzel III (@wwitzel3)" -short_description: create, update, or destroy Ansible Tower role. +short_description: grant or revoke an Ansible Tower role. description: - - Create, update, or destroy Ansible Tower roles. See - U(https://www.ansible.com/tower) for an overview. + - Roles are used for access control, this module is for managing user access to server resources. + - Grant or revoke Ansible Tower roles to users. See U(https://www.ansible.com/tower) for an overview. options: user: description: @@ -41,6 +41,8 @@ options: target_team: description: - Team 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: str inventory: description: @@ -52,7 +54,7 @@ options: type: str workflow: description: - - The job template the role acts on. + - The workflow job template the role acts on. type: str credential: description: @@ -68,10 +70,18 @@ options: type: str state: description: - - Desired state of the resource. + - 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 + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str + version_added: "3.7" requirements: - ansible-tower-cli >= 3.0.2 @@ -87,45 +97,9 @@ EXAMPLES = ''' target_team: "My Team" role: member state: present - tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode - -try: - import tower_cli - import tower_cli.exceptions as exc - - from tower_cli.conf import settings -except ImportError: - pass - - -def update_resources(module, p): - '''update_resources attempts to fetch any of the resources given - by name using their unique field (identity) - ''' - params = p.copy() - identity_map = { - 'user': 'username', - 'team': 'name', - 'target_team': 'name', - 'inventory': 'name', - 'job_template': 'name', - 'workflow': 'name', - 'credential': 'name', - 'organization': 'name', - 'project': 'name', - } - for k, v in identity_map.items(): - try: - if params[k]: - key = 'team' if k == 'target_team' else k - result = tower_cli.get_resource(key).get(**{v: params[k]}) - params[k] = result['id'] - except (exc.NotFound) as excinfo: - module.fail_json(msg='Failed to update role, {0} not found: {1}'.format(k, excinfo), changed=False) - return params +from ..module_utils.tower_api import TowerModule def main(): @@ -147,32 +121,75 @@ def main(): module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) - module.deprecate(msg="This module is being moved to a different collection. Instead of awx.awx it will be migrated into awx.tower_cli", version="3.7") - role_type = module.params.pop('role') + role_field = role_type + '_role' state = module.params.pop('state') - json_output = {'role': role_type, 'state': state} + module.json_output['role'] = role_type - tower_auth = tower_auth_config(module) - with settings.runtime_values(**tower_auth): - tower_check_mode(module) - role = tower_cli.get_resource('role') + # Lookup data for all the objects specified in params + params = module.params.copy() + resource_param_keys = ( + 'user', 'team', + 'target_team', 'inventory', 'job_template', 'workflow', 'credential', 'organization', 'project' + ) + resource_data = {} + for param in resource_param_keys: + endpoint = module.param_to_endpoint(param) + name_field = 'username' if param == 'user' else 'name' - params = update_resources(module, module.params) - params['type'] = role_type + resource_name = params.get(param) + if resource_name: + resource = module.get_one(endpoint, **{'data': {name_field: resource_name}}) + if not resource: + module.fail_json( + msg='Failed to update role, {0} not found in {1}'.format(param, endpoint), + changed=False + ) + resource_data[param] = resource - try: - if state == 'present': - result = role.grant(**params) - json_output['id'] = result['id'] - elif state == 'absent': - result = role.revoke(**params) - except (exc.ConnectionError, exc.BadRequest, exc.NotFound, exc.AuthError) as excinfo: - module.fail_json(msg='Failed to update role: {0}'.format(excinfo), changed=False) + # separate actors from resources + actor_data = {} + for key in ('user', 'team'): + if key in resource_data: + actor_data[key] = resource_data.pop(key) - json_output['changed'] = result['changed'] - module.exit_json(**json_output) + # build association agenda + associations = {} + for actor_type, actor in actor_data.items(): + for resource_type, resource in resource_data.items(): + 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'], module.param_to_endpoint(actor_type)) + associations.setdefault(endpoint, []) + 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']['detail'])) + 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']['detail'])) + + module.exit_json(**module.json_output) if __name__ == '__main__': diff --git a/awx_collection/test/awx/test_role.py b/awx_collection/test/awx/test_role.py index 464a474b0a..a97bdb76a8 100644 --- a/awx_collection/test/awx/test_role.py +++ b/awx_collection/test/awx/test_role.py @@ -7,31 +7,58 @@ from awx.main.models import WorkflowJobTemplate, User @pytest.mark.django_db -def test_grant_organization_permission(run_module, admin_user, organization, silence_deprecation): +@pytest.mark.parametrize('state', ('present', 'absent')) +def test_grant_organization_permission(run_module, admin_user, organization, state): rando = User.objects.create(username='rando') + if state == 'absent': + organization.admin_role.members.add(rando) result = run_module('tower_role', { 'user': rando.username, 'organization': organization.name, 'role': 'admin', - 'state': 'present' + 'state': state }, admin_user) assert not result.get('failed', False), result.get('msg', result) - assert rando in organization.execute_role + if state == 'present': + assert rando in organization.execute_role + else: + assert rando not in organization.execute_role @pytest.mark.django_db -def test_grant_workflow_permission(run_module, admin_user, organization, silence_deprecation): +@pytest.mark.parametrize('state', ('present', 'absent')) +def test_grant_workflow_permission(run_module, admin_user, organization, state): wfjt = WorkflowJobTemplate.objects.create(organization=organization, name='foo-workflow') rando = User.objects.create(username='rando') + if state == 'absent': + wfjt.execute_role.members.add(rando) result = run_module('tower_role', { 'user': rando.username, 'workflow': wfjt.name, 'role': 'execute', - 'state': 'present' + 'state': state }, admin_user) assert not result.get('failed', False), result.get('msg', result) - assert rando in wfjt.execute_role + if state == 'present': + assert rando in wfjt.execute_role + else: + assert rando not in wfjt.execute_role + + +@pytest.mark.django_db +def test_invalid_role(run_module, admin_user, project): + rando = User.objects.create(username='rando') + result = run_module('tower_role', { + 'user': rando.username, + 'project': project.name, + 'role': 'adhoc', + 'state': 'present' + }, admin_user) + assert result.get('failed', False) + msg = result.get('msg') + assert 'has no role adhoc_role' in msg + assert 'available roles: admin_role, use_role, update_role, read_role' in msg