ORMified RBAC classes; Added GenericForeignKey backref for convenience

The RoleHierarchy table has been eliminated in favor of just using
a ManyToMany map, which is what we should have been using all along.

ORMifications still need improvement, in particular filtering on
ResourceMixin.accessible_by should reduce permission calculation
overhead, but with the current implemenation this is not true.
ResourceMixin.get_permission performs adequately but not as good
as it can yet.
This commit is contained in:
Akita Noek
2016-02-11 16:13:41 -05:00
parent ac7d50048c
commit 9a3ef6b998
2 changed files with 197 additions and 117 deletions

View File

@@ -7,11 +7,13 @@ import logging
# Django
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
# AWX
from awx.main.models.base import * # noqa
__all__ = ['Role', 'RolePermission', 'Resource', 'RoleHierarchy']
__all__ = ['Role', 'RolePermission', 'Resource']
logger = logging.getLogger('awx.main.models.rbac')
@@ -28,32 +30,41 @@ class Role(CommonModelNameNotUnique):
singleton_name = models.TextField(null=True, default=None, db_index=True, unique=True)
parents = models.ManyToManyField('Role', related_name='children')
ancestors = models.ManyToManyField('Role', related_name='descendents') # auto-generated by `rebuild_role_ancestor_list`
members = models.ManyToManyField('auth.User', related_name='roles')
content_type = models.ForeignKey(ContentType, null=True, default=None)
object_id = models.PositiveIntegerField(null=True, default=None)
content_object = GenericForeignKey('content_type', 'object_id')
def save(self, *args, **kwargs):
super(Role, self).save(*args, **kwargs)
self.rebuild_role_hierarchy_cache()
self.rebuild_role_ancestor_list()
def rebuild_role_hierarchy_cache(self):
'Rebuilds the associated entries in the RoleHierarchy model'
def rebuild_role_ancestor_list(self):
'''
Updates our `ancestors` map to accurately reflect all of the ancestors for a role
# 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)])
You should never need to call this. Signal handlers should be calling
this method when the role hierachy changes automatically.
Note that this method relies on any parents' ancestor list being correct.
'''
actual_ancestors = set(Role.objects.filter(id=self.id).values_list('parents__ancestors__id', flat=True))
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 None in actual_ancestors:
actual_ancestors.remove(None)
stored_ancestors = set(self.ancestors.all().values_list('id', flat=True))
# 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 id in actual_ancestors - stored_ancestors:
self.ancestors.add(Role.objects.get(id=id))
for id in stored_ancestors - actual_ancestors:
self.ancestors.remove(Role.objects.get(id=id))
for child in self.children.all():
child.rebuild_role_hierarchy_cache()
child.rebuild_role_ancestor_list()
def grant(self, resource, permissions):
# take either the raw Resource or something that includes the ResourceMixin
@@ -85,22 +96,7 @@ class Role(CommonModelNameNotUnique):
return ret
def is_ancestor_of(self, role):
return RoleHierarchy.objects.filter(role_id=role.id, ancestor_id=self.id).count() > 0
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)
return role.ancestors.filter(id=self.id).exists()
class Resource(CommonModelNameNotUnique):
@@ -113,6 +109,10 @@ class Resource(CommonModelNameNotUnique):
verbose_name_plural = _('resources')
db_table = 'main_rbac_resources'
content_type = models.ForeignKey(ContentType, null=True, default=None)
object_id = models.PositiveIntegerField(null=True, default=None)
content_object = GenericForeignKey('content_type', 'object_id')
class RolePermission(CreatedModifiedModel):
'''