mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 19:30:39 -03:30
Add support for following parental changes on save and delete in the RBAC system
This commit is contained in:
parent
c5b7d3363b
commit
e94d441fb0
@ -3,7 +3,11 @@
|
||||
|
||||
# Django
|
||||
from django.db import connection
|
||||
from django.db.models.signals import post_save
|
||||
from django.db.models.signals import (
|
||||
post_init,
|
||||
post_save,
|
||||
post_delete,
|
||||
)
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.db import models
|
||||
from django.db.models.fields.related import (
|
||||
@ -69,7 +73,8 @@ class ResourceFieldDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
raise TransactionManagementError('Current transaction has failed, cannot create implicit resource')
|
||||
resource = Resource.objects.create(content_object=instance)
|
||||
setattr(instance, self.field.name, resource)
|
||||
instance.save(update_fields=[self.field.name,])
|
||||
if instance.pk:
|
||||
instance.save(update_fields=[self.field.name,])
|
||||
return resource
|
||||
|
||||
|
||||
@ -85,12 +90,45 @@ class ImplicitResourceField(models.ForeignKey):
|
||||
def contribute_to_class(self, cls, name):
|
||||
super(ImplicitResourceField, self).contribute_to_class(cls, name)
|
||||
setattr(cls, self.name, ResourceFieldDescriptor(self))
|
||||
post_save.connect(self._save, cls, True)
|
||||
post_save.connect(self._post_save, cls, True)
|
||||
post_delete.connect(self._post_delete, cls, True)
|
||||
|
||||
def _post_save(self, instance, *args, **kwargs):
|
||||
# Ensures our resource object exists and that it's content_object
|
||||
# points back to our hosting instance.
|
||||
this_resource = getattr(instance, self.name)
|
||||
if not this_resource.object_id:
|
||||
this_resource.content_object = instance
|
||||
this_resource.save()
|
||||
|
||||
def _post_delete(self, instance, *args, **kwargs):
|
||||
getattr(instance, self.name).delete()
|
||||
|
||||
|
||||
|
||||
|
||||
def resolve_role_field(obj, field):
|
||||
ret = []
|
||||
|
||||
field_components = field.split('.', 1)
|
||||
if hasattr(obj, field_components[0]):
|
||||
obj = getattr(obj, field_components[0])
|
||||
else:
|
||||
return []
|
||||
|
||||
if len(field_components) == 1:
|
||||
if type(obj) is not ImplicitRoleDescriptor and type(obj) is not Role:
|
||||
raise Exception('%s refers to a %s, not an ImplicitRoleField or Role' % (field, str(type(obj))))
|
||||
ret.append(obj)
|
||||
else:
|
||||
if type(obj) is ManyRelatedObjectsDescriptor:
|
||||
for o in obj.all():
|
||||
ret += resolve_role_field(o, field_components[1])
|
||||
else:
|
||||
ret += resolve_role_field(obj, field_components[1])
|
||||
|
||||
return ret
|
||||
|
||||
def _save(self, instance, *args, **kwargs):
|
||||
# Ensure that our field gets initialized after our first save
|
||||
if not hasattr(instance, self.name):
|
||||
getattr(instance, self.name)
|
||||
|
||||
|
||||
class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
@ -116,27 +154,6 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
|
||||
role = Role.objects.create(name=self.role_name, content_object=instance)
|
||||
if self.parent_role:
|
||||
def resolve_field(obj, field):
|
||||
ret = []
|
||||
|
||||
field_components = field.split('.', 1)
|
||||
if hasattr(obj, field_components[0]):
|
||||
obj = getattr(obj, field_components[0])
|
||||
else:
|
||||
return []
|
||||
|
||||
if len(field_components) == 1:
|
||||
if type(obj) is not ImplicitRoleDescriptor and type(obj) is not Role:
|
||||
raise Exception('%s refers to a %s, not an ImplicitRoleField or Role' % (field, str(type(obj))))
|
||||
ret.append(obj)
|
||||
else:
|
||||
if type(obj) is ManyRelatedObjectsDescriptor:
|
||||
for o in obj.all():
|
||||
ret += resolve_field(o, field_components[1])
|
||||
else:
|
||||
ret += resolve_field(obj, field_components[1])
|
||||
|
||||
return ret
|
||||
|
||||
# Add all non-null parent roles as parents
|
||||
paths = self.parent_role if type(self.parent_role) is list else [self.parent_role]
|
||||
@ -144,11 +161,12 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
if path.startswith("singleton:"):
|
||||
parents = [Role.singleton(path[10:])]
|
||||
else:
|
||||
parents = resolve_field(instance, path)
|
||||
parents = resolve_role_field(instance, path)
|
||||
for parent in parents:
|
||||
role.parents.add(parent)
|
||||
setattr(instance, self.field.name, role)
|
||||
instance.save(update_fields=[self.field.name,])
|
||||
if instance.pk:
|
||||
instance.save(update_fields=[self.field.name,])
|
||||
|
||||
if self.permissions is not None:
|
||||
permissions = RolePermission(
|
||||
@ -198,7 +216,9 @@ class ImplicitRoleField(models.ForeignKey):
|
||||
self
|
||||
)
|
||||
)
|
||||
post_save.connect(self._save, cls, True)
|
||||
post_init.connect(self._post_init, cls, True)
|
||||
post_save.connect(self._post_save, cls, True)
|
||||
post_delete.connect(self._post_delete, cls, True)
|
||||
add_lazy_relation(cls, self, "self", self.bind_m2m_changed)
|
||||
|
||||
def bind_m2m_changed(self, _self, _role_class, cls):
|
||||
@ -263,7 +283,81 @@ class ImplicitRoleField(models.ForeignKey):
|
||||
getattr(instance, self.name).parents.remove(getattr(obj, self.m2m_field_attr))
|
||||
|
||||
|
||||
def _save(self, instance, *args, **kwargs):
|
||||
def _post_init(self, instance, *args, **kwargs):
|
||||
if not self.parent_role:
|
||||
return
|
||||
#if not hasattr(instance, self.name):
|
||||
# getattr(instance, self.name)
|
||||
|
||||
if not hasattr(self, '__original_parent_roles'):
|
||||
paths = self.parent_role if type(self.parent_role) is list else [self.parent_role]
|
||||
all_parents = set()
|
||||
for path in paths:
|
||||
if path.startswith("singleton:"):
|
||||
parents = [Role.singleton(path[10:])]
|
||||
else:
|
||||
parents = resolve_role_field(instance, path)
|
||||
for parent in parents:
|
||||
all_parents.add(parent)
|
||||
#role.parents.add(parent)
|
||||
self.__original_parent_roles = all_parents
|
||||
|
||||
'''
|
||||
field_names = self.parent_role
|
||||
if type(field_names) is not list:
|
||||
field_names = [field_names]
|
||||
self.__original_values = {}
|
||||
for field_name in field_names:
|
||||
if field_name.startswith('singleton:'):
|
||||
continue
|
||||
first_field_name = field_name.split('.')[0]
|
||||
self.__original_values[first_field_name] = getattr(instance, first_field_name)
|
||||
'''
|
||||
else:
|
||||
print('WE DO NEED THIS')
|
||||
pass
|
||||
|
||||
def _post_save(self, instance, *args, **kwargs):
|
||||
# Ensure that our field gets initialized after our first save
|
||||
if not hasattr(instance, self.name):
|
||||
getattr(instance, self.name)
|
||||
this_role = getattr(instance, self.name)
|
||||
if not this_role.object_id:
|
||||
# Ensure our ref back to our instance is set. This will not be set the
|
||||
# first time the object is saved because we create the role in our _post_init
|
||||
# but that happens before an id for the instance has been set (because it
|
||||
# hasn't been saved yet!). Now that everything has an id, we patch things
|
||||
# so the role references the instance.
|
||||
this_role.content_object = instance
|
||||
this_role.save()
|
||||
|
||||
# As object relations change, the role hierarchy might also change if the relations
|
||||
# that changed were referenced in our magic parent_role field. This code synchronizes
|
||||
# these changes.
|
||||
if not self.parent_role:
|
||||
return
|
||||
|
||||
paths = self.parent_role if type(self.parent_role) is list else [self.parent_role]
|
||||
original_parents = self.__original_parent_roles
|
||||
new_parents = set()
|
||||
for path in paths:
|
||||
if path.startswith("singleton:"):
|
||||
parents = [Role.singleton(path[10:])]
|
||||
else:
|
||||
parents = resolve_role_field(instance, path)
|
||||
for parent in parents:
|
||||
new_parents.add(parent)
|
||||
|
||||
Role.pause_role_ancestor_rebuilding()
|
||||
for role in original_parents - new_parents:
|
||||
this_role.parents.remove(role)
|
||||
for role in new_parents - original_parents:
|
||||
this_role.parents.add(role)
|
||||
Role.unpause_role_ancestor_rebuilding()
|
||||
|
||||
self.__original_parent_roles = new_parents
|
||||
|
||||
def _post_delete(self, instance, *args, **kwargs):
|
||||
this_role = getattr(instance, self.name)
|
||||
children = [c for c in this_role.children.all()]
|
||||
this_role.delete()
|
||||
for child in children:
|
||||
children.rebuild_role_ancestor_list()
|
||||
|
||||
@ -15,13 +15,23 @@ from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
# AWX
|
||||
from awx.main.models.base import * # noqa
|
||||
|
||||
__all__ = ['Role', 'RolePermission', 'Resource', 'ROLE_SINGLETON_SYSTEM_ADMINISTRATOR', 'ROLE_SINGLETON_SYSTEM_AUDITOR']
|
||||
__all__ = [
|
||||
'Role',
|
||||
'RolePermission',
|
||||
'Resource',
|
||||
'ROLE_SINGLETON_SYSTEM_ADMINISTRATOR',
|
||||
'ROLE_SINGLETON_SYSTEM_AUDITOR',
|
||||
]
|
||||
|
||||
logger = logging.getLogger('awx.main.models.rbac')
|
||||
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='System Administrator'
|
||||
ROLE_SINGLETON_SYSTEM_AUDITOR='System Auditor'
|
||||
|
||||
role_rebuilding_paused = False
|
||||
roles_needing_rebuilding = set()
|
||||
|
||||
|
||||
|
||||
class Role(CommonModelNameNotUnique):
|
||||
'''
|
||||
@ -48,6 +58,36 @@ class Role(CommonModelNameNotUnique):
|
||||
def get_absolute_url(self):
|
||||
return reverse('api:role_detail', args=(self.pk,))
|
||||
|
||||
@staticmethod
|
||||
def pause_role_ancestor_rebuilding():
|
||||
'''
|
||||
Pauses role ancestor list updating. This is useful when you're making
|
||||
many changes to the same roles, for example doing bulk inserts or
|
||||
making many changes to the same object in succession.
|
||||
|
||||
Note that the unpause_role_ancestor_rebuilding MUST be called within
|
||||
the same execution context (preferably within the same transaction),
|
||||
otherwise the RBAC role ancestor hierarchy will not be properly
|
||||
updated.
|
||||
'''
|
||||
|
||||
global role_rebuilding_paused
|
||||
role_rebuilding_paused = True
|
||||
|
||||
@staticmethod
|
||||
def unpause_role_ancestor_rebuilding():
|
||||
'''
|
||||
Unpauses the role ancestor list updating. This will will rebuild all
|
||||
roles that need updating since the last call to
|
||||
pause_role_ancestor_rebuilding and bring everything back into sync.
|
||||
'''
|
||||
global role_rebuilding_paused
|
||||
global roles_needing_rebuilding
|
||||
role_rebuilding_paused = False
|
||||
for role in Role.objects.filter(id__in=list(roles_needing_rebuilding)).all():
|
||||
role.rebuild_role_ancestor_list()
|
||||
roles_needing_rebuilding = set()
|
||||
|
||||
def rebuild_role_ancestor_list(self):
|
||||
'''
|
||||
Updates our `ancestors` map to accurately reflect all of the ancestors for a role
|
||||
@ -57,6 +97,11 @@ class Role(CommonModelNameNotUnique):
|
||||
|
||||
Note that this method relies on any parents' ancestor list being correct.
|
||||
'''
|
||||
global role_rebuilding_paused, roles_needing_rebuilding
|
||||
|
||||
if role_rebuilding_paused:
|
||||
roles_needing_rebuilding.add(self.id)
|
||||
return
|
||||
|
||||
actual_ancestors = set(Role.objects.filter(id=self.id).values_list('parents__ancestors__id', flat=True))
|
||||
actual_ancestors.add(self.id)
|
||||
@ -67,9 +112,9 @@ class Role(CommonModelNameNotUnique):
|
||||
# If it differs, update, and then update all of our children
|
||||
if actual_ancestors != stored_ancestors:
|
||||
for id in actual_ancestors - stored_ancestors:
|
||||
self.ancestors.add(Role.objects.get(id=id))
|
||||
self.ancestors.add(id)
|
||||
for id in stored_ancestors - actual_ancestors:
|
||||
self.ancestors.remove(Role.objects.get(id=id))
|
||||
self.ancestors.remove(id)
|
||||
|
||||
for child in self.children.all():
|
||||
child.rebuild_role_ancestor_list()
|
||||
|
||||
@ -2,6 +2,7 @@ import pytest
|
||||
|
||||
from awx.main.models import (
|
||||
Role,
|
||||
Resource,
|
||||
Organization,
|
||||
)
|
||||
|
||||
@ -90,14 +91,50 @@ def test_auto_m2m_adjuments(organization, project, alice):
|
||||
assert project.accessible_by(alice, {'read': True}) is True
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.skipif(True, reason='Unimplemented')
|
||||
def test_auto_field_adjuments(organization, inventory, team, alice):
|
||||
'Ensures the auto role reparenting is working correctly through m2m maps'
|
||||
'Ensures the auto role reparenting is working correctly through non m2m fields'
|
||||
org2 = Organization.objects.create(name='Org 2', description='org 2')
|
||||
org2.admin_role.members.add(alice)
|
||||
assert inventory.accessible_by(alice, {'read': True}) is False
|
||||
inventory.organization = org2
|
||||
inventory.save()
|
||||
assert inventory.accessible_by(alice, {'read': True}) is True
|
||||
inventory.organization = organization
|
||||
inventory.save()
|
||||
assert inventory.accessible_by(alice, {'read': True}) is False
|
||||
#assert False
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_implicit_deletes(alice):
|
||||
'Ensures implicit resources and roles delete themselves'
|
||||
delorg = Organization.objects.create(name='test-org')
|
||||
delorg.admin_role.members.add(alice)
|
||||
|
||||
resource_id = delorg.resource.id
|
||||
admin_role_id = delorg.admin_role.id
|
||||
auditor_role_id = delorg.auditor_role.id
|
||||
|
||||
assert Role.objects.filter(id=admin_role_id).count() == 1
|
||||
assert Role.objects.filter(id=auditor_role_id).count() == 1
|
||||
assert Resource.objects.filter(id=resource_id).count() == 1
|
||||
n_alice_roles = alice.roles.count()
|
||||
n_system_admin_children = Role.singleton('System Administrator').children.count()
|
||||
|
||||
delorg.delete()
|
||||
|
||||
assert Role.objects.filter(id=admin_role_id).count() == 0
|
||||
assert Role.objects.filter(id=auditor_role_id).count() == 0
|
||||
assert Resource.objects.filter(id=resource_id).count() == 0
|
||||
assert alice.roles.count() == (n_alice_roles - 1)
|
||||
assert Role.singleton('System Administrator').children.count() == (n_system_admin_children - 1)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_content_object(user):
|
||||
'Ensure our conent_object stuf seems to be working'
|
||||
|
||||
print('Creating organization')
|
||||
org = Organization.objects.create(name='test-org')
|
||||
print('Organizaiton id: %d resource: %d admin_role: %d' % (org.id, org.resource.id, org.admin_role.id))
|
||||
assert org.resource.content_object.id == org.id
|
||||
assert org.admin_role.content_object.id == org.id
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user