419 lines
14 KiB
Python
419 lines
14 KiB
Python
# authentic2 - versatile identity manager
|
|
# Copyright (C) 2010-2019 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
import uuid
|
|
from functools import wraps
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
|
from django.core.validators import validate_slug
|
|
from django.utils.text import format_lazy
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from authentic2.a2_rbac.models import RoleAttribute
|
|
from authentic2.decorators import errorcollector
|
|
from authentic2.utils.lazy import lazy_join
|
|
from django_rbac.models import Operation
|
|
from django_rbac.utils import get_ou_model, get_permission_model, get_role_model, get_role_parenting_model
|
|
|
|
|
|
def update_model(obj, d):
|
|
for attr, value in d.items():
|
|
setattr(obj, attr, value)
|
|
errors = {}
|
|
with errorcollector(errors):
|
|
if hasattr(obj, 'validate'):
|
|
obj.validate()
|
|
|
|
with errorcollector(errors):
|
|
if hasattr(obj, 'validate_unique'):
|
|
obj.validate_unique()
|
|
if errors:
|
|
errorlist = []
|
|
for key, messages in list(errors.items()):
|
|
if key == NON_FIELD_ERRORS:
|
|
errorlist.extend(messages)
|
|
else:
|
|
value = getattr(obj, key)
|
|
|
|
def error_list(messages):
|
|
for message in messages:
|
|
if isinstance(message, ValidationError):
|
|
yield message.message
|
|
else:
|
|
yield message
|
|
|
|
for message in error_list(messages):
|
|
errorlist.append(
|
|
format_lazy(
|
|
'{}="{}": {}', obj.__class__._meta.get_field(key).verbose_name, value, message
|
|
)
|
|
)
|
|
raise ValidationError(errorlist)
|
|
obj.save()
|
|
|
|
|
|
class ExportContext:
|
|
_role_qs = None
|
|
_ou_qs = None
|
|
export_roles = None
|
|
export_ous = None
|
|
|
|
def __init__(self, role_qs=None, ou_qs=None, export_roles=True, export_ous=True):
|
|
self._role_qs = role_qs
|
|
self._ou_qs = ou_qs
|
|
self.export_roles = export_roles
|
|
self.export_ous = export_ous
|
|
|
|
@property
|
|
def role_qs(self):
|
|
return self._role_qs or get_role_model().objects.all()
|
|
|
|
@property
|
|
def ou_qs(self):
|
|
return self._ou_qs or get_ou_model().objects.all()
|
|
|
|
|
|
def export_site(context=None):
|
|
context = context or ExportContext()
|
|
d = {}
|
|
if context.export_roles:
|
|
d['roles'] = export_roles(context)
|
|
if context.export_ous:
|
|
d['ous'] = export_ous(context)
|
|
return d
|
|
|
|
|
|
def export_ous(context):
|
|
return [ou.export_json() for ou in context.ou_qs]
|
|
|
|
|
|
def export_roles(context):
|
|
"""Serialize roles in role_queryset"""
|
|
return [role.export_json(attributes=True, parents=True, permissions=True) for role in context.role_qs]
|
|
|
|
|
|
def search_ou(ou_d):
|
|
try:
|
|
OU = get_ou_model()
|
|
return OU.objects.get_by_natural_key_json(ou_d)
|
|
except OU.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def search_role(role_d, ou=None):
|
|
Role = get_role_model()
|
|
try:
|
|
role = Role.objects.get_by_natural_key_json(role_d)
|
|
except Role.DoesNotExist:
|
|
return None
|
|
else:
|
|
if ou and role.ou != ou:
|
|
# Allow creation of the role in a different OU
|
|
role_d.pop('uuid')
|
|
return None
|
|
return role
|
|
|
|
|
|
class ImportContext:
|
|
"""Holds information on how to perform the import.
|
|
|
|
ou_delete_orphans: if True any existing ou that is not found in the export will
|
|
be deleted
|
|
|
|
role_delete_orphans: if True any existing role that is not found in the export will
|
|
be deleted
|
|
|
|
|
|
role_attributes_update: for each role in the import data,
|
|
attributes will deleted and re-created
|
|
|
|
|
|
role_parentings_update: for each role in the import data,
|
|
parentings will deleted and re-created
|
|
|
|
role_permissions_update: for each role in the import data,
|
|
permissions will deleted and re-created
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
import_roles=True,
|
|
import_ous=True,
|
|
role_delete_orphans=False,
|
|
role_parentings_update=True,
|
|
role_permissions_update=True,
|
|
role_attributes_update=True,
|
|
ou_delete_orphans=False,
|
|
set_ou=None,
|
|
):
|
|
self.import_roles = import_roles
|
|
self.import_ous = import_ous
|
|
self.role_delete_orphans = role_delete_orphans
|
|
self.ou_delete_orphans = ou_delete_orphans
|
|
self.role_parentings_update = role_parentings_update
|
|
self.role_permissions_update = role_permissions_update
|
|
self.role_attributes_update = role_attributes_update
|
|
self.set_ou = set_ou
|
|
|
|
|
|
class RoleDeserializer:
|
|
def __init__(self, d, import_context):
|
|
self._import_context = import_context
|
|
self._obj = None
|
|
self._parents = None
|
|
self._attributes = None
|
|
self._permissions = None
|
|
|
|
self._role_d = dict()
|
|
for key, value in d.items():
|
|
if key == 'parents':
|
|
self._parents = value
|
|
elif key == 'attributes':
|
|
self._attributes = value
|
|
elif key == 'permissions':
|
|
self._permissions = value
|
|
else:
|
|
self._role_d[key] = value
|
|
|
|
def wraps_validationerror(func):
|
|
@wraps(func)
|
|
def f(self, *args, **kwargs):
|
|
try:
|
|
return func(self, *args, **kwargs)
|
|
except ValidationError as e:
|
|
raise ValidationError(
|
|
_('Role "%(name)s": %(errors)s')
|
|
% {
|
|
'name': self._role_d.get('name', self._role_d.get('slug')),
|
|
'errors': lazy_join(', ', [v.message for v in e.error_list]),
|
|
}
|
|
)
|
|
|
|
return f
|
|
|
|
@wraps_validationerror
|
|
def deserialize(self):
|
|
if self._import_context.set_ou:
|
|
ou = self._import_context.set_ou
|
|
has_ou = True
|
|
else:
|
|
ou_d = self._role_d['ou']
|
|
has_ou = bool(ou_d)
|
|
ou = None if not has_ou else search_ou(ou_d)
|
|
if has_ou and not ou:
|
|
raise ValidationError(_("Can't import role because missing Organizational Unit: %s") % ou_d)
|
|
|
|
obj = search_role(self._role_d, ou=self._import_context.set_ou)
|
|
|
|
kwargs = self._role_d.copy()
|
|
kwargs.pop('ou', None)
|
|
kwargs.pop('service', None)
|
|
if has_ou:
|
|
kwargs['ou'] = ou
|
|
|
|
if 'uuid' in kwargs:
|
|
if not isinstance(kwargs['uuid'], str):
|
|
raise ValidationError(_("Cannot import role '%s' with invalid uuid") % kwargs.get('name'))
|
|
try:
|
|
uuid.UUID(kwargs['uuid'])
|
|
except ValueError:
|
|
raise ValidationError(_("Cannot import role '%s' with invalid uuid") % kwargs.get('name'))
|
|
|
|
if 'slug' in kwargs:
|
|
if not isinstance(kwargs['slug'], str):
|
|
raise ValidationError(_("Cannot import role '%s' with invalid slug") % kwargs.get('name'))
|
|
try:
|
|
validate_slug(kwargs['slug'])
|
|
except ValidationError:
|
|
raise ValidationError(_("Cannot import role '%s' with invalid slug") % kwargs.get('name'))
|
|
|
|
if obj: # Role already exist
|
|
self._obj = obj
|
|
status = 'updated'
|
|
update_model(self._obj, kwargs)
|
|
else: # Create role
|
|
if 'uuid' in kwargs and not kwargs['uuid']:
|
|
raise ValidationError(_("Cannot import role '%s' with empty uuid") % kwargs.get('name'))
|
|
self._obj = get_role_model().objects.create(**kwargs)
|
|
status = 'created'
|
|
|
|
# Ensure admin role is created.
|
|
# Absoluteley necessary to create
|
|
# parentings relationship later on,
|
|
# since we don't deserialize technical role.
|
|
self._obj.get_admin_role()
|
|
return self._obj, status
|
|
|
|
@wraps_validationerror
|
|
def attributes(self):
|
|
"""Update attributes (delete everything then create)"""
|
|
created, deleted = [], []
|
|
for attr in self._obj.attributes.all():
|
|
attr.delete()
|
|
deleted.append(attr)
|
|
# Create attributes
|
|
if self._attributes:
|
|
for attr_dict in self._attributes:
|
|
attr_dict['role'] = self._obj
|
|
created.append(RoleAttribute.objects.create(**attr_dict))
|
|
|
|
return created, deleted
|
|
|
|
@wraps_validationerror
|
|
def parentings(self):
|
|
"""Update parentings (delete everything then create)"""
|
|
created, deleted = [], []
|
|
Parenting = get_role_parenting_model()
|
|
for parenting in Parenting.objects.filter(child=self._obj, direct=True):
|
|
parenting.delete()
|
|
deleted.append(parenting)
|
|
|
|
if self._parents:
|
|
for parent_d in self._parents:
|
|
parent = search_role(parent_d)
|
|
if not parent:
|
|
raise ValidationError(_("Could not find parent role: %s") % parent_d)
|
|
created.append(Parenting.objects.create(child=self._obj, direct=True, parent=parent))
|
|
|
|
return created, deleted
|
|
|
|
@wraps_validationerror
|
|
def permissions(self):
|
|
"""Update permissions (delete everything then create)"""
|
|
created, deleted = [], []
|
|
for perm in self._obj.permissions.all():
|
|
perm.delete()
|
|
deleted.append(perm)
|
|
self._obj.permissions.clear()
|
|
if self._permissions:
|
|
for perm in self._permissions:
|
|
op = Operation.objects.get_by_natural_key_json(perm['operation'])
|
|
ou = get_ou_model().objects.get_by_natural_key_json(perm['ou']) if perm['ou'] else None
|
|
ct = ContentType.objects.get_by_natural_key_json(perm['target_ct'])
|
|
target = ct.model_class().objects.get_by_natural_key_json(perm['target'])
|
|
perm = get_permission_model().objects.create(
|
|
operation=op, ou=ou, target_ct=ct, target_id=target.pk
|
|
)
|
|
self._obj.permissions.add(perm)
|
|
created.append(perm)
|
|
|
|
return created, deleted
|
|
|
|
|
|
class ImportResult:
|
|
def __init__(self):
|
|
self.roles = {'created': [], 'updated': []}
|
|
self.ous = {'created': [], 'updated': []}
|
|
self.attributes = {'created': [], 'deleted': []}
|
|
self.parentings = {'created': [], 'deleted': []}
|
|
self.permissions = {'created': [], 'deleted': []}
|
|
|
|
def update_roles(self, role, d_status):
|
|
self.roles[d_status].append(role)
|
|
|
|
def update_ous(self, ou, status):
|
|
self.ous[status].append(ou)
|
|
|
|
def _bulk_update(self, attrname, created, deleted):
|
|
attr = getattr(self, attrname)
|
|
attr['created'].extend(created)
|
|
attr['deleted'].extend(deleted)
|
|
|
|
def update_attributes(self, created, deleted):
|
|
self._bulk_update('attributes', created, deleted)
|
|
|
|
def update_parentings(self, created, deleted):
|
|
self._bulk_update('parentings', created, deleted)
|
|
|
|
def update_permissions(self, created, deleted):
|
|
self._bulk_update('permissions', created, deleted)
|
|
|
|
def to_str(self, verbose=False):
|
|
res = ""
|
|
for attr in ('roles', 'ous', 'parentings', 'permissions', 'attributes'):
|
|
data = getattr(self, attr)
|
|
for status in ('created', 'updated', 'deleted'):
|
|
if status in data:
|
|
s_data = data[status]
|
|
res += "%s %s %s\n" % (len(s_data), attr, status)
|
|
return res
|
|
|
|
|
|
def import_ou(ou_d):
|
|
OU = get_ou_model()
|
|
ou = search_ou(ou_d)
|
|
if ou is None:
|
|
ou = OU.objects.create(**ou_d)
|
|
status = 'created'
|
|
else:
|
|
update_model(ou, ou_d)
|
|
status = 'updated'
|
|
# Ensure admin role is created
|
|
ou.get_admin_role()
|
|
return ou, status
|
|
|
|
|
|
def import_site(json_d, import_context=None):
|
|
import_context = import_context or ImportContext()
|
|
result = ImportResult()
|
|
|
|
if not isinstance(json_d, dict):
|
|
raise ValidationError(_('Import file is invalid: not a dictionnary'))
|
|
|
|
if import_context.import_ous:
|
|
for ou_d in json_d.get('ous', []):
|
|
result.update_ous(*import_ou(ou_d))
|
|
|
|
if import_context.import_roles:
|
|
roles_ds = []
|
|
for role_d in json_d.get('roles', []):
|
|
# ignore internal roles
|
|
slug = role_d.get('slug')
|
|
if isinstance(slug, str) and slug.startswith('_'):
|
|
continue
|
|
roles_ds.append(RoleDeserializer(role_d, import_context))
|
|
|
|
for ds in roles_ds:
|
|
result.update_roles(*ds.deserialize())
|
|
|
|
if import_context.role_attributes_update:
|
|
for ds in roles_ds:
|
|
result.update_attributes(*ds.attributes())
|
|
|
|
if import_context.role_parentings_update:
|
|
for ds in roles_ds:
|
|
result.update_parentings(*ds.parentings())
|
|
|
|
if import_context.role_permissions_update:
|
|
for ds in roles_ds:
|
|
result.update_permissions(*ds.permissions())
|
|
|
|
if import_context.ou_delete_orphans:
|
|
raise ValidationError(
|
|
_("Unsupported context value for ou_delete_orphans : %s") % (import_context.ou_delete_orphans)
|
|
)
|
|
|
|
if import_context.role_delete_orphans:
|
|
# FIXME : delete each role that is in DB but not in the export
|
|
raise ValidationError(
|
|
_("Unsupported context value for role_delete_orphans : %s")
|
|
% (import_context.role_delete_orphans)
|
|
)
|
|
|
|
return result
|