diff --git a/awx/main/fields.py b/awx/main/fields.py index e002ab74c9..69a5cfa089 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -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 diff --git a/awx/main/signals.py b/awx/main/signals.py index 302db7e60b..f5778dbb2e 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -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.