mirror of
https://github.com/ansible/awx.git
synced 2026-03-07 19:51:08 -03:30
Initial RBAC field and model definitions
This commit is contained in:
@@ -4,8 +4,11 @@
|
|||||||
# Django
|
# Django
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.fields.related import SingleRelatedObjectDescriptor
|
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:
|
# Based on AutoOneToOneField from django-annoying:
|
||||||
# https://bitbucket.org/offline/django-annoying/src/a0de8b294db3/annoying/fields.py
|
# 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):
|
def contribute_to_related_class(self, cls, related):
|
||||||
setattr(cls, related.get_accessor_name(),
|
setattr(cls, related.get_accessor_name(),
|
||||||
AutoSingleRelatedObjectDescriptor(related))
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from awx.main.models.schedules import * # noqa
|
|||||||
from awx.main.models.activity_stream import * # noqa
|
from awx.main.models.activity_stream import * # noqa
|
||||||
from awx.main.models.ha import * # noqa
|
from awx.main.models.ha import * # noqa
|
||||||
from awx.main.models.configuration 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
|
# Monkeypatch Django serializer to ignore django-taggit fields (which break
|
||||||
# the dumpdata command; see https://github.com/alex/django-taggit/issues/155).
|
# the dumpdata command; see https://github.com/alex/django-taggit/issues/155).
|
||||||
|
|||||||
158
awx/main/models/rbac.py
Normal file
158
awx/main/models/rbac.py
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user