mirror of
https://github.com/ansible/awx.git
synced 2026-01-19 21:51:26 -03:30
Generically handle automatic role rebinding through m2m relations
This commit is contained in:
parent
9a3ef6b998
commit
72419f7eb9
@ -3,17 +3,26 @@
|
||||
|
||||
# Django
|
||||
from django.db.models.signals import post_save
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.db import models
|
||||
from django.db.models.fields.related import SingleRelatedObjectDescriptor
|
||||
from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor
|
||||
from django.db.models.fields.related import (
|
||||
add_lazy_relation,
|
||||
SingleRelatedObjectDescriptor,
|
||||
ReverseSingleRelatedObjectDescriptor,
|
||||
ManyRelatedObjectsDescriptor,
|
||||
ReverseManyRelatedObjectsDescriptor,
|
||||
)
|
||||
|
||||
from django.core.exceptions import FieldError
|
||||
|
||||
|
||||
# AWX
|
||||
from awx.main.models.rbac import Resource, RolePermission, Role
|
||||
|
||||
|
||||
__all__ = ['AutoOneToOneField', 'ImplicitResourceField', 'ImplicitRoleField']
|
||||
|
||||
|
||||
# Based on AutoOneToOneField from django-annoying:
|
||||
# https://bitbucket.org/offline/django-annoying/src/a0de8b294db3/annoying/fields.py
|
||||
|
||||
@ -44,14 +53,6 @@ class AutoOneToOneField(models.OneToOneField):
|
||||
|
||||
|
||||
|
||||
def resolve_field(obj, field):
|
||||
for f in field.split('.'):
|
||||
if hasattr(obj, f):
|
||||
obj = getattr(obj, f)
|
||||
else:
|
||||
obj = None
|
||||
return obj
|
||||
|
||||
class ResourceFieldDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
"""Descriptor for access to the object from its related class."""
|
||||
|
||||
@ -62,7 +63,7 @@ class ResourceFieldDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
resource = super(ResourceFieldDescriptor, self).__get__(instance, instance_type)
|
||||
if resource:
|
||||
return resource
|
||||
resource = Resource._default_manager.create()
|
||||
resource = Resource._default_manager.create(content_object=instance)
|
||||
setattr(instance, self.field.name, resource)
|
||||
instance.save(update_fields=[self.field.name,])
|
||||
return resource
|
||||
@ -79,7 +80,7 @@ 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.parent_resource, self))
|
||||
setattr(cls, self.name, ResourceFieldDescriptor(self))
|
||||
post_save.connect(self._save, cls, True)
|
||||
|
||||
def _save(self, instance, *args, **kwargs):
|
||||
@ -106,23 +107,38 @@ class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor):
|
||||
if not self.role_name:
|
||||
raise FieldError('Implicit role missing `role_name`')
|
||||
|
||||
role = Role._default_manager.create(name=self.role_name)
|
||||
role = Role._default_manager.create(name=self.role_name, content_object=instance)
|
||||
if self.parent_role:
|
||||
# Add all non-null parent roles as parents
|
||||
if type(self.parent_role) is list:
|
||||
for path in self.parent_role:
|
||||
if path.startswith("singleton:"):
|
||||
parent = Role.singleton(path[10:])
|
||||
else:
|
||||
parent = resolve_field(instance, path)
|
||||
if parent:
|
||||
role.parents.add(parent)
|
||||
else:
|
||||
if self.parent_role.startswith("singleton:"):
|
||||
parent = Role.singleton(self.parent_role[10:])
|
||||
def resolve_field(obj, field):
|
||||
ret = []
|
||||
|
||||
field_components = field.split('.', 1)
|
||||
if hasattr(obj, field_components[0]):
|
||||
obj = getattr(obj, field_components[0])
|
||||
else:
|
||||
parent = resolve_field(instance, self.parent_role)
|
||||
if parent:
|
||||
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]
|
||||
for path in paths:
|
||||
if path.startswith("singleton:"):
|
||||
parents = [Role.singleton(path[10:])]
|
||||
else:
|
||||
parents = resolve_field(instance, path)
|
||||
for parent in parents:
|
||||
role.parents.add(parent)
|
||||
setattr(instance, self.field.name, role)
|
||||
instance.save(update_fields=[self.field.name,])
|
||||
@ -178,6 +194,65 @@ class ImplicitRoleField(models.ForeignKey):
|
||||
)
|
||||
)
|
||||
post_save.connect(self._save, cls, True)
|
||||
add_lazy_relation(cls, self, "self", self.bind_m2m_changed)
|
||||
|
||||
def bind_m2m_changed(self, _self, _role_class, cls):
|
||||
if not self.parent_role:
|
||||
return
|
||||
field_names = self.parent_role
|
||||
if type(field_names) is not list:
|
||||
field_names = [field_names]
|
||||
|
||||
found_m2m_field = False
|
||||
for field_name in field_names:
|
||||
if field_name.startswith('singleton:'):
|
||||
continue
|
||||
|
||||
first_field_name = field_name.split('.')[0]
|
||||
field = getattr(cls, first_field_name)
|
||||
|
||||
if type(field) is ReverseManyRelatedObjectsDescriptor:
|
||||
if found_m2m_field:
|
||||
# This limitation is due to a lack of understanding on my part, the
|
||||
# trouble being that I can't seem to get m2m_changed to call anything that
|
||||
# encapsulates what field we're working with. So the workaround that I've
|
||||
# settled on for the time being is to simply iterate over the fields in the
|
||||
# m2m_update and look for the m2m map and update accordingly. This solution
|
||||
# is lame, I'd love for someone to show me the way to get the necessary
|
||||
# state down to the m2m_update callback. - anoek 2016-02-10
|
||||
raise Exception('Multiple ManyToMany fields not allowed in parent_role list')
|
||||
if len(field_name.split('.')) != 2:
|
||||
raise Exception('Referencing deep roles through ManyToMany fields is unsupported.')
|
||||
|
||||
found_m2m_field = True
|
||||
self.m2m_field_name = first_field_name
|
||||
self.m2m_field_attr = field_name.split('.',1)[1]
|
||||
m2m_changed.connect(self.m2m_update, field.through)
|
||||
|
||||
if type(field) is ManyRelatedObjectsDescriptor:
|
||||
raise Exception('ManyRelatedObjectsDescriptor references are currently unsupported ' +
|
||||
'(but the reverse is, so supporting this is probably easy to add)): %s.%s' %
|
||||
(cls.__name__, first_field_name))
|
||||
|
||||
|
||||
def m2m_update(self, sender, instance, action, reverse, model, pk_set, **kwargs):
|
||||
if action == 'post_add' or action == 'pre_remove':
|
||||
if reverse:
|
||||
for pk in pk_set:
|
||||
obj = model.objects.get(pk=pk)
|
||||
if action == 'post_add':
|
||||
getattr(instance, self.name).children.add(getattr(obj, self.m2m_field_attr))
|
||||
if action == 'pre_remove':
|
||||
getattr(instance, self.name).children.remove(getattr(obj, self.m2m_field_attr))
|
||||
|
||||
else:
|
||||
for pk in pk_set:
|
||||
obj = model.objects.get(pk=pk)
|
||||
if action == 'post_add':
|
||||
getattr(instance, self.name).parents.add(getattr(obj, self.m2m_field_attr))
|
||||
if action == 'pre_remove':
|
||||
getattr(instance, self.name).parents.remove(getattr(obj, self.m2m_field_attr))
|
||||
|
||||
|
||||
def _save(self, instance, *args, **kwargs):
|
||||
# Ensure that our field gets initialized after our first save
|
||||
|
||||
@ -18,7 +18,6 @@ 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
|
||||
@ -116,37 +115,13 @@ def store_initial_active_state(sender, **kwargs):
|
||||
else:
|
||||
instance._saved_active_state = True
|
||||
|
||||
def rebuild_role_hierarchy_cache(sender, reverse, model, pk_set, **kwargs):
|
||||
def rebuild_role_ancestor_list(sender, reverse, model, instance, pk_set, **kwargs):
|
||||
if reverse:
|
||||
for id in pk_set:
|
||||
model.objects.get(id=id).rebuild_role_hierarchy_cache()
|
||||
model.objects.get(id=id).rebuild_role_ancestor_list()
|
||||
else:
|
||||
kwargs['instance'].rebuild_role_hierarchy_cache()
|
||||
instance.rebuild_role_ancestor_list()
|
||||
|
||||
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)
|
||||
@ -166,8 +141,8 @@ post_save.connect(emit_update_inventory_on_created_or_deleted, sender=Job)
|
||||
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)
|
||||
m2m_changed.connect(rebuild_role_ancestor_list, 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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user