From 82bb8033ec6ed89e20d7e542657e014459140dde Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Fri, 16 Oct 2020 13:27:28 -0500 Subject: [PATCH 01/10] update to approval role --- awx_collection/plugins/modules/tower_role.py | 4 ++-- awx_collection/test/awx/test_role.py | 20 +++++++++++++++++++ .../targets/tower_role/tasks/main.yml | 12 +++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index eab7c3d250..10c143864a 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: @@ -97,7 +97,7 @@ 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(), inventory=dict(), diff --git a/awx_collection/test/awx/test_role.py b/awx_collection/test/awx/test_role.py index a97bdb76a8..436d189a1d 100644 --- a/awx_collection/test/awx/test_role.py +++ b/awx_collection/test/awx/test_role.py @@ -48,6 +48,26 @@ def test_grant_workflow_permission(run_module, admin_user, organization, state): 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): 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..b1d2a98881 100644 --- a/awx_collection/tests/integration/targets/tower_role/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_role/tasks/main.yml @@ -96,6 +96,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: From 0393d537de0d50f0dba6e85ea6d06735ca16a1a9 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Sat, 17 Oct 2020 00:08:39 -0500 Subject: [PATCH 02/10] rewrite to use lists --- awx_collection/plugins/modules/tower_role.py | 148 ++++++++++++++++--- 1 file changed, 124 insertions(+), 24 deletions(-) diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index eab7c3d250..d360110d7a 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -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 organizaiton 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. @@ -101,11 +151,17 @@ def main(): "workflow_admin", "notification_admin", "job_template_admin"], required=True), target_team=dict(), 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(), + lookup_organization=dict(), project=dict(), + projects=dict(type='list', elements='str'), state=dict(choices=['present', 'absent'], default='present'), ) @@ -117,41 +173,85 @@ 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', + 'target_teams': 'target_team', + 'organizations': 'organization', + 'projects': 'project', + 'target_teams': 'target_team', + 'workflows': 'workflow' + } + # Remove Project after testing 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']) + # Stopped improvements # perform associations for association_endpoint, new_association_list in associations.items(): From c0e1ac266c8d93b966376d5b0593993776b42d39 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Sat, 17 Oct 2020 01:00:14 -0500 Subject: [PATCH 03/10] add tests --- awx_collection/test/awx/test_role.py | 22 ++++++++++ .../targets/tower_role/tasks/main.yml | 42 +++++++++++++++++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/awx_collection/test/awx/test_role.py b/awx_collection/test/awx/test_role.py index 436d189a1d..984825e27f 100644 --- a/awx_collection/test/awx/test_role.py +++ b/awx_collection/test/awx/test_role.py @@ -48,6 +48,28 @@ def test_grant_workflow_permission(run_module, admin_user, organization, state): else: 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): 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 b1d2a98881..e4859b0e62 100644 --- a/awx_collection/tests/integration/targets/tower_role/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_role/tasks/main.yml @@ -5,8 +5,11 @@ - name: Generate names set_fact: - username: "AWX-Collection-tests-tower_role-user-{{ test_id }}" - project_name: "AWX-Collection-tests-tower_role-project-{{ test_id }}" + username: "AWX-Collection-tests-tower_role-user-{{ 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 @@ -116,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 }}" From 41513614204ffbbfcf7d8eaca80ff049ed7b6273 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Sat, 17 Oct 2020 01:13:50 -0500 Subject: [PATCH 04/10] linting --- .../tests/integration/targets/tower_role/tasks/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 e4859b0e62..6c62e31832 100644 --- a/awx_collection/tests/integration/targets/tower_role/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_role/tasks/main.yml @@ -5,11 +5,11 @@ - name: Generate names set_fact: - username: "AWX-Collection-tests-tower_role-user-{{ test_id }}" + username: "AWX-Collection-tests-tower_role-user-{{ 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 }}" + 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 From e416b55b1a472685e8c83ab378831ade70b1e171 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 19 Oct 2020 15:15:33 -0500 Subject: [PATCH 05/10] update to fix missing options and fix pep8 --- awx_collection/plugins/modules/tower_role.py | 6 ++++-- awx_collection/test/awx/test_role.py | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index 6ab8b0573d..f734fab852 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -147,9 +147,11 @@ def main(): argument_spec = dict( user=dict(), team=dict(), - role=dict(choices=["admin", "read", "member", "execute", "adhoc", "update", "use", "approval", "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(), @@ -159,6 +161,7 @@ def main(): 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'), @@ -178,7 +181,6 @@ def main(): 'credentials': 'credential', 'inventories': 'inventory', 'job_templates': 'job_template', - 'target_teams': 'target_team', 'organizations': 'organization', 'projects': 'project', 'target_teams': 'target_team', diff --git a/awx_collection/test/awx/test_role.py b/awx_collection/test/awx/test_role.py index 984825e27f..fcf8bf5900 100644 --- a/awx_collection/test/awx/test_role.py +++ b/awx_collection/test/awx/test_role.py @@ -48,6 +48,7 @@ def test_grant_workflow_permission(run_module, admin_user, organization, state): else: 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): @@ -70,6 +71,7 @@ def test_grant_workflow_list_permission(run_module, admin_user, organization, st 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): @@ -91,6 +93,7 @@ def test_grant_workflow_approval_permission(run_module, admin_user, organization 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') From 1636f0cb25fdc5958be87f15a0cd4e033bf5eb9a Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 19 Oct 2020 17:35:18 -0500 Subject: [PATCH 06/10] linting --- awx_collection/plugins/modules/tower_role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index f734fab852..e01f6bd8c0 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -147,7 +147,7 @@ def main(): argument_spec = dict( user=dict(), team=dict(), - role=dict(choices=["admin", "read", "member", "execute", "adhoc", "update", "use", "approval", + 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(), From 1ca46893bb7dcdb1685842527beeef1f6e0aa1a5 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 20 Oct 2020 15:55:15 -0500 Subject: [PATCH 07/10] update --- awx_collection/plugins/modules/tower_role.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index e01f6bd8c0..dae8f866a5 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -137,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 From ab4abf4e3ba7bca893c0891719b37f18437fec9a Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 10 Nov 2020 08:17:01 -0600 Subject: [PATCH 08/10] update comment --- awx_collection/plugins/modules/tower_role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index dae8f866a5..4dbd3c7f6f 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -196,7 +196,7 @@ def main(): 'target_teams': 'target_team', 'workflows': 'workflow' } - # Remove Project after testing + # Singular parameters resource_param_keys = ( 'user', 'team', 'lookup_organization' ) From e2e3d30b49b72a0b8c4286533fbf3f8c1efd45eb Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 10 Nov 2020 09:16:08 -0600 Subject: [PATCH 09/10] update comment --- awx_collection/plugins/modules/tower_role.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index 4dbd3c7f6f..7da9b0f596 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -263,7 +263,6 @@ def main(): endpoint = '/roles/{0}/{1}/'.format(role_data['id'], module.param_to_endpoint(actor_type)) associations.setdefault(endpoint, []) associations[endpoint].append(actor['id']) - # Stopped improvements # perform associations for association_endpoint, new_association_list in associations.items(): From 4e4f1d3cceffa33c02588f501fee8f148f653ad9 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 10 Nov 2020 10:04:05 -0600 Subject: [PATCH 10/10] update comment --- awx_collection/plugins/modules/tower_role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index 7da9b0f596..980c187450 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -104,7 +104,7 @@ options: lookup_organization: description: - Organization the inventories, job templates, projects, or workflows the items exists in. - - Used to help lookup the object, for organizaiton roles see organization. + - 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: