diff --git a/awx/main/fields.py b/awx/main/fields.py index b86fef5095..7d903d1278 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -46,8 +46,10 @@ class AutoOneToOneField(models.OneToOneField): def resolve_field(obj, field): for f in field.split('.'): - if obj: + if hasattr(obj, f): obj = getattr(obj, f) + else: + obj = None return obj class ResourceFieldDescriptor(ReverseSingleRelatedObjectDescriptor): diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index e33cec1a23..adaca41184 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -544,23 +544,27 @@ class Group(CommonModelNameNotUnique, ResourceMixin): ) admin_role = ImplicitRoleField( role_name='Inventory Group Administrator', - parent_role='inventory.admin_role', + parent_role=['inventory.admin_role', 'parents.admin_role'], resource_field='resource', permissions = {'all': True} ) auditor_role = ImplicitRoleField( role_name='Inventory Group Auditor', - parent_role='inventory.auditor_role', + parent_role=['inventory.auditor_role', 'parents.auditor_role'], resource_field='resource', permissions = {'read': True} ) updater_role = ImplicitRoleField( role_name='Inventory Group Updater', - parent_role='inventory.updater_role' + parent_role=['inventory.updater_role', 'parents.updater_role'], + resource_field='resource', + permissions = {'read': True, 'write': True, 'create': True, 'use': True}, ) executor_role = ImplicitRoleField( role_name='Inventory Group Executor', - parent_role='inventory.executor_role' + parent_role=['inventory.executor_role', 'parents.executor_role'], + resource_field='resource', + permissions = {'read':True, 'execute':True}, ) def __unicode__(self): diff --git a/awx/main/signals.py b/awx/main/signals.py index d5f07170ab..302db7e60b 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -18,6 +18,7 @@ from crum.signals import current_user_getter # AWX from awx.main.models import * # noqa from awx.api.serializers import * # noqa +from awx.main.fields import ImplicitRoleField from awx.main.utils import model_instance_diff, model_to_dict, camelcase_to_underscore, emit_websocket_notification from awx.main.utils import ignore_inventory_computed_fields, ignore_inventory_group_removal, _inventory_updates from awx.main.tasks import update_inventory_computed_fields @@ -122,6 +123,31 @@ def rebuild_role_hierarchy_cache(sender, reverse, model, pk_set, **kwargs): else: kwargs['instance'].rebuild_role_hierarchy_cache() +def rebuild_group_parent_roles(instance, action, reverse, **kwargs): + objects = [] + if reverse: + objects = instance.children.all() + else: + objects = instance.parents.all() + + for obj in objects: + fields = [f for f in instance._meta.get_fields() if type(f) is ImplicitRoleField] + for field in fields: + role = None + if reverse: + if hasattr(obj, field.name): + parent_role = getattr(instance, field.name) + role = getattr(obj, field.name) + else: + role = getattr(instance, field.name) + parent_role = getattr(obj, field.name) + + if role: + if action == 'post_add': + role.parents.add(parent_role) + elif action == 'pre_remove': + role.parents.remove(parent_role) + pre_save.connect(store_initial_active_state, 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) @@ -141,7 +167,7 @@ post_delete.connect(emit_update_inventory_on_created_or_deleted, sender=Job) 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_hierarchy_cache, Role.parents.through) - +m2m_changed.connect(rebuild_group_parent_roles, Group.parents.through) # Migrate hosts, groups to parent group(s) whenever a group is deleted or # marked as inactive. diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index db4143f13d..ca30f8315e 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -1,7 +1,10 @@ import pytest from awx.main.models.credential import Credential -from awx.main.models.inventory import Inventory +from awx.main.models.inventory import ( + Inventory, + Group, +) from awx.main.models.projects import Project from awx.main.models.organization import ( Organization, @@ -45,6 +48,12 @@ def credential(): def inventory(organization): return Inventory.objects.create(name="test-inventory", organization=organization) +@pytest.fixture +def group(inventory): + def g(name): + return Group.objects.create(inventory=inventory, name=name) + return g + @pytest.fixture def permissions(): return { diff --git a/awx/main/tests/functional/test_rbac_inventory.py b/awx/main/tests/functional/test_rbac_inventory.py index 3d15584afd..8834545140 100644 --- a/awx/main/tests/functional/test_rbac_inventory.py +++ b/awx/main/tests/functional/test_rbac_inventory.py @@ -172,3 +172,26 @@ def test_inventory_executor(inventory, permissions, user, team): assert team.member_role.is_ancestor_of(inventory.updater_role) is False assert team.member_role.is_ancestor_of(inventory.executor_role) +@pytest.mark.django_db +def test_group_parent_admin(group, permissions, user): + u = user('admin', False) + parent1 = group('parent-1') + parent2 = group('parent-2') + childA = group('child-1') + + parent1.admin_role.members.add(u) + assert parent1.accessible_by(u, permissions['admin']) + assert not parent2.accessible_by(u, permissions['admin']) + assert not childA.accessible_by(u, permissions['admin']) + + childA.parents.add(parent1) + assert childA.accessible_by(u, permissions['admin']) + + childA.parents.remove(parent1) + assert not childA.accessible_by(u, permissions['admin']) + + parent2.children.add(childA) + assert not childA.accessible_by(u, permissions['admin']) + + parent2.admin_role.members.add(u) + assert childA.accessible_by(u, permissions['admin'])