Merge pull request #1250 from wwitzel3/fix-1228

Update role hierarchy when a JobTemplate moves orgs.
This commit is contained in:
Wayne Witzel III
2018-04-06 15:31:23 -04:00
committed by GitHub
3 changed files with 87 additions and 17 deletions

View File

@@ -48,7 +48,9 @@ from awx.main.models.rbac import batch_role_ancestor_rebuilding, Role
from awx.main import utils 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 # Provide a (better) custom error message for enum jsonschema validation
@@ -181,6 +183,23 @@ def is_implicit_parent(parent_role, child_role):
return False 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): class ImplicitRoleDescriptor(ForwardManyToOneDescriptor):
pass pass
@@ -303,19 +322,7 @@ class ImplicitRoleField(models.ForeignKey):
type(latest_instance).objects.filter(pk=latest_instance.pk).update(**updates) type(latest_instance).objects.filter(pk=latest_instance.pk).update(**updates)
Role.rebuild_role_ancestor_list(role_ids, []) Role.rebuild_role_ancestor_list(role_ids, [])
# Update parentage if necessary update_role_parentage_for_instance(latest_instance)
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()
instance.refresh_from_db() instance.refresh_from_db()

View File

@@ -9,7 +9,13 @@ import json
# Django # Django
from django.conf import settings 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.dispatch import receiver
from django.contrib.auth import SESSION_KEY from django.contrib.auth import SESSION_KEY
from django.utils import timezone from django.utils import timezone
@@ -28,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 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.utils import ignore_inventory_computed_fields, ignore_inventory_group_removal, _inventory_updates
from awx.main.tasks import update_inventory_computed_fields 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 from awx.main import consumers
@@ -230,6 +239,29 @@ def cleanup_detached_labels_on_deleted_parent(sender, instance, **kwargs):
l.delete() 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 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:
jtq = JobTemplate.objects.filter(**{sender.__name__.lower(): instance})
for jt in jtq:
update_role_parentage_for_instance(jt)
def connect_computed_field_signals(): def connect_computed_field_signals():
post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host) post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Host) post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Host)
@@ -247,7 +279,10 @@ def connect_computed_field_signals():
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_job_event_detail, sender=JobEvent)
post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent) post_save.connect(emit_ad_hoc_command_event_detail, sender=AdHocCommandEvent)
post_save.connect(emit_project_update_event_detail, sender=ProjectUpdateEvent) post_save.connect(emit_project_update_event_detail, sender=ProjectUpdateEvent)

View File

@@ -11,6 +11,7 @@ from awx.main.access import (
ScheduleAccess ScheduleAccess
) )
from awx.main.models.jobs import JobTemplate from awx.main.models.jobs import JobTemplate
from awx.main.models.organization import Organization
from awx.main.models.schedules import Schedule from awx.main.models.schedules import Schedule
@@ -296,3 +297,30 @@ class TestJobTemplateSchedules:
mock_change.return_value = True mock_change.return_value = True
assert access.can_change(schedule, {'inventory': 42}) assert access.can_change(schedule, {'inventory': 42})
mock_change.assert_called_once_with(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)