From 4eab362318a706290ae005c2e5c2840605735962 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 2 Apr 2019 13:26:37 -0400 Subject: [PATCH] fix RBAC bugs with notification attachment Allow notification_admin_role users to attach NTs from that organization Require either read_role or auditor_role to the object which the NT is being attached to --- awx/main/access.py | 51 +++++++-- .../functional/test_rbac_notifications.py | 101 ++++++++++++++++++ 2 files changed, 145 insertions(+), 7 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 52a138b970..01bd3e3096 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -460,6 +460,42 @@ class BaseAccess(object): return False +class NotificationAttachMixin(BaseAccess): + '''For models that can have notifications attached + + I can attach a notification template when + - I have notification_admin_role to organization of the NT + - I can read the object I am attaching it to + + I can unattach when those same critiera are met + ''' + notification_attach_roles = None + + def _can_attach(self, notification_template, resource_obj): + if not NotificationTemplateAccess(self.user).can_change(notification_template, {}): + return False + if self.notification_attach_roles is None: + return self.can_read(resource_obj) + return any(self.user in getattr(resource_obj, role) for role in self.notification_attach_roles) + + @check_superuser + def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False): + if isinstance(sub_obj, NotificationTemplate): + # reverse obj and sub_obj + return self._can_attach(notification_template=sub_obj, resource_obj=obj) + return super(NotificationAttachMixin, self).can_attach( + obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check) + + @check_superuser + def can_unattach(self, obj, sub_obj, relationship, data=None): + if isinstance(sub_obj, NotificationTemplate): + # due to this special case, we use symmetrical logic with attach permission + return self._can_attach(notification_template=sub_obj, resource_obj=obj) + return super(NotificationAttachMixin, self).can_unattach( + obj, sub_obj, relationship, relationship, data=data + ) + + class InstanceAccess(BaseAccess): model = Instance @@ -715,7 +751,7 @@ class OAuth2TokenAccess(BaseAccess): return True -class OrganizationAccess(BaseAccess): +class OrganizationAccess(NotificationAttachMixin, BaseAccess): ''' I can see organizations when: - I am a superuser. @@ -729,6 +765,8 @@ class OrganizationAccess(BaseAccess): model = Organization prefetch_related = ('created_by', 'modified_by',) + # organization admin_role is not a parent of organization auditor_role + notification_attach_roles = ['admin_role', 'auditor_role'] def filtered_queryset(self): return self.model.accessible_objects(self.user, 'read_role') @@ -966,7 +1004,7 @@ class GroupAccess(BaseAccess): return False -class InventorySourceAccess(BaseAccess): +class InventorySourceAccess(NotificationAttachMixin, BaseAccess): ''' I can see inventory sources whenever I can see their inventory. I can change inventory sources whenever I can change their inventory. @@ -1282,7 +1320,7 @@ class TeamAccess(BaseAccess): *args, **kwargs) -class ProjectAccess(BaseAccess): +class ProjectAccess(NotificationAttachMixin, BaseAccess): ''' I can see projects when: - I am a superuser. @@ -1301,6 +1339,7 @@ class ProjectAccess(BaseAccess): model = Project select_related = ('modified_by', 'credential', 'current_job', 'last_job',) + notification_attach_roles = ['admin_role'] def filtered_queryset(self): return self.model.accessible_objects(self.user, 'read_role') @@ -1363,7 +1402,7 @@ class ProjectUpdateAccess(BaseAccess): return obj and self.user in obj.project.admin_role -class JobTemplateAccess(BaseAccess): +class JobTemplateAccess(NotificationAttachMixin, BaseAccess): ''' I can see job templates when: - I have read role for the job template. @@ -1514,8 +1553,6 @@ class JobTemplateAccess(BaseAccess): @check_superuser def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False): - if isinstance(sub_obj, NotificationTemplate): - return self.check_related('organization', Organization, {}, obj=sub_obj, mandatory=True) if relationship == "instance_groups": if not obj.project.organization: return False @@ -1913,7 +1950,7 @@ class WorkflowJobNodeAccess(BaseAccess): # TODO: notification attachments? -class WorkflowJobTemplateAccess(BaseAccess): +class WorkflowJobTemplateAccess(NotificationAttachMixin, BaseAccess): ''' I can only see/manage Workflow Job Templates if I'm a super user ''' diff --git a/awx/main/tests/functional/test_rbac_notifications.py b/awx/main/tests/functional/test_rbac_notifications.py index 18ff3959aa..e98cae3ade 100644 --- a/awx/main/tests/functional/test_rbac_notifications.py +++ b/awx/main/tests/functional/test_rbac_notifications.py @@ -1,5 +1,6 @@ import pytest +from awx.main.models import Organization, Project from awx.main.access import ( NotificationTemplateAccess, NotificationAccess, @@ -137,6 +138,106 @@ def test_system_auditor_JT_attach(system_auditor, job_template, notification_tem {'id': notification_template.id}) +@pytest.mark.django_db +@pytest.mark.parametrize("org_role,expect", [ + ('admin_role', True), + ('notification_admin_role', True), + ('workflow_admin_role', False), + ('auditor_role', False), + ('member_role', False) +]) +def test_org_role_JT_attach(rando, job_template, project, workflow_job_template, inventory_source, + notification_template, org_role, expect): + nt_organization = Organization.objects.create(name='organization just for the notification template') + notification_template.organization = nt_organization + notification_template.save() + getattr(notification_template.organization, org_role).members.add(rando) + kwargs = dict( + sub_obj=notification_template, + relationship='notification_templates_success', + data={'id': notification_template.id} + ) + permissions = {} + expected_permissions = {} + organization = Organization.objects.create(name='objective organization') + + for resource in (organization, job_template, project, workflow_job_template, inventory_source): + permission_resource = resource + if resource == inventory_source: + permission_resource = inventory_source.inventory + getattr(permission_resource, 'admin_role').members.add(rando) + model_name = resource.__class__.__name__ + permissions[model_name] = rando.can_access(resource.__class__, 'attach', resource, **kwargs) + expected_permissions[model_name] = expect + + assert permissions == expected_permissions + + +@pytest.mark.django_db +def test_organization_NT_attach_permission(rando, notification_template): + notification_template.organization.notification_admin_role.members.add(rando) + target_organization = Organization.objects.create(name='objective organization') + target_organization.workflow_admin_role.members.add(rando) + assert not rando.can_access(Organization, 'attach', obj=target_organization, sub_obj=notification_template, + relationship='notification_templates_success', data={}) + target_organization.auditor_role.members.add(rando) + assert rando.can_access(Organization, 'attach', obj=target_organization, sub_obj=notification_template, + relationship='notification_templates_success', data={}) + + +@pytest.mark.django_db +def test_project_NT_attach_permission(rando, notification_template): + notification_template.organization.notification_admin_role.members.add(rando) + project = Project.objects.create( + name='objective project', + organization=Organization.objects.create(name='foo') + ) + project.update_role.members.add(rando) + assert not rando.can_access(Project, 'attach', obj=project, sub_obj=notification_template, + relationship='notification_templates_success', data={}) + project.admin_role.members.add(rando) + assert rando.can_access(Project, 'attach', obj=project, sub_obj=notification_template, + relationship='notification_templates_success', data={}) + + +@pytest.mark.django_db +@pytest.mark.parametrize("res_role,expect", [ + ('read_role', True), + (None, False) +]) +def test_object_role_JT_attach(rando, job_template, workflow_job_template, inventory_source, + notification_template, res_role, expect): + nt_organization = Organization.objects.create(name='organization just for the notification template') + nt_organization.notification_admin_role.members.add(rando) + notification_template.organization = nt_organization + notification_template.save() + kwargs = dict( + sub_obj=notification_template, + relationship='notification_templates_success', + data={'id': notification_template.id} + ) + permissions = {} + expected_permissions = {} + + for resource in (job_template, workflow_job_template, inventory_source): + permission_resource = resource + if resource == inventory_source: + permission_resource = inventory_source.inventory + model_name = resource.__class__.__name__ + if res_role is None or hasattr(permission_resource, res_role): + if res_role is not None: + getattr(permission_resource, res_role).members.add(rando) + permissions[model_name] = rando.can_access( + resource.__class__, 'attach', resource, **kwargs + ) + expected_permissions[model_name] = expect + else: + permissions[model_name] = None + expected_permissions[model_name] = None + + assert permissions == expected_permissions + + @pytest.mark.django_db def test_notification_access_org_admin(notification, org_admin): access = NotificationAccess(org_admin)