231 lines
7.8 KiB
Python
231 lines
7.8 KiB
Python
import contextlib
|
|
import functools
|
|
import threading
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db import models
|
|
from django.db.models import query
|
|
from django.db.models.query import Prefetch, Q
|
|
|
|
from . import utils
|
|
|
|
|
|
class AbstractBaseManager(models.Manager):
|
|
def get_by_natural_key(self, uuid):
|
|
return self.get(uuid=uuid)
|
|
|
|
|
|
class OperationManager(models.Manager):
|
|
def get_by_natural_key(self, slug):
|
|
return self.get(slug=slug)
|
|
|
|
def has_perm(self, user, operation_slug, object_or_model, ou=None):
|
|
"""Test if an user can do the operation given by operation_slug
|
|
on the given object_or_model eventually scoped by an organizational
|
|
unit given by ou.
|
|
|
|
Returns True or False.
|
|
"""
|
|
ou_query = query.Q(ou__isnull=True)
|
|
if ou:
|
|
ou_query |= query.Q(ou=ou.as_scope())
|
|
ct = ContentType.objects.get_for_model(object_or_model)
|
|
target_query = query.Q(target_ct=ContentType.objects.get_for_model(ContentType), target_id=ct.pk)
|
|
if isinstance(object_or_model, models.Model):
|
|
target_query |= query.Q(target_ct=ct, target_id=object.pk)
|
|
Permission = utils.get_permission_model()
|
|
qs = Permission.objects.for_user(user)
|
|
qs = qs.filter(operation__slug=operation_slug)
|
|
qs = qs.filter(ou_query & target_query)
|
|
return qs.exists()
|
|
|
|
|
|
class PermissionManagerBase(models.Manager):
|
|
def get_by_natural_key(self, operation_slug, ou_nk, target_ct, target_nk):
|
|
qs = self.filter(operation__slug=operation_slug)
|
|
if ou_nk:
|
|
OrganizationalUnit = utils.get_ou_model()
|
|
try:
|
|
ou = OrganizationalUnit.objects.get_by_natural_key(*ou_nk)
|
|
except OrganizationalUnit.DoesNotExist:
|
|
raise self.model.DoesNotExist
|
|
qs = qs.filter(ou=ou)
|
|
else:
|
|
qs = qs.filter(ou__isnull=True)
|
|
try:
|
|
target_ct = ContentType.objects.get_by_natural_key(*target_ct)
|
|
except ContentType.DoesNotExist:
|
|
raise self.model.DoesNotExist
|
|
target_model = target_ct.model_class()
|
|
try:
|
|
target = target_model.objects.get_by_natural_key(*target_nk)
|
|
except target_model.DoesNotExist:
|
|
raise self.model.DoesNotExist
|
|
return qs.get(target_ct=ContentType.objects.get_for_model(target), target_id=target.pk)
|
|
|
|
|
|
class PermissionQueryset(query.QuerySet):
|
|
def by_target_ct(self, target):
|
|
"""Filter permission whose target content-type matches the content
|
|
type of the target argument
|
|
"""
|
|
target_ct = ContentType.objects.get_for_model(target)
|
|
return self.filter(target_ct=target_ct)
|
|
|
|
def by_target(self, target):
|
|
'''Filter permission whose target matches target'''
|
|
return self.by_target_ct(target).filter(target_id=target.pk)
|
|
|
|
def for_user(self, user):
|
|
"""Retrieve all permissions hold by an user through its role and
|
|
inherited roles.
|
|
"""
|
|
Role = utils.get_role_model()
|
|
roles = Role.objects.for_user(user=user)
|
|
return self.filter(roles__in=roles)
|
|
|
|
def cleanup(self):
|
|
count = 0
|
|
for p in self:
|
|
if not p.target and (p.target_ct_id or p.target_id):
|
|
p.delete()
|
|
count += 1
|
|
return count
|
|
|
|
|
|
PermissionManager = PermissionManagerBase.from_queryset(PermissionQueryset)
|
|
|
|
|
|
class IntCast(models.Func):
|
|
function = 'int'
|
|
template = 'CAST((%(expressions)s) AS %(function)s)'
|
|
|
|
|
|
class RoleQuerySet(query.QuerySet):
|
|
def for_user(self, user):
|
|
return self.filter(members=user).parents().distinct()
|
|
|
|
def parents(self, include_self=True, annotate=False):
|
|
qs = self.model.objects.filter(child_relation__child__in=self)
|
|
if include_self:
|
|
qs = self | qs
|
|
qs = qs.distinct()
|
|
if annotate:
|
|
qs = qs.annotate(direct=models.Max(IntCast('child_relation__direct')))
|
|
return qs
|
|
|
|
def children(self, include_self=True, annotate=False):
|
|
qs = self.model.objects.filter(parent_relation__parent__in=self)
|
|
if include_self:
|
|
qs = self | qs
|
|
qs = qs.distinct()
|
|
if annotate:
|
|
qs = qs.annotate(direct=models.Max(IntCast('parent_relation__direct')))
|
|
return qs
|
|
|
|
def all_members(self):
|
|
User = get_user_model()
|
|
prefetch = Prefetch('roles', queryset=self, to_attr='direct')
|
|
return (
|
|
User.objects.filter(Q(roles__in=self) | Q(roles__parent_relation__parent__in=self))
|
|
.distinct()
|
|
.prefetch_related(prefetch)
|
|
)
|
|
|
|
def by_admin_scope_ct(self, admin_scope):
|
|
admin_scope_ct = ContentType.objects.get_for_model(admin_scope)
|
|
return self.filter(admin_scope_ct=admin_scope_ct)
|
|
|
|
def cleanup(self):
|
|
count = 0
|
|
for r in self.filter(Q(admin_scope_ct_id__isnull=False) | Q(admin_scope_id__isnull=False)):
|
|
if not r.admin_scope:
|
|
r.delete()
|
|
count += 1
|
|
return count
|
|
|
|
|
|
RoleManager = AbstractBaseManager.from_queryset(RoleQuerySet)
|
|
|
|
|
|
class RoleParentingManager(models.Manager):
|
|
class Local(threading.local):
|
|
DO_UPDATE_CLOSURE = True
|
|
CLOSURE_UPDATED = False
|
|
|
|
tls = Local()
|
|
|
|
def get_by_natural_key(self, parent_nk, child_nk, direct):
|
|
Role = utils.get_role_model()
|
|
try:
|
|
parent = Role.objects.get_by_natural_key(*parent_nk)
|
|
except Role.DoesNotExist:
|
|
raise self.model.DoesNotExist
|
|
try:
|
|
child = Role.objects.get_by_natural_key(*child_nk)
|
|
except Role.DoesNotExist:
|
|
raise self.model.DoesNotExist
|
|
return self.get(parent=parent, child=child, direct=direct)
|
|
|
|
def update_transitive_closure(self):
|
|
"""Recompute the transitive closure of the inheritance relation
|
|
from scratch. Add missing indirect relations and delete
|
|
obsolete indirect relations.
|
|
"""
|
|
if not self.tls.DO_UPDATE_CLOSURE:
|
|
self.tls.CLOSURE_UPDATED = True
|
|
return
|
|
|
|
# existing indirect paths
|
|
old = set(self.filter(direct=False).values_list('parent_id', 'child_id'))
|
|
# existing direct paths
|
|
ris = set(self.filter(direct=True).values_list('parent_id', 'child_id'))
|
|
add = set()
|
|
new = set()
|
|
old_new = ris
|
|
|
|
# Start computing new indirect paths
|
|
while True:
|
|
for (i, j) in ris:
|
|
for (k, l) in old_new:
|
|
if j == k and (i, l) not in ris:
|
|
new.add((i, l))
|
|
if old_new != ris:
|
|
for (i, j) in old_new:
|
|
for (k, l) in ris:
|
|
if j == k and (i, l) not in ris:
|
|
new.add((i, l))
|
|
if not new:
|
|
break
|
|
add.update(new)
|
|
ris.update(new)
|
|
old_new = new
|
|
new = set()
|
|
# Create new relations
|
|
self.model.objects.bulk_create(
|
|
self.model(parent_id=a, child_id=b, direct=False) for a, b in add - old
|
|
)
|
|
# Delete old ones
|
|
obsolete = old - add
|
|
if obsolete:
|
|
queries = (query.Q(parent_id=a, child_id=b, direct=False) for a, b in obsolete)
|
|
self.model.objects.filter(functools.reduce(query.Q.__or__, queries)).delete()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def defer_update_transitive_closure():
|
|
from . import utils
|
|
|
|
RoleParentingManager.tls.DO_UPDATE_CLOSURE = False
|
|
try:
|
|
yield
|
|
except Exception:
|
|
raise
|
|
else:
|
|
if RoleParentingManager.tls.CLOSURE_UPDATED:
|
|
utils.get_role_parenting_model().objects.update_transitive_closure()
|
|
finally:
|
|
RoleParentingManager.tls.DO_UPDATE_CLOSURE = True
|
|
RoleParentingManager.tls.CLOSURE_UPDATED = False
|