diff --git a/awx/main/fields.py b/awx/main/fields.py index 85c8c4ff5f..545a773108 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -4,8 +4,11 @@ # Django from django.db import models from django.db.models.fields.related import SingleRelatedObjectDescriptor +from django.db.models.fields.related import ForeignRelatedObjectsDescriptor +from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor -__all__ = ['AutoOneToOneField'] + +__all__ = ['AutoOneToOneField', 'ImplicitResourceField', 'ImplicitRoleField'] # Based on AutoOneToOneField from django-annoying: # https://bitbucket.org/offline/django-annoying/src/a0de8b294db3/annoying/fields.py @@ -32,3 +35,117 @@ class AutoOneToOneField(models.OneToOneField): def contribute_to_related_class(self, cls, related): setattr(cls, related.get_accessor_name(), AutoSingleRelatedObjectDescriptor(related)) + + + + + +def resolve_field(obj, field): + for f in field.split('.'): + obj = getattr(obj, f) + return obj + +class ResourceFieldDescriptor(ReverseSingleRelatedObjectDescriptor): + """Descriptor for access to the object from its related class.""" + + def __init__(self, parent_resource, *args, **kwargs): + self.parent_resource = parent_resource + super(ResourceFieldDescriptor, self).__init__(*args, **kwargs) + + def __get__(self, instance, instance_type=None): + resource = super(ResourceFieldDescriptor, self).__get__(instance, instance_type) + if resource: + return resource + resource = Resource._default_manager.create() + if self.parent_resource: + resource.parent = resolve_field(instance, self.parent_resource) + resource.save() + setattr(instance, self.field.name, resource) + instance.save(update_fields=[self.field.name,]) + return resource; + + +class ImplicitResourceField(models.ForeignKey): + """Creates an associated resource object if one doesn't already exist""" + + def __init__(self, parent_resource=None, *args, **kwargs): + self.parent_resource = parent_resource + kwargs.setdefault('to', 'Resource') + kwargs.setdefault('related_name', '+') + kwargs.setdefault('null', 'True') + super(ImplicitResourceField, self).__init__(*args, **kwargs) + + def contribute_to_class(self, cls, name): + super(ImplicitResourceField, self).contribute_to_class(cls, name); + setattr(cls, self.name, ResourceFieldDescriptor(self.parent_resource, self)) + + + +class ImplicitRoleDescriptor(ReverseSingleRelatedObjectDescriptor): + """Descriptor Implict Role Fields. Auto-creates the appropriate role entry on first access""" + + def __init__(self, role_name, resource_field, permissions, parent_role, *args, **kwargs): + self.role_name = role_name + self.resource_field = resource_field + self.permissions = permissions + self.parent_role = parent_role + + super(ImplicitRoleDescriptor, self).__init__(*args, **kwargs) + + def __get__(self, instance, instance_type=None): + role = super(ImplicitRoleDescriptor, self).__get__(instance, instance_type) + if role: + return role + + if not self.role_name: + raise FieldError('Implicit role missing `role_name`') + if not self.resource_field: + raise FieldError('Implicit role missing `resource_field` specification') + if not self.permissions: + raise FieldError('Implicit role missing `permissions`') + + role = Role._default_manager.create(name=self.role_name) + role.save() + if self.parent_role: + role.parents.add(resolve_field(instance, self.parent_role)) + setattr(instance, self.field.name, role) + instance.save(update_fields=[self.field.name,]) + + permissions = RolePermission( + role=role, + resource=getattr(instance, self.resource_field) + ) + for k,v in self.permissions.items(): + setattr(permissions, k, v) + permissions.save() + + return role; + + +class ImplicitRoleField(models.ForeignKey): + """Implicitly creates a role entry for a resource""" + + def __init__(self, role_name=None, resource_field=None, permissions=None, parent_role=None, *args, **kwargs): + self.role_name = role_name + self.resource_field = resource_field + self.permissions = permissions + self.parent_role = parent_role + + kwargs.setdefault('to', 'Role') + kwargs.setdefault('related_name', '+') + kwargs.setdefault('null', 'True') + super(ImplicitRoleField, self).__init__(*args, **kwargs) + + def contribute_to_class(self, cls, name): + super(ImplicitRoleField, self).contribute_to_class(cls, name); + setattr(cls, + self.name, + ImplicitRoleDescriptor( + self.role_name, + self.resource_field, + self.permissions, + self.parent_role, + self + ) + ) + diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 23cf591e6b..ce01f5f51e 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -17,6 +17,7 @@ from awx.main.models.schedules import * # noqa from awx.main.models.activity_stream import * # noqa from awx.main.models.ha import * # noqa from awx.main.models.configuration import * # noqa +from awx.main.models.rbac import * # noqa # Monkeypatch Django serializer to ignore django-taggit fields (which break # the dumpdata command; see https://github.com/alex/django-taggit/issues/155). diff --git a/awx/main/models/rbac.py b/awx/main/models/rbac.py new file mode 100644 index 0000000000..73deeaf303 --- /dev/null +++ b/awx/main/models/rbac.py @@ -0,0 +1,158 @@ +# Copyright (c) 2016 Ansible, Inc. +# All Rights Reserved. + +# Python +import logging + +# Django +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import FieldError + +# AWX +from awx.main.models.base import * # noqa +from awx.main.fields import * # noqa + +__all__ = ['Role', 'RolePermission', 'Resource'] + +logger = logging.getLogger('awx.main.models.rbac') + + + +class Role(CommonModelNameNotUnique): + ''' + Role model + ''' + + class Meta: + app_label = 'main' + verbose_name_plural = _('roles') + db_table = 'main_rbac_roles' + + parents = models.ManyToManyField('Role', related_name='children') + members = models.ManyToManyField('auth.User', related_name='roles') + + def save(self, *args, **kwargs): + super(Role, self).save(*args, **kwargs) + self.rebuild_role_hierarchy_cache() + + def rebuild_role_hierarchy_cache(self): + 'Rebuilds the associated entries in the RoleHierarchy model' + + # Compute what our hierarchy should be. (Note: this depends on our + # parent's cached hierarchy being correct) + parent_ids = set([parent.id for parent in self.parents.all()]) + actual_ancestors = set([r.ancestor.id for r in RoleHierarchy.objects.filter(role__id__in=parent_ids)]) + actual_ancestors.add(self.id) + + # Compute what we have stored + stored_ancestors = set([r.ancestor.id for r in RoleHierarchy.objects.filter(role__id=self.id)]) + + # If it differs, update, and then update all of our children + if actual_ancestors != stored_ancestors: + RoleHierarchy.objects.filter(role__id=self.id).delete() + for id in actual_ancestors: + rh = RoleHierarchy(role=self, ancestor=Role.objects.get(id=id)) + rh.save() + for child in self.children.all(): + child.rebuild_role_hierarchy_cache(); + + +class RoleHierarchy(CreatedModifiedModel): + ''' + Stores a flattened relation map of all roles in the system for easy joining + ''' + + class Meta: + app_label = 'main' + verbose_name_plural = _('role_ancestors') + db_table = 'main_rbac_role_hierarchy' + + role = models.ForeignKey('Role', related_name='+', on_delete=models.CASCADE) + ancestor = models.ForeignKey('Role', related_name='+', on_delete=models.CASCADE) + + +class Resource(CommonModelNameNotUnique): + ''' + Role model + ''' + + class Meta: + app_label = 'main' + verbose_name_plural = _('resources') + db_table = 'main_rbac_resources' + + parent = models.ForeignKey('Resource', related_name='children', null=True, default=None) + + def save(self, *args, **kwargs): + super(Resource, self).save(*args, **kwargs) + self.rebuild_resource_hierarchy_cache() + + def rebuild_resource_hierarchy_cache(self): + 'Rebuilds the associated entries in the ResourceHierarchy model' + + # Compute what our hierarchy should be. (Note: this depends on our + # parent's cached hierarchy being correct) + actual_ancestors = set() + if self.parent: + actual_ancestors = set([r.ancestor.id for r in ResourceHierarchy.objects.filter(resource__id=self.parent.id)]) + actual_ancestors.add(self.id) + + # Compute what we have stored + stored_ancestors = set([r.ancestor.id for r in ResourceHierarchy.objects.filter(resource__id=self.id)]) + + # If it differs, update, and then update all of our children + if actual_ancestors != stored_ancestors: + ResourceHierarchy.objects.filter(resource__id=self.id).delete() + for id in actual_ancestors: + rh = ResourceHierarchy(resource=self, ancestor=Resource.objects.get(id=id)) + rh.save() + for child in self.children.all(): + child.rebuild_resource_hierarchy_cache(); + + + +class ResourceHierarchy(CreatedModifiedModel): + ''' + Stores a flattened relation map of all resources in the system for easy joining + ''' + + class Meta: + app_label = 'main' + verbose_name_plural = _('resource_ancestors') + db_table = 'main_rbac_resource_hierarchy' + + resource = models.ForeignKey('Resource', related_name='+', on_delete=models.CASCADE) + ancestor = models.ForeignKey('Resource', related_name='+', on_delete=models.CASCADE) + + +class RolePermission(CreatedModifiedModel): + ''' + Defines the permissions a role has + ''' + + class Meta: + app_label = 'main' + verbose_name_plural = _('permissions') + db_table = 'main_rbac_permissions' + + role = models.ForeignKey( + Role, + null=False, + on_delete=models.CASCADE, + related_name='permissions', + ) + resource = models.ForeignKey( + Resource, + null=False, + on_delete=models.CASCADE, + related_name='permissions', + ) + create = models.BooleanField(default = False) + read = models.BooleanField(default = False) + write = models.BooleanField(default = False) + update = models.BooleanField(default = False) + delete = models.BooleanField(default = False) + scm_update = models.BooleanField(default = False) + +