From 453a0aaff231935e6762d68f6db8ba12bcc52106 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Tue, 3 May 2016 10:16:04 -0400 Subject: [PATCH 1/4] Implement ActivityStream for RBAC Roles --- .../migrations/0021_v300_activity_stream.py | 19 ++++++++++++++++++ awx/main/models/activity_stream.py | 1 + awx/main/signals.py | 20 ++++++++++++++++++- .../functional/api/test_activity_streams.py | 13 ++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 awx/main/migrations/0021_v300_activity_stream.py diff --git a/awx/main/migrations/0021_v300_activity_stream.py b/awx/main/migrations/0021_v300_activity_stream.py new file mode 100644 index 0000000000..900fd4b07d --- /dev/null +++ b/awx/main/migrations/0021_v300_activity_stream.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0020_v300_labels_changes'), + ] + + operations = [ + migrations.AddField( + model_name='activitystream', + name='role', + field=models.ManyToManyField(to='main.Role', blank=True), + ), + ] diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index cffdf83809..ae07acd79c 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -56,6 +56,7 @@ class ActivityStream(models.Model): notifier = models.ManyToManyField("Notifier", blank=True) notification = models.ManyToManyField("Notification", blank=True) label = models.ManyToManyField("Label", blank=True) + role = models.ManyToManyField("Role", blank=True) def get_absolute_url(self): return reverse('api:activity_stream_detail', args=(self.pk,)) diff --git a/awx/main/signals.py b/awx/main/signals.py index b404d4eb81..2c88c0f800 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -156,6 +156,14 @@ def org_admin_edit_members(instance, action, model, reverse, pk_set, **kwargs): if action == 'pre_remove': instance.content_object.admin_role.children.remove(user.admin_role) +def rbac_activity_stream(instance, sender, **kwargs): + user_type = ContentType.objects.get_for_model(User) + # Only if we are associating/disassociating + if kwargs['action'] in ['pre_add', 'pre_remove']: + # Only if this isn't for the User.admin_role + if instance.content_type is not None and user_type is not instance.content_type: + activity_stream_associate(sender, instance.content_object, role=instance, **kwargs) + def cleanup_detached_labels_on_deleted_parent(sender, instance, **kwargs): for l in instance.labels.all(): if l.is_candidate_for_detach(): @@ -177,6 +185,7 @@ post_save.connect(emit_job_event_detail, sender=JobEvent) post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent) m2m_changed.connect(rebuild_role_ancestor_list, Role.parents.through) m2m_changed.connect(org_admin_edit_members, Role.members.through) +m2m_changed.connect(rbac_activity_stream, Role.members.through) post_save.connect(sync_superuser_status_to_rbac, sender=User) post_save.connect(create_user_role, sender=User) pre_delete.connect(cleanup_detached_labels_on_deleted_parent, sender=UnifiedJob) @@ -354,7 +363,7 @@ def activity_stream_delete(sender, instance, **kwargs): def activity_stream_associate(sender, instance, **kwargs): if not activity_stream_enabled: return - if 'pre_add' in kwargs['action'] or 'pre_remove' in kwargs['action']: + if kwargs['action'] in ['pre_add', 'pre_remove']: if kwargs['action'] == 'pre_add': action = 'associate' elif kwargs['action'] == 'pre_remove': @@ -382,6 +391,15 @@ def activity_stream_associate(sender, instance, **kwargs): activity_entry.save() getattr(activity_entry, object1).add(obj1) getattr(activity_entry, object2).add(obj2_actual) + # Record the role for RBAC changes + if 'role' in kwargs: + role = kwargs['role'] + obj_rel = '.'.join([instance.__module__, + instance.__class__.__name__, + role.role_field]) + activity_entry.role.add(role) + activity_entry.object_relationship_type = obj_rel + activity_entry.save() @receiver(current_user_getter) diff --git a/awx/main/tests/functional/api/test_activity_streams.py b/awx/main/tests/functional/api/test_activity_streams.py index 5cb74222f6..6d75f3c4bb 100644 --- a/awx/main/tests/functional/api/test_activity_streams.py +++ b/awx/main/tests/functional/api/test_activity_streams.py @@ -59,3 +59,16 @@ def test_middleware_actor_added(monkeypatch, post, get, user): assert response.status_code == 200 assert response.data['summary_fields']['actor']['username'] == 'admin-poster' + +@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(mocker, organization, user): + 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.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' From b78c3a3e61015e0bc2460747dcce314f4e9865c0 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Tue, 3 May 2016 11:55:40 -0400 Subject: [PATCH 2/4] Fix error when adding roles from the User.roles side --- awx/main/signals.py | 22 ++++++++++++++----- .../functional/api/test_activity_streams.py | 15 ++++++++++++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index 2c88c0f800..4f88f5e559 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -161,8 +161,14 @@ def rbac_activity_stream(instance, sender, **kwargs): # Only if we are associating/disassociating if kwargs['action'] in ['pre_add', 'pre_remove']: # Only if this isn't for the User.admin_role - if instance.content_type is not None and user_type is not instance.content_type: - activity_stream_associate(sender, instance.content_object, role=instance, **kwargs) + if hasattr(instance, 'content_type'): + if instance.content_type in [None, user_type]: + return + role = instance + instance = instance.content_object + else: + role = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']).first() + activity_stream_associate(sender, instance, role=role, **kwargs) def cleanup_detached_labels_on_deleted_parent(sender, instance, **kwargs): for l in instance.labels.all(): @@ -394,9 +400,15 @@ def activity_stream_associate(sender, instance, **kwargs): # Record the role for RBAC changes if 'role' in kwargs: role = kwargs['role'] - obj_rel = '.'.join([instance.__module__, - instance.__class__.__name__, - role.role_field]) + obj_rel = '.'.join([role.content_object.__module__, + role.content_object.__class__.__name__, + role.role_field]) + + # If the m2m is from the User side we need to + # set the content_object of the Role for our entry. + if type(instance) == User: + getattr(activity_entry, role.content_type.name).add(role.content_object) + activity_entry.role.add(role) activity_entry.object_relationship_type = obj_rel activity_entry.save() diff --git a/awx/main/tests/functional/api/test_activity_streams.py b/awx/main/tests/functional/api/test_activity_streams.py index 6d75f3c4bb..4658470177 100644 --- a/awx/main/tests/functional/api/test_activity_streams.py +++ b/awx/main/tests/functional/api/test_activity_streams.py @@ -63,7 +63,7 @@ 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(mocker, organization, user): +def test_rbac_stream_resource_roles(mocker, organization, user): member = user('test', False) organization.admin_role.members.add(member) @@ -72,3 +72,16 @@ def test_rbac_stream(mocker, organization, user): 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' + +@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) + + 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' From f60ce147f4623d54abeb1cf3773a973bcd5c56dc Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Tue, 3 May 2016 12:06:17 -0400 Subject: [PATCH 3/4] Add role to summarizable fields --- awx/api/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index a889896c39..0215ecb80c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -90,6 +90,7 @@ SUMMARIZABLE_FK_FIELDS = { 'current_job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'license_error'), 'inventory_source': ('source', 'last_updated', 'status'), 'source_script': ('name', 'description'), + 'role': ('id', 'role_field') } From 65f71ba2ab37512c375fb17144497dfb47d957b2 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Tue, 3 May 2016 13:32:40 -0400 Subject: [PATCH 4/4] Fixing issue when Role is not associated with a Resource (generally never) --- awx/main/signals.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index 4f88f5e559..799a70f372 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -397,16 +397,18 @@ def activity_stream_associate(sender, instance, **kwargs): activity_entry.save() getattr(activity_entry, object1).add(obj1) getattr(activity_entry, object2).add(obj2_actual) + # Record the role for RBAC changes if 'role' in kwargs: role = kwargs['role'] - obj_rel = '.'.join([role.content_object.__module__, - role.content_object.__class__.__name__, - role.role_field]) + if role.content_object is not None: + obj_rel = '.'.join([role.content_object.__module__, + role.content_object.__class__.__name__, + role.role_field]) # If the m2m is from the User side we need to # set the content_object of the Role for our entry. - if type(instance) == User: + if type(instance) == User and role.content_object is not None: getattr(activity_entry, role.content_type.name).add(role.content_object) activity_entry.role.add(role)