From 524343870b036273b1881078725fd7f4183675b1 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 5 Apr 2018 09:46:03 -0400 Subject: [PATCH 1/5] Added Project & Inventory signals for JobTemplate RBAC --- awx/main/signals.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index c79b1742aa..cc7f7624f4 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -9,7 +9,13 @@ import json # Django from django.conf import settings -from django.db.models.signals import post_save, pre_delete, post_delete, m2m_changed +from django.db.models.signals import ( + post_init, + post_save, + pre_delete, + post_delete, + m2m_changed, +) from django.dispatch import receiver from django.contrib.auth import SESSION_KEY from django.utils import timezone @@ -229,6 +235,30 @@ def cleanup_detached_labels_on_deleted_parent(sender, instance, **kwargs): if l.is_candidate_for_detach(): l.delete() +def set_original_organization(sender, instance, **kwargs): + '''set_original_organization is used to set the original, or + pre-save organization, so we can later determine if the organization + field is dirty. + ''' + instance.__original_org = instance.organization + +def save_related_job_templates(sender, instance, **kwargs): + '''save_related_job_templates loops through all of the + job templates that use an Inventory or Project that have had their + Organization updated. This triggers the rebuilding of the RBAC hierarchy + and ensures the proper access restrictions. + ''' + if instance.__original_org != instance.organization: + instance.__original_org = instance.organization + jtq = None + if sender == Project: + jtq = JobTemplate.objects.filter(project=instance) + elif sender == Inventory: + jtq = JobTemplate.objects.filter(inventory=instance) + if jtq: + for jt in jtq.all(): + jt.save() + def connect_computed_field_signals(): post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host) @@ -247,7 +277,10 @@ def connect_computed_field_signals(): connect_computed_field_signals() - +post_init.connect(set_original_organization, sender=Project) +post_init.connect(set_original_organization, sender=Inventory) +post_save.connect(save_related_job_templates, sender=Project) +post_save.connect(save_related_job_templates, sender=Inventory) post_save.connect(emit_job_event_detail, sender=JobEvent) post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent) post_save.connect(emit_project_update_event_detail, sender=ProjectUpdateEvent) From 3411389d00eeaed2b5ca178f71f2fe436532296c Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 5 Apr 2018 09:46:38 -0400 Subject: [PATCH 2/5] Added JobTemplate ownership change test --- awx/main/signals.py | 2 ++ .../functional/test_rbac_job_templates.py | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/awx/main/signals.py b/awx/main/signals.py index cc7f7624f4..64f053eb23 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -235,6 +235,7 @@ def cleanup_detached_labels_on_deleted_parent(sender, instance, **kwargs): if l.is_candidate_for_detach(): l.delete() + def set_original_organization(sender, instance, **kwargs): '''set_original_organization is used to set the original, or pre-save organization, so we can later determine if the organization @@ -242,6 +243,7 @@ def set_original_organization(sender, instance, **kwargs): ''' instance.__original_org = instance.organization + def save_related_job_templates(sender, instance, **kwargs): '''save_related_job_templates loops through all of the job templates that use an Inventory or Project that have had their diff --git a/awx/main/tests/functional/test_rbac_job_templates.py b/awx/main/tests/functional/test_rbac_job_templates.py index a12bc31b70..0b9d0c46bd 100644 --- a/awx/main/tests/functional/test_rbac_job_templates.py +++ b/awx/main/tests/functional/test_rbac_job_templates.py @@ -11,6 +11,7 @@ from awx.main.access import ( ScheduleAccess ) from awx.main.models.jobs import JobTemplate +from awx.main.models.organization import Organization from awx.main.models.schedules import Schedule @@ -296,3 +297,30 @@ class TestJobTemplateSchedules: mock_change.return_value = True assert access.can_change(schedule, {'inventory': 42}) mock_change.assert_called_once_with(schedule, {'inventory': 42}) + + +@pytest.mark.django_db +def test_jt_org_ownership_change(user, jt_linked): + admin1 = user('admin1') + org1 = jt_linked.project.organization + org1.admin_role.members.add(admin1) + a1_access = JobTemplateAccess(admin1) + + assert a1_access.can_read(jt_linked) + + + admin2 = user('admin2') + org2 = Organization.objects.create(name='mrroboto', description='domo') + org2.admin_role.members.add(admin2) + a2_access = JobTemplateAccess(admin2) + + assert not a2_access.can_read(jt_linked) + + + jt_linked.project.organization = org2 + jt_linked.project.save() + jt_linked.inventory.organization = org2 + jt_linked.inventory.save() + + assert a2_access.can_read(jt_linked) + assert not a1_access.can_read(jt_linked) From 0bd9919108e636a3d7023abccc039ee900f152cf Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 5 Apr 2018 11:05:48 -0400 Subject: [PATCH 3/5] Make use of callback explicitly for Project and Inventory --- awx/main/signals.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index 64f053eb23..36ad3e21e9 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -250,16 +250,14 @@ def save_related_job_templates(sender, instance, **kwargs): Organization updated. This triggers the rebuilding of the RBAC hierarchy and ensures the proper access restrictions. ''' + if sender not in (Project, Inventory): + raise ValueError('This signal callback is only intended for use with Project or Inventory') + if instance.__original_org != instance.organization: instance.__original_org = instance.organization - jtq = None - if sender == Project: - jtq = JobTemplate.objects.filter(project=instance) - elif sender == Inventory: - jtq = JobTemplate.objects.filter(inventory=instance) - if jtq: - for jt in jtq.all(): - jt.save() + jtq = JobTemplate.objects.filter(**{sender.__name__.lower(): instance}) + for jt in jtq.all(): + jt.save() def connect_computed_field_signals(): From 81fe778676e192240d1a0b33a94cc2192b46a98a Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 6 Apr 2018 13:35:24 -0400 Subject: [PATCH 4/5] Collect roles and update parentage instead of saving JT --- awx/main/signals.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/awx/main/signals.py b/awx/main/signals.py index 36ad3e21e9..f75fa50bf0 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -254,10 +254,22 @@ def save_related_job_templates(sender, instance, **kwargs): raise ValueError('This signal callback is only intended for use with Project or Inventory') if instance.__original_org != instance.organization: - instance.__original_org = instance.organization jtq = JobTemplate.objects.filter(**{sender.__name__.lower(): instance}) - for jt in jtq.all(): - jt.save() + for jt in jtq: + for implicit_role_field in getattr(JobTemplate, '__implicit_role_fields'): + role = getattr(jt, implicit_role_field.name) + original_parents = set(json.loads(role.implicit_parents)) + new_parents = implicit_role_field._resolve_parent_roles(instance) + + role.parents.remove(*list(original_parents - new_parents)) + role.parents.add(*list(new_parents - original_parents)) + + new_parents_list = list(new_parents) + new_parents_list.sort() + new_parents_json = json.dumps(new_parents_list) + if role.implicit_parents != new_parents_json: + role.implicit_parents = new_parents_json + role.save() def connect_computed_field_signals(): From 99fb0fa4cd9b0e11563a3fdd39011210c3b0af74 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 6 Apr 2018 14:49:27 -0400 Subject: [PATCH 5/5] Extract update_role_parentage_for_instance --- awx/main/fields.py | 35 +++++++++++++++++++++-------------- awx/main/signals.py | 20 +++++--------------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/awx/main/fields.py b/awx/main/fields.py index 3d541d524a..a4519610c3 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -48,7 +48,9 @@ from awx.main.models.rbac import batch_role_ancestor_rebuilding, Role from awx.main import utils -__all__ = ['AutoOneToOneField', 'ImplicitRoleField', 'JSONField', 'SmartFilterField'] +__all__ = ['AutoOneToOneField', 'ImplicitRoleField', 'JSONField', + 'SmartFilterField', 'update_role_parentage_for_instance', + 'is_implicit_parent'] # Provide a (better) custom error message for enum jsonschema validation @@ -181,6 +183,23 @@ def is_implicit_parent(parent_role, child_role): return False +def update_role_parentage_for_instance(instance): + '''update_role_parentage_for_instance + updates the parents listing for all the roles + of a given instance if they have changed + ''' + for implicit_role_field in getattr(instance.__class__, '__implicit_role_fields'): + cur_role = getattr(instance, implicit_role_field.name) + new_parents = implicit_role_field._resolve_parent_roles(instance) + cur_role.parents.set(new_parents) + new_parents_list = list(new_parents) + new_parents_list.sort() + new_parents_json = json.dumps(new_parents_list) + if cur_role.implicit_parents != new_parents_json: + cur_role.implicit_parents = new_parents_json + cur_role.save() + + class ImplicitRoleDescriptor(ForwardManyToOneDescriptor): pass @@ -303,19 +322,7 @@ class ImplicitRoleField(models.ForeignKey): type(latest_instance).objects.filter(pk=latest_instance.pk).update(**updates) Role.rebuild_role_ancestor_list(role_ids, []) - # Update parentage if necessary - for implicit_role_field in getattr(latest_instance.__class__, '__implicit_role_fields'): - cur_role = getattr(latest_instance, implicit_role_field.name) - original_parents = set(json.loads(cur_role.implicit_parents)) - new_parents = implicit_role_field._resolve_parent_roles(latest_instance) - cur_role.parents.remove(*list(original_parents - new_parents)) - cur_role.parents.add(*list(new_parents - original_parents)) - new_parents_list = list(new_parents) - new_parents_list.sort() - new_parents_json = json.dumps(new_parents_list) - if cur_role.implicit_parents != new_parents_json: - cur_role.implicit_parents = new_parents_json - cur_role.save() + update_role_parentage_for_instance(latest_instance) instance.refresh_from_db() diff --git a/awx/main/signals.py b/awx/main/signals.py index f75fa50bf0..ea4a5ae927 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -34,7 +34,10 @@ from awx.api.serializers import * # noqa from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore from awx.main.utils import ignore_inventory_computed_fields, ignore_inventory_group_removal, _inventory_updates from awx.main.tasks import update_inventory_computed_fields -from awx.main.fields import is_implicit_parent +from awx.main.fields import ( + is_implicit_parent, + update_role_parentage_for_instance, +) from awx.main import consumers @@ -256,20 +259,7 @@ def save_related_job_templates(sender, instance, **kwargs): if instance.__original_org != instance.organization: jtq = JobTemplate.objects.filter(**{sender.__name__.lower(): instance}) for jt in jtq: - for implicit_role_field in getattr(JobTemplate, '__implicit_role_fields'): - role = getattr(jt, implicit_role_field.name) - original_parents = set(json.loads(role.implicit_parents)) - new_parents = implicit_role_field._resolve_parent_roles(instance) - - role.parents.remove(*list(original_parents - new_parents)) - role.parents.add(*list(new_parents - original_parents)) - - new_parents_list = list(new_parents) - new_parents_list.sort() - new_parents_json = json.dumps(new_parents_list) - if role.implicit_parents != new_parents_json: - role.implicit_parents = new_parents_json - role.save() + update_role_parentage_for_instance(jt) def connect_computed_field_signals():