From c8863a10b173f9f649fa9b81f6348a04c2c3acd3 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Sat, 7 May 2016 18:06:57 -0400 Subject: [PATCH 1/4] add access filters to the ActivityStream list --- awx/main/access.py | 104 +++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 7856c23a01..f4404c4d58 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1304,6 +1304,26 @@ class ActivityStreamAccess(BaseAccess): model = ActivityStream def get_queryset(self): + ''' + The full set is returned if the user is: + - System Administrator + - System Auditor + These users will be able to see orphaned activity stream items + (the related resource has been deleted), as well as the other + obscure cases listed here + + Complex permissions omitted from the activity stream of a normal user: + - host access via group + - permissions (from prior versions) + - notifications via team admin access + + Activity stream events that have been omitted from list for + normal users since 2.4: + - unified job templates + - unified jobs + - schedules + - custom inventory scripts + ''' qs = self.model.objects.all() qs = qs.select_related('actor') qs = qs.prefetch_related('organization', 'user', 'inventory', 'host', 'group', 'inventory_source', @@ -1311,60 +1331,44 @@ class ActivityStreamAccess(BaseAccess): 'permission', 'job_template', 'job') if self.user.is_superuser: return qs.all() + if self.user in Role.singleton('system_auditor'): + return qs.all() + inventory_set = Inventory.accessible_objects(self.user, 'read_role') + credential_set = Credential.accessible_objects(self.user, 'read_role') + organization_set = Organization.accessible_objects(self.user, 'read_role') + group_set = Group.accessible_objects(self.user, 'read_role') + project_set = Project.accessible_objects(self.user, 'read_role') + jt_set = JobTemplate.accessible_objects(self.user, 'read_role') + team_set = Team.accessible_objects(self.user, 'read_role') - # All of these filters are noops and tests fail when we do qs = - # qs.filter for them, so we need to figure out what the intent was, - # fix this up, and add some tests to enforce the expected behavior - # - anoek - 2016-03-31 - ''' - #Inventory filter - inventory_qs = self.user.get_queryset(Inventory) - qs.filter(inventory__in=inventory_qs) + ad_hoc_results = qs.filter( + ad_hoc_command__inventory__in=inventory_set, + ad_hoc_command__credential__in=credential_set + ) - #Host filter - qs.filter(host__inventory__in=inventory_qs) + global_results = qs.filter( + Q(user__in=organization_set.values('member_role__members')) | + Q(user=self.user) | + Q(organization__in=organization_set) | + Q(inventory__in=inventory_set) | + Q(host__inventory__in=inventory_set) | + Q(group__in=group_set) | + Q(inventory_source__inventory__in=inventory_set) | + Q(inventory_update__inventory_source__inventory__in=inventory_set) | + Q(credential__in=credential_set) | + Q(team__in=team_set) | + Q(project__in=project_set) | + Q(project_update__project__in=project_set) | + Q(job_template__in=jt_set) | + Q(job__job_template__in=jt_set) | + Q(notification_template__organization__admin_role__members__in=[self.user]) | + Q(notification__notification_template__organization__admin_role__members__in=[self.user]) | + Q(label__organization__in=organization_set) | + Q(role__in=Role.visible_roles(self.user)) + ) - #Group filter - qs.filter(group__inventory__in=inventory_qs) - - #Inventory Source Filter - qs.filter(Q(inventory_source__inventory__in=inventory_qs) | - Q(inventory_source__group__inventory__in=inventory_qs)) - - #Inventory Update Filter - qs.filter(Q(inventory_update__inventory_source__inventory__in=inventory_qs) | - Q(inventory_update__inventory_source__group__inventory__in=inventory_qs)) - - #Credential Update Filter - credential_qs = self.user.get_queryset(Credential) - qs.filter(credential__in=credential_qs) - - #Team Filter - team_qs = self.user.get_queryset(Team) - qs.filter(team__in=team_qs) - - #Project Filter - project_qs = self.user.get_queryset(Project) - qs.filter(project__in=project_qs) - - #Project Update Filter - qs.filter(project_update__project__in=project_qs) - - #Job Template Filter - jobtemplate_qs = self.user.get_queryset(JobTemplate) - qs.filter(job_template__in=jobtemplate_qs) - - #Job Filter - job_qs = self.user.get_queryset(Job) - qs.filter(job__in=job_qs) - - # Ad Hoc Command Filter - ad_hoc_command_qs = self.user.get_queryset(AdHocCommand) - qs.filter(ad_hoc_command__in=ad_hoc_command_qs) - ''' - - return qs.all() + return (ad_hoc_results | global_results).distinct() def can_add(self, data): return False From f805f43eaab8c3e81cfbfb8dfb4f59c7444e5ad9 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 16 May 2016 11:47:32 -0400 Subject: [PATCH 2/4] new fixtures for activity stream tests --- .../functional/api/test_activity_streams.py | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/awx/main/tests/functional/api/test_activity_streams.py b/awx/main/tests/functional/api/test_activity_streams.py index 4658470177..97aae7da3e 100644 --- a/awx/main/tests/functional/api/test_activity_streams.py +++ b/awx/main/tests/functional/api/test_activity_streams.py @@ -3,12 +3,19 @@ import pytest from awx.main.middleware import ActivityStreamMiddleware from awx.main.models.activity_stream import ActivityStream +from awx.main.access import ActivityStreamAccess + from django.core.urlresolvers import reverse from django.conf import settings def mock_feature_enabled(feature, bypass_database=None): return True +@pytest.fixture +def activity_stream_entry(organization, rando): + rando.roles.add(organization.admin_role) + return ActivityStream.objects.filter(organization__pk=organization.pk, operation='associate').first() + @pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled") @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db @@ -63,25 +70,30 @@ def test_middleware_actor_added(monkeypatch, post, get, user): @pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled") @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db -def test_rbac_stream_resource_roles(mocker, organization, user): - member = user('test', False) - organization.admin_role.members.add(member) +def test_rbac_stream_resource_roles(activity_stream_entry, organization, rando): - activity_stream = ActivityStream.objects.filter(organization__pk=organization.pk, operation='associate').first() - assert activity_stream.user.first() == member - assert activity_stream.organization.first() == organization - assert activity_stream.role.first() == organization.admin_role - assert activity_stream.object_relationship_type == 'awx.main.models.organization.Organization.admin_role' + assert activity_stream_entry.user.first() == rando + assert activity_stream_entry.organization.first() == organization + assert activity_stream_entry.role.first() == organization.admin_role + assert activity_stream_entry.object_relationship_type == 'awx.main.models.organization.Organization.admin_role' @pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled") @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db -def test_rbac_stream_user_roles(mocker, organization, user): - member = user('test', False) - member.roles.add(organization.admin_role) +def test_rbac_stream_user_roles(activity_stream_entry, organization, rando): - activity_stream = ActivityStream.objects.filter(organization__pk=organization.pk, operation='associate').first() - assert activity_stream.user.first() == member - assert activity_stream.organization.first() == organization - assert activity_stream.role.first() == organization.admin_role - assert activity_stream.object_relationship_type == 'awx.main.models.organization.Organization.admin_role' + assert activity_stream_entry.user.first() == rando + assert activity_stream_entry.organization.first() == organization + assert activity_stream_entry.role.first() == organization.admin_role + assert activity_stream_entry.object_relationship_type == 'awx.main.models.organization.Organization.admin_role' + +@pytest.mark.django_db +@pytest.mark.activity_stream_access +@pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled") +@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) +def test_stream_access_cant_change(activity_stream_entry, organization, rando): + access = ActivityStreamAccess(rando) + # These should always return false because the activity stream can not be edited + assert not access.can_add(activity_stream_entry) + assert not access.can_change(activity_stream_entry, {'organization': None}) + assert not access.can_delete(activity_stream_entry) From 4e2e4667defde4616c36f594fb7bff8659935031 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 16 May 2016 13:09:14 -0400 Subject: [PATCH 3/4] test for activity stream selective access --- .../tests/functional/api/test_activity_streams.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/awx/main/tests/functional/api/test_activity_streams.py b/awx/main/tests/functional/api/test_activity_streams.py index 97aae7da3e..2a85a86a5b 100644 --- a/awx/main/tests/functional/api/test_activity_streams.py +++ b/awx/main/tests/functional/api/test_activity_streams.py @@ -97,3 +97,18 @@ def test_stream_access_cant_change(activity_stream_entry, organization, rando): assert not access.can_add(activity_stream_entry) assert not access.can_change(activity_stream_entry, {'organization': None}) assert not access.can_delete(activity_stream_entry) + +@pytest.mark.django_db +@pytest.mark.activity_stream_access +@pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled") +@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) +def test_stream_queryset(activity_stream_entry, organization, rando, user_project): + # user_project and its owner has no relationship to the user rando + rando_access = ActivityStreamAccess(rando) + queryset = rando_access.get_queryset() + + # Rando can see that he was appointed organization admin + assert queryset.filter(organization__pk=organization.pk, operation='associate') + + # Rando can not see anything related to the project + assert not queryset.filter(project__pk=user_project.pk) From 9cc8ae93297fbd095c2203843092ffba5c675766 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 16 May 2016 16:17:36 -0400 Subject: [PATCH 4/4] ActivityStream access checking test for most resources --- .../functional/api/test_activity_streams.py | 51 +++++++++++++------ awx/main/tests/functional/conftest.py | 23 +++++++-- awx/main/tests/functional/test_inventory.py | 6 +-- awx/main/tests/functional/test_rbac_core.py | 6 +-- .../tests/functional/test_rbac_inventory.py | 14 ++--- 5 files changed, 66 insertions(+), 34 deletions(-) diff --git a/awx/main/tests/functional/api/test_activity_streams.py b/awx/main/tests/functional/api/test_activity_streams.py index 2a85a86a5b..43e809afb9 100644 --- a/awx/main/tests/functional/api/test_activity_streams.py +++ b/awx/main/tests/functional/api/test_activity_streams.py @@ -12,8 +12,7 @@ def mock_feature_enabled(feature, bypass_database=None): return True @pytest.fixture -def activity_stream_entry(organization, rando): - rando.roles.add(organization.admin_role) +def activity_stream_entry(organization, org_admin): return ActivityStream.objects.filter(organization__pk=organization.pk, operation='associate').first() @pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled") @@ -70,9 +69,9 @@ def test_middleware_actor_added(monkeypatch, post, get, user): @pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled") @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db -def test_rbac_stream_resource_roles(activity_stream_entry, organization, rando): +def test_rbac_stream_resource_roles(activity_stream_entry, organization, org_admin): - assert activity_stream_entry.user.first() == rando + assert activity_stream_entry.user.first() == org_admin assert activity_stream_entry.organization.first() == organization assert activity_stream_entry.role.first() == organization.admin_role assert activity_stream_entry.object_relationship_type == 'awx.main.models.organization.Organization.admin_role' @@ -80,9 +79,9 @@ def test_rbac_stream_resource_roles(activity_stream_entry, organization, rando): @pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled") @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db -def test_rbac_stream_user_roles(activity_stream_entry, organization, rando): +def test_rbac_stream_user_roles(activity_stream_entry, organization, org_admin): - assert activity_stream_entry.user.first() == rando + assert activity_stream_entry.user.first() == org_admin assert activity_stream_entry.organization.first() == organization assert activity_stream_entry.role.first() == organization.admin_role assert activity_stream_entry.object_relationship_type == 'awx.main.models.organization.Organization.admin_role' @@ -91,8 +90,8 @@ def test_rbac_stream_user_roles(activity_stream_entry, organization, rando): @pytest.mark.activity_stream_access @pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled") @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) -def test_stream_access_cant_change(activity_stream_entry, organization, rando): - access = ActivityStreamAccess(rando) +def test_stream_access_cant_change(activity_stream_entry, organization, org_admin): + access = ActivityStreamAccess(org_admin) # These should always return false because the activity stream can not be edited assert not access.can_add(activity_stream_entry) assert not access.can_change(activity_stream_entry, {'organization': None}) @@ -102,13 +101,33 @@ def test_stream_access_cant_change(activity_stream_entry, organization, rando): @pytest.mark.activity_stream_access @pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled") @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) -def test_stream_queryset(activity_stream_entry, organization, rando, user_project): - # user_project and its owner has no relationship to the user rando - rando_access = ActivityStreamAccess(rando) - queryset = rando_access.get_queryset() +def test_stream_queryset_hides_shows_items( + activity_stream_entry, organization, user, org_admin, + project, org_credential, inventory, label, deploy_jobtemplate, + notification_template, group, host, team): + # this user is not in any organizations and should not see any resource activity + no_access_user = user('no-access-user', False) + queryset = ActivityStreamAccess(no_access_user).get_queryset() - # Rando can see that he was appointed organization admin - assert queryset.filter(organization__pk=organization.pk, operation='associate') + assert not queryset.filter(project__pk=project.pk) + assert not queryset.filter(credential__pk=org_credential.pk) + assert not queryset.filter(inventory__pk=inventory.pk) + assert not queryset.filter(label__pk=label.pk) + assert not queryset.filter(job_template__pk=deploy_jobtemplate.pk) + assert not queryset.filter(group__pk=group.pk) + assert not queryset.filter(host__pk=host.pk) + assert not queryset.filter(team__pk=team.pk) + assert not queryset.filter(notification_template__pk=notification_template.pk) - # Rando can not see anything related to the project - assert not queryset.filter(project__pk=user_project.pk) + # Organization admin should be able to see most things in the ActivityStream + queryset = ActivityStreamAccess(org_admin).get_queryset() + + assert queryset.filter(project__pk=project.pk, operation='create').count() == 1 + assert queryset.filter(credential__pk=org_credential.pk, operation='create').count() == 1 + assert queryset.filter(inventory__pk=inventory.pk, operation='create').count() == 1 + assert queryset.filter(label__pk=label.pk, operation='create').count() == 1 + assert queryset.filter(job_template__pk=deploy_jobtemplate.pk, operation='create').count() == 1 + assert queryset.filter(group__pk=group.pk, operation='create').count() == 1 + assert queryset.filter(host__pk=host.pk, operation='create').count() == 1 + assert queryset.filter(team__pk=team.pk, operation='create').count() == 1 + assert queryset.filter(notification_template__pk=notification_template.pk, operation='create').count() == 1 diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 21411dd57b..59d34a2832 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -167,6 +167,11 @@ def credential(): def machine_credential(): return Credential.objects.create(name='machine-cred', kind='ssh', username='test_user', password='pas4word') +@pytest.fixture +def org_credential(organization, credential): + credential.owner_role.parents.add(organization.admin_role) + return credential + @pytest.fixture def inventory(organization): return organization.inventories.create(name="test-inv") @@ -233,7 +238,7 @@ def organizations(instance): return rf @pytest.fixture -def group(inventory): +def group_factory(inventory): def g(name): try: return Group.objects.get(name=name, inventory=inventory) @@ -242,8 +247,8 @@ def group(inventory): return g @pytest.fixture -def hosts(group): - group1 = group('group-1') +def hosts(group_factory): + group1 = group_factory('group-1') def rf(host_count=1): hosts = [] @@ -256,6 +261,14 @@ def hosts(group): return hosts return rf +@pytest.fixture +def group(inventory): + return inventory.groups.create(name='single-group') + +@pytest.fixture +def host(group, inventory): + return group.hosts.create(name='single-host', inventory=inventory) + @pytest.fixture def permissions(): return { @@ -406,8 +419,8 @@ def options(): @pytest.fixture -def fact_scans(group, fact_ansible_json, fact_packages_json, fact_services_json): - group1 = group('group-1') +def fact_scans(group_factory, fact_ansible_json, fact_packages_json, fact_services_json): + group1 = group_factory('group-1') def rf(fact_scans=1, timestamp_epoch=timezone.now()): facts_json = {} diff --git a/awx/main/tests/functional/test_inventory.py b/awx/main/tests/functional/test_inventory.py index 768162b103..5a77340c74 100644 --- a/awx/main/tests/functional/test_inventory.py +++ b/awx/main/tests/functional/test_inventory.py @@ -3,10 +3,10 @@ import pytest from django.core.urlresolvers import reverse @pytest.mark.django_db -def test_inventory_source_notification_on_cloud_only(get, post, group, user, notification_template): +def test_inventory_source_notification_on_cloud_only(get, post, group_factory, user, notification_template): u = user('admin', True) - g_cloud = group('cloud') - g_not = group('not_cloud') + g_cloud = group_factory('cloud') + g_not = group_factory('not_cloud') cloud_is = g_cloud.inventory_source not_is = g_not.inventory_source cloud_is.source = 'ec2' diff --git a/awx/main/tests/functional/test_rbac_core.py b/awx/main/tests/functional/test_rbac_core.py index 8001cb6d71..080dbbd931 100644 --- a/awx/main/tests/functional/test_rbac_core.py +++ b/awx/main/tests/functional/test_rbac_core.py @@ -81,12 +81,12 @@ def test_team_symantics(organization, team, alice): assert alice not in organization.auditor_role @pytest.mark.django_db -def test_auto_m2m_adjustments(organization, inventory, group, alice): +def test_auto_m2m_adjustments(organization, inventory, group_factory, alice): 'Ensures the auto role reparenting is working correctly through m2m maps' - g1 = group(name='g1') + g1 = group_factory(name='g1') g1.admin_role.members.add(alice) assert alice in g1.admin_role - g2 = group(name='g2') + g2 = group_factory(name='g2') assert alice not in g2.admin_role g2.parents.add(g1) diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index 29c851b094..dcb996e301 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -170,11 +170,11 @@ def test_inventory_executor(inventory, permissions, user, team): assert team.member_role.is_ancestor_of(inventory.execute_role) @pytest.mark.django_db -def test_group_parent_admin(group, permissions, user): +def test_group_parent_admin(group_factory, permissions, user): u = user('admin', False) - parent1 = group('parent-1') - parent2 = group('parent-2') - childA = group('child-1') + parent1 = group_factory('parent-1') + parent2 = group_factory('parent-2') + childA = group_factory('child-1') parent1.admin_role.members.add(u) assert u in parent1.admin_role @@ -230,11 +230,11 @@ def test_access_auditor(organization, inventory, user): @pytest.mark.django_db -def test_host_access(organization, inventory, user, group): +def test_host_access(organization, inventory, user, group_factory): other_inventory = organization.inventories.create(name='other-inventory') inventory_admin = user('inventory_admin', False) - my_group = group('my-group') - not_my_group = group('not-my-group') + my_group = group_factory('my-group') + not_my_group = group_factory('not-my-group') group_admin = user('group_admin', False) inventory_admin_access = HostAccess(inventory_admin)