diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index eab7c3d250..980c187450 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -34,7 +34,7 @@ options: description: - The role type to grant/revoke. required: True - choices: ["admin", "read", "member", "execute", "adhoc", "update", "use", "auditor", "project_admin", "inventory_admin", "credential_admin", + choices: ["admin", "read", "member", "execute", "adhoc", "update", "use", "approval", "auditor", "project_admin", "inventory_admin", "credential_admin", "workflow_admin", "notification_admin", "job_template_admin"] type: str target_team: @@ -42,31 +42,81 @@ options: - 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. + - Deprecated, use 'target_teams'. type: str + target_teams: + 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: list + elements: str inventory: description: - Inventory the role acts on. + - Deprecated, use 'inventories'. type: str + inventories: + description: + - Inventory the role acts on. + type: list + elements: str job_template: description: - The job template the role acts on. + - Deprecated, use 'job_templates'. type: str + job_templates: + description: + - The job template the role acts on. + type: list + elements: str workflow: description: - The workflow job template the role acts on. + - Deprecated, use 'workflows'. type: str + workflows: + description: + - The workflow job template the role acts on. + type: list + elements: str credential: description: - Credential the role acts on. + - Deprecated, use 'credentials'. type: str + credentials: + description: + - Credential the role acts on. + type: list + elements: str organization: description: - Organization the role acts on. + - Deprecated, use 'organizations'. + type: str + organizations: + description: + - Organization the role acts on. + type: list + elements: str + lookup_organization: + description: + - Organization 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 the role acts on. + - Deprecated, use 'projects'. type: str + projects: + description: + - Project the role acts on. + type: list + elements: str state: description: - Desired state. @@ -87,6 +137,16 @@ EXAMPLES = ''' target_team: "My Team" role: member state: present + +- name: Add Joe to multiple job templates and a workflow + tower_role: + user: joe + role: execute + workflow: test-role-workflow + job_templates: + - jt1 + - jt2 + state: present ''' from ..module_utils.tower_api import TowerAPIModule @@ -97,15 +157,24 @@ def main(): argument_spec = dict( user=dict(), team=dict(), - role=dict(choices=["admin", "read", "member", "execute", "adhoc", "update", "use", "auditor", "project_admin", "inventory_admin", "credential_admin", + 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"], 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'), state=dict(choices=['present', 'absent'], default='present'), ) @@ -117,41 +186,83 @@ def main(): module.json_output['role'] = role_type - # Lookup data for all the objects specified in params - params = module.params.copy() + # 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' + } + # Singular parameters resource_param_keys = ( - 'user', 'team', - 'target_team', 'inventory', 'job_template', 'workflow', 'credential', 'organization', 'project' + 'user', 'team', 'lookup_organization' ) - resource_data = {} - for param in resource_param_keys: - endpoint = module.param_to_endpoint(param) - resource_name = params.get(param) - if resource_name: - resource = module.get_exactly_one(module.param_to_endpoint(param), resource_name) - resource_data[param] = resource + resources = {} + for resource_group in resource_list_param_keys: + if module.params.get(resource_group) is not None: + resources.setdefault(resource_group, []).extend(module.params.get(resource_group)) + if module.params.get(resource_list_param_keys[resource_group]) is not None: + resources.setdefault(resource_group, []).append(module.params.get(resource_list_param_keys[resource_group])) + for resource_group in resource_param_keys: + if module.params.get(resource_group) is not None: + resources[resource_group] = module.params.get(resource_group) + # Change workflows and target_teams key to its endpoint name. + if 'workflows' in resources: + resources['workflow_job_templates'] = resources.pop('workflows') + if 'target_teams' in resources: + resources['teams'] = resources.pop('target_teams') + # 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 = {} for key in ('user', 'team'): - if key in resource_data: - actor_data[key] = resource_data.pop(key) + if key in resources: + if key == 'user': + lookup_data_populated = {} + else: + lookup_data_populated = lookup_data + # Attempt to look up project based on the provided name or ID and lookup data + actor_data[key] = module.get_one('{0}s'.format(key), name_or_id=resources[key], data=lookup_data_populated) + resources.pop(key) + + # Lookup Resources + resource_data = {} + for key in resources: + for resource in resources[key]: + # Attempt to look up project based on the provided name or ID and lookup data + if key in resources: + if key == 'organizations': + lookup_data_populated = {} + else: + lookup_data_populated = lookup_data + + resource_data.setdefault(key, []).append(module.get_one(key, name_or_id=resource, data=lookup_data_populated)) # 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']) + for key in resource_data: + for resource in resource_data[key]: + 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(): diff --git a/awx_collection/test/awx/test_role.py b/awx_collection/test/awx/test_role.py index a97bdb76a8..fcf8bf5900 100644 --- a/awx_collection/test/awx/test_role.py +++ b/awx_collection/test/awx/test_role.py @@ -49,6 +49,51 @@ def test_grant_workflow_permission(run_module, admin_user, organization, state): assert rando not in wfjt.execute_role +@pytest.mark.django_db +@pytest.mark.parametrize('state', ('present', 'absent')) +def test_grant_workflow_list_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, + 'lookup_organization': wfjt.organization.name, + 'workflows': [wfjt.name], + 'role': 'execute', + 'state': state + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + + if state == 'present': + assert rando in wfjt.execute_role + else: + assert rando not in wfjt.execute_role + + +@pytest.mark.django_db +@pytest.mark.parametrize('state', ('present', 'absent')) +def test_grant_workflow_approval_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': 'approval', + 'state': state + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + + if state == 'present': + assert rando in wfjt.approval_role + else: + assert rando not in wfjt.approval_role + + @pytest.mark.django_db def test_invalid_role(run_module, admin_user, project): rando = User.objects.create(username='rando') diff --git a/awx_collection/tests/integration/targets/tower_role/tasks/main.yml b/awx_collection/tests/integration/targets/tower_role/tasks/main.yml index 4102a11402..6c62e31832 100644 --- a/awx_collection/tests/integration/targets/tower_role/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_role/tasks/main.yml @@ -6,7 +6,10 @@ - name: Generate names set_fact: username: "AWX-Collection-tests-tower_role-user-{{ test_id }}" - project_name: "AWX-Collection-tests-tower_role-project-{{ test_id }}" + project_name: "AWX-Collection-tests-tower_role-project-1-{{ test_id }}" + jt1: "AWX-Collection-tests-tower_role-jt1-{{ test_id }}" + jt2: "AWX-Collection-tests-tower_role-jt2-{{ test_id }}" + wfjt_name: "AWX-Collection-tests-tower_role-project-wfjt-{{ test_id }}" - block: - name: Create a User @@ -29,17 +32,33 @@ organization: Default scm_type: git scm_url: https://github.com/ansible/test-playbooks - wait: false + wait: true register: project_info - assert: that: - project_info is changed - - name: Add Joe to the update role of the default Project + - name: Create job templates + tower_job_template: + name: "{{ item }}" + project: "{{ project_name }}" + inventory: "Demo Inventory" + playbook: become.yml + with_items: + - jt1 + - jt2 + register: result + + - assert: + that: + - "result is changed" + + - name: Add Joe to the update role of the default Project with lookup Organization tower_role: user: "{{ username }}" role: update + lookup_organization: Default project: "Demo Project" state: "{{ item }}" register: result @@ -77,6 +96,9 @@ user: "{{ username }}" role: execute workflow: test-role-workflow + job_templates: + - jt1 + - jt2 state: present register: result @@ -96,6 +118,18 @@ that: - "result is not changed" + - name: Add Joe to workflow approve role + tower_role: + user: "{{ username }}" + role: approval + workflow: test-role-workflow + state: present + register: result + + - assert: + that: + - "result is changed" + always: - name: Delete a User tower_user: @@ -104,6 +138,18 @@ state: absent register: result + - name: Delete job templates + tower_job_template: + name: "{{ item }}" + project: "{{ project_name }}" + inventory: "Demo Inventory" + playbook: debug.yml + state: absent + with_items: + - jt1 + - jt2 + register: result + - name: Delete the project tower_project: name: "{{ project_name }}"