diff --git a/awx/main/access.py b/awx/main/access.py index a2002ad67e..e513a69772 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -545,7 +545,12 @@ class InventoryUpdateAccess(BaseAccess): return qs.filter(inventory_source__in=inventory_sources_qs) def can_cancel(self, obj): - return self.can_change(obj, {}) and obj.can_cancel + if not obj.can_cancel: + return False + if self.user.is_superuser or self.user == obj.created_by: + return True + # Inventory cascade deletes to inventory update, descends from org admin + return self.user in obj.inventory_source.inventory.admin_role @check_superuser def can_delete(self, obj): @@ -736,7 +741,12 @@ class ProjectUpdateAccess(BaseAccess): @check_superuser def can_cancel(self, obj): - return self.can_change(obj, {}) and obj.can_cancel + if not obj.can_cancel: + return False + if self.user == obj.created_by: + return True + # Project updates cascade delete with project, admin role descends from org admin + return self.user in obj.project.admin_role @check_superuser def can_delete(self, obj): @@ -1015,7 +1025,12 @@ class JobAccess(BaseAccess): return inventory_access and credential_access and (org_access or project_access) def can_cancel(self, obj): - return self.can_read(obj) and obj.can_cancel + if not obj.can_cancel: + return False + # Delete access allows org admins to stop running jobs + if self.user == obj.created_by or self.can_delete(obj): + return True + return obj.job_template is not None and self.user in obj.job_template.admin_role class SystemJobTemplateAccess(BaseAccess): ''' @@ -1083,8 +1098,9 @@ class AdHocCommandAccess(BaseAccess): def can_change(self, obj, data): return False + @check_superuser def can_delete(self, obj): - return self.can_read(obj) + return obj.inventory is not None and self.user in obj.inventory.organization.admin_role def can_start(self, obj): return self.can_add({ @@ -1093,7 +1109,11 @@ class AdHocCommandAccess(BaseAccess): }) def can_cancel(self, obj): - return self.can_read(obj) and obj.can_cancel + if not obj.can_cancel: + return False + if self.user == obj.created_by: + return True + return obj.inventory is not None and self.user in obj.inventory.admin_role class AdHocCommandEventAccess(BaseAccess): ''' diff --git a/awx/main/tests/functional/test_rbac_job.py b/awx/main/tests/functional/test_rbac_job.py index f54d661095..f1688b7046 100644 --- a/awx/main/tests/functional/test_rbac_job.py +++ b/awx/main/tests/functional/test_rbac_job.py @@ -1,7 +1,18 @@ import pytest -from awx.main.access import JobAccess -from awx.main.models import Job +from awx.main.access import ( + JobAccess, + AdHocCommandAccess, + InventoryUpdateAccess, + ProjectUpdateAccess +) +from awx.main.models import ( + Job, + AdHocCommand, + InventoryUpdate, + InventorySource, + ProjectUpdate +) @pytest.fixture @@ -12,6 +23,28 @@ def normal_job(deploy_jobtemplate): inventory=deploy_jobtemplate.inventory ) +@pytest.fixture +def jt_user(deploy_jobtemplate, rando): + deploy_jobtemplate.execute_role.members.add(rando) + return rando + +@pytest.fixture +def inv_updater(inventory, rando): + inventory.update_role.members.add(rando) + return rando + +@pytest.fixture +def host_adhoc(host, machine_credential, rando): + host.inventory.adhoc_role.members.add(rando) + machine_credential.use_role.members.add(rando) + return rando + +@pytest.fixture +def proj_updater(project, rando): + project.update_role.members.add(rando) + return rando + + # Read permissions testing @pytest.mark.django_db def test_superuser_sees_orphans(normal_job, admin_user): @@ -70,3 +103,59 @@ def test_project_org_admin_delete_allowed(normal_job, org_admin): normal_job.inventory = None # do this so we test job->project->org->admin connection access = JobAccess(org_admin) assert access.can_delete(normal_job) + +@pytest.mark.django_db +class TestJobAndUpdateCancels: + + # used in view: job_template_launch + def test_jt_self_cancel(self, deploy_jobtemplate, jt_user): + job = Job(job_template=deploy_jobtemplate, created_by=jt_user) + access = JobAccess(jt_user) + assert access.can_cancel(job) + + def test_jt_friend_cancel(self, deploy_jobtemplate, admin_user, jt_user): + job = Job(job_template=deploy_jobtemplate, created_by=admin_user) + access = JobAccess(jt_user) + assert not access.can_cancel(job) + + def test_jt_org_admin_cancel(self, deploy_jobtemplate, org_admin, jt_user): + job = Job(job_template=deploy_jobtemplate, created_by=jt_user) + access = JobAccess(org_admin) + assert access.can_cancel(job) + + # used in view: host_ad_hoc_commands_list + def test_host_self_cancel(self, host, host_adhoc): + adhoc_command = AdHocCommand(inventory=host.inventory, created_by=host_adhoc) + access = AdHocCommandAccess(host_adhoc) + assert access.can_cancel(adhoc_command) + + def test_host_friend_cancel(self, host, admin_user, host_adhoc): + adhoc_command = AdHocCommand(inventory=host.inventory, created_by=admin_user) + access = AdHocCommandAccess(host_adhoc) + assert not access.can_cancel(adhoc_command) + + # used in view: inventory_source_update_view + def test_inventory_self_cancel(self, inventory, inv_updater): + inventory_update = InventoryUpdate(inventory_source=InventorySource( + name=inventory.name, inventory=inventory, source='gce' + ), created_by=inv_updater) + access = InventoryUpdateAccess(inv_updater) + assert access.can_cancel(inventory_update) + + def test_inventory_friend_cancel(self, inventory, admin_user, inv_updater): + inventory_update = InventoryUpdate(inventory_source=InventorySource( + name=inventory.name, inventory=inventory, source='gce' + ), created_by=admin_user) + access = InventoryUpdateAccess(inv_updater) + assert not access.can_cancel(inventory_update) + + # used in view: project_update_view + def test_project_self_cancel(self, project, proj_updater): + project_update = ProjectUpdate(project=project, created_by=proj_updater) + access = ProjectUpdateAccess(proj_updater) + assert access.can_cancel(project_update) + + def test_project_friend_cancel(self, project, admin_user, proj_updater): + project_update = ProjectUpdate(project=project, created_by=admin_user) + access = ProjectUpdateAccess(proj_updater) + assert not access.can_cancel(project_update)