authentic/src/authentic2/custom_user/backends.py

300 lines
12 KiB
Python

import copy
import functools
from django.apps import apps
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from django.db.models.query import Q
from authentic2.a2_rbac.models import OrganizationalUnit as OU
from authentic2.a2_rbac.models import Permission
def get_fk_model(model, fieldname):
try:
field = model._meta.get_field('ou')
except FieldDoesNotExist:
return None
else:
if not field.is_relation or not field.many_to_one:
return None
return field.related_model
_MODEL_CHILDREN = None
_MODEL_PARENTS = None
def get_model_inheritance():
global _MODEL_CHILDREN # pylint: disable=global-statement
global _MODEL_PARENTS # pylint: disable=global-statement
if _MODEL_CHILDREN is None or _MODEL_PARENTS is None:
_MODEL_CHILDREN = {}
_MODEL_PARENTS = {}
for app in apps.get_app_configs():
for child in app.get_models():
for parent in child.__bases__:
if (
issubclass(parent, models.Model)
and hasattr(parent, '_meta')
and not parent._meta.abstract
):
_MODEL_CHILDREN.setdefault(parent, set()).add(child)
_MODEL_PARENTS.setdefault(child, set()).add(parent)
return _MODEL_CHILDREN, _MODEL_PARENTS
def get_model_child_classes(model):
return get_model_inheritance()[0].get(model) or ()
def get_model_parent_classes(model):
return get_model_inheritance()[1].get(model) or ()
class DjangoRBACBackend:
_DEFAULT_DJANGO_RBAC_PERMISSIONS_HIERARCHY = {
'view': ['search'],
'admin': ['change', 'delete', 'add', 'view', 'search'],
'change': ['view', 'search'],
'delete': ['view', 'search'],
'add': ['view', 'search'],
}
def authenticate(self, request=None, **kwargs):
# this method is mandatory
pass
def get_permission_cache(self, user_obj):
"""Returns the permission cache for an user
The permission cache is a dictionnary, key can be of many types:
- `'ou.<ou.id>'` ; contains a list of permissions owner by this user as strings
(<app_label>.<permission>_<model_name>), those permissions are restricted to objects in
this organizational unit,
- `'__all__'`: contains a list of global permissions (applicable to any object in any
organizaton unit) owner by this user,
- `'<content_type.id>.<object.pk>'`: contains permissions restricted to a specific
object,
- `'<app_label>'`: contains a boolean, it indicates that the user own at least on
permision on a model of this application.
"""
if not hasattr(user_obj, '_rbac_perms_cache'):
perms_cache = {}
qs = Permission.objects.for_user(user_obj)
ct_ct = ContentType.objects.get_for_model(ContentType)
qs = qs.select_related('operation')
for permission in qs:
target_ct = ContentType.objects.get_for_id(permission.target_ct_id)
if target_ct == ct_ct:
target = ContentType.objects.get_for_id(permission.target_id)
app_label = target.app_label
model = target.model
model_child_classes = get_model_child_classes(target.model_class())
if permission.ou_id:
key = 'ou.%s' % permission.ou_id
else:
key = '__all__'
else:
app_label = target_ct.app_label
model = target_ct.model
model_child_classes = get_model_child_classes(target_ct.model_class)
key = '%s.%s' % (permission.target_ct_id, permission.target_id)
slug = permission.operation.slug
perms = ['%s.%s_%s' % (app_label, slug, model)]
for model_child_class in model_child_classes:
perms.append(
f'{model_child_class._meta.app_label}.{slug}_{model_child_class._meta.model_name}'
)
perm_hierarchy = getattr(
settings,
'DJANGO_RBAC_PERMISSIONS_HIERARCHY',
self._DEFAULT_DJANGO_RBAC_PERMISSIONS_HIERARCHY,
)
if slug in perm_hierarchy:
for other_perm in perm_hierarchy[slug]:
perms.append(str('%s.%s_%s' % (app_label, other_perm, model)))
for model_child_class in model_child_classes:
for other_perm in perm_hierarchy[slug]:
perms.append(
f'{model_child_class._meta.app_label}.{other_perm}_{model_child_class._meta.model_name}'
)
permissions = perms_cache.setdefault(key, set())
permissions.update(perms)
# optimization for has_module_perms
perms_cache[app_label] = True
user_obj._rbac_perms_cache = perms_cache
return user_obj._rbac_perms_cache
def get_all_permissions(self, user_obj, obj=None):
if user_obj.is_anonymous:
return ()
perms_cache = self.get_permission_cache(user_obj)
if obj:
permissions = set()
ct = ContentType.objects.get_for_model(obj)
key = '%s.%s' % (ct.id, obj.pk)
if key in perms_cache:
object_permissions = perms_cache[key]
permissions.update(perms_cache[key])
# add equivalent permissions with parent app_label.model_name
for parent in get_model_parent_classes(ct.model_class()):
for permission in object_permissions:
_, rest = permission.split('.')
operation, _ = rest.rsplit('_', 1)
permissions.add(f'{parent._meta.app_label}.{operation}_{parent._meta.model_name}')
for permission in perms_cache.get('__all__', set()):
if permission.startswith('%s.' % ct.app_label) and permission.endswith('_%s' % ct.model):
permissions.add(permission)
for parent in get_model_parent_classes(ct.model_class()):
if permission.startswith(parent._meta.app_label) and permission.endswith(
f'_{parent._meta.model_name}'
):
permissions.add(permission)
if hasattr(obj, 'ou_id') and obj.ou_id:
key = 'ou.%s' % obj.ou_id
for permission in perms_cache.get(key, ()):
if permission.startswith('%s.' % ct.app_label) and permission.endswith('_%s' % ct.model):
permissions.add(permission)
for parent in get_model_parent_classes(ct.model_class()):
if permission.startswith(parent._meta.app_label) and permission.endswith(
f'_{parent._meta.model_name}'
):
permissions.add(permission)
return permissions
else:
return perms_cache.get('__all__', [])
def has_perm(self, user_obj, perm, obj=None):
if user_obj.is_anonymous:
return False
if not user_obj.is_active:
return False
if user_obj.is_superuser:
return True
return perm in self.get_all_permissions(user_obj, obj=obj)
def has_perms(self, user_obj, perm_list, obj=None):
if user_obj.is_anonymous:
return False
if not user_obj.is_active:
return False
all_permissions = self.get_all_permissions(user_obj, obj=obj)
return all(perm in all_permissions for perm in perm_list)
def has_module_perms(self, user_obj, package_name):
if user_obj.is_anonymous:
return False
if not user_obj.is_active:
return False
if user_obj.is_superuser:
return True
return package_name in self.get_permission_cache(user_obj)
def has_perm_any(self, user_obj, perm_or_perms):
'''Return True if user has any perm on any object'''
if user_obj.is_anonymous:
return False
if not user_obj.is_active:
return False
if user_obj.is_superuser:
return True
if isinstance(perm_or_perms, str):
perm_or_perms = [perm_or_perms]
perm_or_perms = set(perm_or_perms)
cache = self.get_permission_cache(user_obj)
if perm_or_perms & cache.get('__all__', set()):
return True
for key, value in cache.items():
if isinstance(value, bool):
continue
if key == '__all__':
continue
if key.startswith('ou.'):
if perm_or_perms & value:
return True
elif perm_or_perms & value:
return True
return False
def filter_by_perm_query(self, user_obj, perm_or_perms, qs):
"""Create a filter for a queryset for the objects on which the user has
the given permission. Permissions can be set on individual objects, globally on
a content type or locally for all objects of an organizational unit.
"""
if user_obj.is_anonymous:
return False
if not user_obj.is_active:
return False
if user_obj.is_superuser:
return True
if isinstance(perm_or_perms, str):
perm_or_perms = [perm_or_perms]
perm_or_perms = set(perm_or_perms)
cache = self.get_permission_cache(user_obj)
model = qs.model
has_ou_field = get_fk_model(model, 'ou') == OU
if perm_or_perms & cache.get('__all__', set()):
return True
q = []
for key, value in cache.items():
if isinstance(value, bool):
continue
if key == '__all__':
continue
if key.startswith('ou.'):
if has_ou_field and perm_or_perms & value:
q.append(Q(ou_id=int(key[3:])))
continue
elif perm_or_perms & value:
dummy_ct_id, fk = key.split('.')
q.append(Q(pk=int(fk)))
if q:
return functools.reduce(Q.__or__, q)
return False
def filter_by_perm(self, user_obj, perm_or_perms, qs):
"""Filter a queryset for the objects on which the user has
the given permission. Permissions can be set on individual objects, globally on
a content type or locally for all objects of an organizational unit.
"""
query = self.filter_by_perm_query(user_obj, perm_or_perms, qs)
if query is True:
return copy.deepcopy(qs)
elif query is False:
return qs.none()
else:
return qs.filter(query)
def has_ou_perm(self, user_obj, perm, ou):
if user_obj.is_anonymous:
return False
if not user_obj.is_active:
return False
if user_obj.is_superuser:
return True
if self.has_perm(user_obj, perm):
return True
return perm in self.get_permission_cache(user_obj).get('ou.%s' % ou.pk, ())
def ous_with_perm(self, user_obj, perm, queryset=None):
qs = queryset or OU.objects.all()
if user_obj.is_anonymous:
return qs.none()
if not user_obj.is_active:
return qs.none()
if user_obj.is_superuser:
return qs
cache = self.get_permission_cache(user_obj)
ou_ids = []
for key in cache:
if key == '__all__' and perm in cache[key]:
return qs
if key.startswith('ou.') and perm in cache[key]:
ou_ids.append(int(key.split('.')[1]))
return qs.filter(id__in=ou_ids)