mirror of
https://github.com/ansible/awx.git
synced 2026-03-23 11:55:04 -02:30
Merge pull request #1892 from AlanCoding/1364_AS_permission
Access filters in ActivityStream
This commit is contained in:
@@ -1304,6 +1304,26 @@ class ActivityStreamAccess(BaseAccess):
|
|||||||
model = ActivityStream
|
model = ActivityStream
|
||||||
|
|
||||||
def get_queryset(self):
|
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 = self.model.objects.all()
|
||||||
qs = qs.select_related('actor')
|
qs = qs.select_related('actor')
|
||||||
qs = qs.prefetch_related('organization', 'user', 'inventory', 'host', 'group', 'inventory_source',
|
qs = qs.prefetch_related('organization', 'user', 'inventory', 'host', 'group', 'inventory_source',
|
||||||
@@ -1311,60 +1331,44 @@ class ActivityStreamAccess(BaseAccess):
|
|||||||
'permission', 'job_template', 'job')
|
'permission', 'job_template', 'job')
|
||||||
if self.user.is_superuser:
|
if self.user.is_superuser:
|
||||||
return qs.all()
|
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 =
|
ad_hoc_results = qs.filter(
|
||||||
# qs.filter for them, so we need to figure out what the intent was,
|
ad_hoc_command__inventory__in=inventory_set,
|
||||||
# fix this up, and add some tests to enforce the expected behavior
|
ad_hoc_command__credential__in=credential_set
|
||||||
# - anoek - 2016-03-31
|
)
|
||||||
'''
|
|
||||||
#Inventory filter
|
|
||||||
inventory_qs = self.user.get_queryset(Inventory)
|
|
||||||
qs.filter(inventory__in=inventory_qs)
|
|
||||||
|
|
||||||
#Host filter
|
global_results = qs.filter(
|
||||||
qs.filter(host__inventory__in=inventory_qs)
|
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
|
return (ad_hoc_results | global_results).distinct()
|
||||||
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()
|
|
||||||
|
|
||||||
def can_add(self, data):
|
def can_add(self, data):
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ import pytest
|
|||||||
|
|
||||||
from awx.main.middleware import ActivityStreamMiddleware
|
from awx.main.middleware import ActivityStreamMiddleware
|
||||||
from awx.main.models.activity_stream import ActivityStream
|
from awx.main.models.activity_stream import ActivityStream
|
||||||
|
from awx.main.access import ActivityStreamAccess
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
def mock_feature_enabled(feature, bypass_database=None):
|
def mock_feature_enabled(feature, bypass_database=None):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
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")
|
@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)
|
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -63,25 +69,65 @@ def test_middleware_actor_added(monkeypatch, post, get, user):
|
|||||||
@pytest.mark.skipif(not getattr(settings, 'ACTIVITY_STREAM_ENABLED', True), reason="Activity stream not enabled")
|
@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)
|
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_rbac_stream_resource_roles(mocker, organization, user):
|
def test_rbac_stream_resource_roles(activity_stream_entry, organization, org_admin):
|
||||||
member = user('test', False)
|
|
||||||
organization.admin_role.members.add(member)
|
|
||||||
|
|
||||||
activity_stream = ActivityStream.objects.filter(organization__pk=organization.pk, operation='associate').first()
|
assert activity_stream_entry.user.first() == org_admin
|
||||||
assert activity_stream.user.first() == member
|
assert activity_stream_entry.organization.first() == organization
|
||||||
assert activity_stream.organization.first() == organization
|
assert activity_stream_entry.role.first() == organization.admin_role
|
||||||
assert activity_stream.role.first() == organization.admin_role
|
assert activity_stream_entry.object_relationship_type == 'awx.main.models.organization.Organization.admin_role'
|
||||||
assert activity_stream.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")
|
@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)
|
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_rbac_stream_user_roles(mocker, organization, user):
|
def test_rbac_stream_user_roles(activity_stream_entry, organization, org_admin):
|
||||||
member = user('test', False)
|
|
||||||
member.roles.add(organization.admin_role)
|
|
||||||
|
|
||||||
activity_stream = ActivityStream.objects.filter(organization__pk=organization.pk, operation='associate').first()
|
assert activity_stream_entry.user.first() == org_admin
|
||||||
assert activity_stream.user.first() == member
|
assert activity_stream_entry.organization.first() == organization
|
||||||
assert activity_stream.organization.first() == organization
|
assert activity_stream_entry.role.first() == organization.admin_role
|
||||||
assert activity_stream.role.first() == organization.admin_role
|
assert activity_stream_entry.object_relationship_type == 'awx.main.models.organization.Organization.admin_role'
|
||||||
assert activity_stream.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, 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})
|
||||||
|
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_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()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|||||||
@@ -167,6 +167,11 @@ def credential():
|
|||||||
def machine_credential():
|
def machine_credential():
|
||||||
return Credential.objects.create(name='machine-cred', kind='ssh', username='test_user', password='pas4word')
|
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
|
@pytest.fixture
|
||||||
def inventory(organization):
|
def inventory(organization):
|
||||||
return organization.inventories.create(name="test-inv")
|
return organization.inventories.create(name="test-inv")
|
||||||
@@ -233,7 +238,7 @@ def organizations(instance):
|
|||||||
return rf
|
return rf
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def group(inventory):
|
def group_factory(inventory):
|
||||||
def g(name):
|
def g(name):
|
||||||
try:
|
try:
|
||||||
return Group.objects.get(name=name, inventory=inventory)
|
return Group.objects.get(name=name, inventory=inventory)
|
||||||
@@ -242,8 +247,8 @@ def group(inventory):
|
|||||||
return g
|
return g
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def hosts(group):
|
def hosts(group_factory):
|
||||||
group1 = group('group-1')
|
group1 = group_factory('group-1')
|
||||||
|
|
||||||
def rf(host_count=1):
|
def rf(host_count=1):
|
||||||
hosts = []
|
hosts = []
|
||||||
@@ -256,6 +261,14 @@ def hosts(group):
|
|||||||
return hosts
|
return hosts
|
||||||
return rf
|
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
|
@pytest.fixture
|
||||||
def permissions():
|
def permissions():
|
||||||
return {
|
return {
|
||||||
@@ -406,8 +419,8 @@ def options():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def fact_scans(group, fact_ansible_json, fact_packages_json, fact_services_json):
|
def fact_scans(group_factory, fact_ansible_json, fact_packages_json, fact_services_json):
|
||||||
group1 = group('group-1')
|
group1 = group_factory('group-1')
|
||||||
|
|
||||||
def rf(fact_scans=1, timestamp_epoch=timezone.now()):
|
def rf(fact_scans=1, timestamp_epoch=timezone.now()):
|
||||||
facts_json = {}
|
facts_json = {}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import pytest
|
|||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@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)
|
u = user('admin', True)
|
||||||
g_cloud = group('cloud')
|
g_cloud = group_factory('cloud')
|
||||||
g_not = group('not_cloud')
|
g_not = group_factory('not_cloud')
|
||||||
cloud_is = g_cloud.inventory_source
|
cloud_is = g_cloud.inventory_source
|
||||||
not_is = g_not.inventory_source
|
not_is = g_not.inventory_source
|
||||||
cloud_is.source = 'ec2'
|
cloud_is.source = 'ec2'
|
||||||
|
|||||||
@@ -81,12 +81,12 @@ def test_team_symantics(organization, team, alice):
|
|||||||
assert alice not in organization.auditor_role
|
assert alice not in organization.auditor_role
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@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'
|
'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)
|
g1.admin_role.members.add(alice)
|
||||||
assert alice in g1.admin_role
|
assert alice in g1.admin_role
|
||||||
g2 = group(name='g2')
|
g2 = group_factory(name='g2')
|
||||||
assert alice not in g2.admin_role
|
assert alice not in g2.admin_role
|
||||||
|
|
||||||
g2.parents.add(g1)
|
g2.parents.add(g1)
|
||||||
|
|||||||
@@ -170,11 +170,11 @@ def test_inventory_executor(inventory, permissions, user, team):
|
|||||||
assert team.member_role.is_ancestor_of(inventory.execute_role)
|
assert team.member_role.is_ancestor_of(inventory.execute_role)
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@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)
|
u = user('admin', False)
|
||||||
parent1 = group('parent-1')
|
parent1 = group_factory('parent-1')
|
||||||
parent2 = group('parent-2')
|
parent2 = group_factory('parent-2')
|
||||||
childA = group('child-1')
|
childA = group_factory('child-1')
|
||||||
|
|
||||||
parent1.admin_role.members.add(u)
|
parent1.admin_role.members.add(u)
|
||||||
assert u in parent1.admin_role
|
assert u in parent1.admin_role
|
||||||
@@ -230,11 +230,11 @@ def test_access_auditor(organization, inventory, user):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@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')
|
other_inventory = organization.inventories.create(name='other-inventory')
|
||||||
inventory_admin = user('inventory_admin', False)
|
inventory_admin = user('inventory_admin', False)
|
||||||
my_group = group('my-group')
|
my_group = group_factory('my-group')
|
||||||
not_my_group = group('not-my-group')
|
not_my_group = group_factory('not-my-group')
|
||||||
group_admin = user('group_admin', False)
|
group_admin = user('group_admin', False)
|
||||||
|
|
||||||
inventory_admin_access = HostAccess(inventory_admin)
|
inventory_admin_access = HostAccess(inventory_admin)
|
||||||
|
|||||||
Reference in New Issue
Block a user