437 lines
15 KiB
Python
437 lines
15 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/>.
|
|
|
|
from collections import namedtuple
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.exceptions import ValidationError
|
|
from django.core.validators import MinValueValidator
|
|
from django.db import models
|
|
from django.utils.text import slugify
|
|
from django.utils.translation import pgettext_lazy
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from authentic2.decorators import errorcollector
|
|
from django_rbac import utils as rbac_utils
|
|
from django_rbac.models import (
|
|
VIEW_OP,
|
|
Operation,
|
|
OrganizationalUnitAbstractBase,
|
|
PermissionAbstractBase,
|
|
RoleAbstractBase,
|
|
RoleParentingAbstractBase,
|
|
)
|
|
|
|
try:
|
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
|
except ImportError:
|
|
# Django < 1.8
|
|
from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation
|
|
|
|
from authentic2.decorators import GlobalCache
|
|
|
|
from . import app_settings, fields, managers
|
|
|
|
|
|
class OrganizationalUnit(OrganizationalUnitAbstractBase):
|
|
|
|
RESET_LINK_POLICY = 0
|
|
MANUAL_PASSWORD_POLICY = 1
|
|
|
|
USER_ADD_PASSWD_POLICY_CHOICES = (
|
|
(RESET_LINK_POLICY, _('Send reset link')),
|
|
(MANUAL_PASSWORD_POLICY, _('Manual password definition')),
|
|
)
|
|
|
|
USER_CAN_RESET_PASSWD_CHOICES = (
|
|
(None, _("System default")),
|
|
(True, _("Yes")),
|
|
(False, _("No")),
|
|
)
|
|
|
|
PolicyValue = namedtuple(
|
|
'PolicyValue',
|
|
['generate_password', 'reset_password_at_next_login', 'send_mail', 'send_password_reset'],
|
|
)
|
|
|
|
USER_ADD_PASSWD_POLICY_VALUES = {
|
|
RESET_LINK_POLICY: PolicyValue(False, False, False, True),
|
|
MANUAL_PASSWORD_POLICY: PolicyValue(False, False, True, False),
|
|
}
|
|
|
|
username_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Username is unique'))
|
|
email_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Email is unique'))
|
|
default = fields.UniqueBooleanField(verbose_name=_('Default organizational unit'))
|
|
|
|
validate_emails = models.BooleanField(blank=True, default=False, verbose_name=_('Validate emails'))
|
|
|
|
show_username = models.BooleanField(blank=True, default=True, verbose_name=_('Show username'))
|
|
|
|
admin_perms = GenericRelation(
|
|
rbac_utils.get_permission_model_name(), content_type_field='target_ct', object_id_field='target_id'
|
|
)
|
|
|
|
user_can_reset_password = models.NullBooleanField(
|
|
verbose_name=_('Users can reset password'), choices=USER_CAN_RESET_PASSWD_CHOICES
|
|
)
|
|
|
|
user_add_password_policy = models.IntegerField(
|
|
verbose_name=_('User creation password policy'), choices=USER_ADD_PASSWD_POLICY_CHOICES, default=0
|
|
)
|
|
|
|
clean_unused_accounts_alert = models.PositiveIntegerField(
|
|
verbose_name=_('Days after which the user receives an account deletion alert'),
|
|
validators=[
|
|
MinValueValidator(
|
|
30, _('Ensure that this value is greater than 30 days, or leave blank for deactivating.')
|
|
)
|
|
],
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
|
|
clean_unused_accounts_deletion = models.PositiveIntegerField(
|
|
verbose_name=_('Delay in days before cleaning unused accounts'),
|
|
validators=[
|
|
MinValueValidator(
|
|
30, _('Ensure that this value is greater than 30 days, or leave blank for deactivating.')
|
|
)
|
|
],
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
|
|
objects = managers.OrganizationalUnitManager()
|
|
|
|
class Meta:
|
|
verbose_name = _('organizational unit')
|
|
verbose_name_plural = _('organizational units')
|
|
ordering = ('name',)
|
|
unique_together = (
|
|
('name',),
|
|
('slug',),
|
|
)
|
|
|
|
def clean(self):
|
|
# if we set this ou as the default one, we must unset the other one if
|
|
# there is
|
|
if self.default:
|
|
qs = self.__class__.objects.filter(default=True)
|
|
if self.pk:
|
|
qs = qs.exclude(pk=self.pk)
|
|
qs.update(default=None)
|
|
if self.pk and not self.default and self.__class__.objects.get(pk=self.pk).default:
|
|
raise ValidationError(
|
|
_(
|
|
'You cannot unset this organizational '
|
|
'unit as the default, but you can set '
|
|
'another one as the default.'
|
|
)
|
|
)
|
|
if bool(self.clean_unused_accounts_alert) ^ bool(self.clean_unused_accounts_deletion):
|
|
raise ValidationError(_('Deletion and alert delays must be set together.'))
|
|
if (
|
|
self.clean_unused_accounts_alert
|
|
and self.clean_unused_accounts_alert >= self.clean_unused_accounts_deletion
|
|
):
|
|
raise ValidationError(_('Deletion alert delay must be less than actual deletion delay.'))
|
|
super().clean()
|
|
|
|
def get_admin_role(self):
|
|
"""Get or create the generic admin role for this organizational
|
|
unit.
|
|
"""
|
|
name = _('Managers of "{ou}"').format(ou=self)
|
|
slug = f'_a2-managers-of-{self.slug}'
|
|
return Role.objects.get_admin_role(
|
|
instance=self,
|
|
name=name,
|
|
slug=slug,
|
|
operation=VIEW_OP,
|
|
update_name=True,
|
|
update_slug=True,
|
|
create=True,
|
|
)
|
|
|
|
def delete(self, *args, **kwargs):
|
|
Permission.objects.filter(ou=self).delete()
|
|
return super(OrganizationalUnitAbstractBase, self).delete(*args, **kwargs)
|
|
|
|
def natural_key(self):
|
|
return [self.slug]
|
|
|
|
@classmethod
|
|
@GlobalCache(timeout=5)
|
|
def cached(cls):
|
|
return cls.objects.all()
|
|
|
|
def export_json(self):
|
|
return {
|
|
'uuid': self.uuid,
|
|
'slug': self.slug,
|
|
'name': self.name,
|
|
'description': self.description,
|
|
'default': self.default,
|
|
'email_is_unique': self.email_is_unique,
|
|
'username_is_unique': self.username_is_unique,
|
|
'validate_emails': self.validate_emails,
|
|
}
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
OrganizationalUnit._meta.natural_key = [['uuid'], ['slug'], ['name']]
|
|
|
|
|
|
class Permission(PermissionAbstractBase):
|
|
class Meta:
|
|
verbose_name = _('permission')
|
|
verbose_name_plural = _('permissions')
|
|
|
|
mirror_roles = GenericRelation(
|
|
rbac_utils.get_role_model_name(),
|
|
content_type_field='admin_scope_ct',
|
|
object_id_field='admin_scope_id',
|
|
)
|
|
|
|
|
|
Permission._meta.natural_key = [
|
|
['operation', 'ou', 'target'],
|
|
['operation', 'ou__isnull', 'target'],
|
|
]
|
|
|
|
|
|
class Role(RoleAbstractBase):
|
|
admin_scope_ct = models.ForeignKey(
|
|
to='contenttypes.ContentType',
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_('administrative scope content type'),
|
|
on_delete=models.CASCADE,
|
|
)
|
|
admin_scope_id = models.PositiveIntegerField(
|
|
verbose_name=_('administrative scope id'), null=True, blank=True
|
|
)
|
|
admin_scope = GenericForeignKey('admin_scope_ct', 'admin_scope_id')
|
|
service = models.ForeignKey(
|
|
to='authentic2.Service',
|
|
verbose_name=_('service'),
|
|
null=True,
|
|
blank=True,
|
|
related_name='roles',
|
|
on_delete=models.CASCADE,
|
|
)
|
|
external_id = models.TextField(verbose_name=_('external id'), blank=True, db_index=True)
|
|
|
|
admin_perms = GenericRelation(
|
|
rbac_utils.get_permission_model_name(), content_type_field='target_ct', object_id_field='target_id'
|
|
)
|
|
|
|
can_manage_members = models.BooleanField(
|
|
default=True, verbose_name=_('Allow adding or deleting role members')
|
|
)
|
|
|
|
def get_admin_role(self, create=True):
|
|
from . import utils
|
|
|
|
if app_settings.ROLE_ADMIN_RESTRICT_TO_OU_USERS:
|
|
view_user_perm = utils.get_view_user_perm(ou=self.ou)
|
|
else:
|
|
view_user_perm = utils.get_view_user_perm()
|
|
|
|
admin_role = self.__class__.objects.get_admin_role(
|
|
self,
|
|
name=_('Managers of role "{role}"').format(role=str(self)),
|
|
slug=f'_a2-managers-of-role-{slugify(str(self))}',
|
|
permissions=(view_user_perm,),
|
|
self_administered=True,
|
|
update_name=True,
|
|
update_slug=True,
|
|
create=create,
|
|
operation=MANAGE_MEMBERS_OP,
|
|
)
|
|
return admin_role
|
|
|
|
def validate_unique(self, exclude=None):
|
|
errors = {}
|
|
|
|
with errorcollector(errors):
|
|
super().validate_unique(exclude=exclude)
|
|
|
|
exclude = exclude or []
|
|
|
|
if 'name' not in exclude:
|
|
qs = self.__class__.objects.filter(name=self.name, ou=self.ou)
|
|
if self.pk:
|
|
qs = qs.exclude(pk=self.pk)
|
|
if qs.exists():
|
|
errors.setdefault('name', []).append(_('Name already used'))
|
|
|
|
if 'slug' not in exclude:
|
|
qs = self.__class__.objects.filter(slug=self.slug, ou=self.ou)
|
|
if self.pk:
|
|
qs = qs.exclude(pk=self.pk)
|
|
if qs.exists():
|
|
errors.setdefault('slug', []).append(_('Slug already used'))
|
|
|
|
if errors:
|
|
raise ValidationError(errors)
|
|
|
|
def save(self, *args, **kwargs):
|
|
# Service roles can only be part of the same ou as the service
|
|
if self.service:
|
|
self.ou = self.service.ou
|
|
result = super().save(*args, **kwargs)
|
|
self.get_admin_role(create=False)
|
|
return result
|
|
|
|
def has_self_administration(self, op=None):
|
|
if not op:
|
|
op = MANAGE_MEMBERS_OP
|
|
Permission = rbac_utils.get_permission_model()
|
|
operation = rbac_utils.get_operation(op)
|
|
self_perm, created = Permission.objects.get_or_create(
|
|
operation=operation,
|
|
target_ct=ContentType.objects.get_for_model(self),
|
|
target_id=self.pk,
|
|
ou__is_null=True,
|
|
)
|
|
return self.permissions.filter(pk=self_perm.pk).exists()
|
|
|
|
def add_self_administration(self, op=None):
|
|
'Add permission to role so that it is self-administered'
|
|
if not op:
|
|
op = MANAGE_MEMBERS_OP
|
|
Permission = rbac_utils.get_permission_model()
|
|
operation = rbac_utils.get_operation(op)
|
|
self_perm, created = Permission.objects.get_or_create(
|
|
operation=operation, target_ct=ContentType.objects.get_for_model(self), target_id=self.pk
|
|
)
|
|
self.permissions.through.objects.get_or_create(role=self, permission=self_perm)
|
|
return self_perm
|
|
|
|
def is_internal(self):
|
|
return self.slug.startswith('_')
|
|
|
|
objects = managers.RoleManager()
|
|
|
|
class Meta:
|
|
verbose_name = _('role')
|
|
verbose_name_plural = _('roles')
|
|
ordering = (
|
|
'ou',
|
|
'service',
|
|
'name',
|
|
)
|
|
unique_together = (('admin_scope_ct', 'admin_scope_id'),)
|
|
|
|
def natural_key(self):
|
|
return [
|
|
self.slug,
|
|
self.ou and self.ou.natural_key(),
|
|
self.service and self.service.natural_key(),
|
|
]
|
|
|
|
def to_json(self):
|
|
return {
|
|
'uuid': self.uuid,
|
|
'name': self.name,
|
|
'slug': self.slug,
|
|
'is_admin': bool(self.admin_scope_ct and self.admin_scope_id),
|
|
'is_service': bool(self.service),
|
|
'ou__uuid': self.ou.uuid if self.ou else None,
|
|
'ou__name': self.ou.name if self.ou else None,
|
|
'ou__slug': self.ou.slug if self.ou else None,
|
|
}
|
|
|
|
def export_json(self, attributes=False, parents=False, permissions=False):
|
|
d = {
|
|
'uuid': self.uuid,
|
|
'slug': self.slug,
|
|
'name': self.name,
|
|
'description': self.description,
|
|
'external_id': self.external_id,
|
|
'ou': self.ou and self.ou.natural_key_json(),
|
|
'service': self.service and self.service.natural_key_json(),
|
|
}
|
|
|
|
if attributes:
|
|
for attribute in self.attributes.all():
|
|
d.setdefault('attributes', []).append(attribute.to_json())
|
|
|
|
if parents:
|
|
RoleParenting = rbac_utils.get_role_parenting_model()
|
|
for parenting in RoleParenting.objects.filter(child_id=self.id, direct=True):
|
|
d.setdefault('parents', []).append(parenting.parent.natural_key_json())
|
|
|
|
if permissions:
|
|
for perm in self.permissions.all():
|
|
d.setdefault('permissions', []).append(perm.export_json())
|
|
|
|
return d
|
|
|
|
|
|
Role._meta.natural_key = [
|
|
['uuid'],
|
|
['slug', 'ou__isnull', 'service__isnull'],
|
|
['name', 'ou__isnull', 'service__isnull'],
|
|
['slug', 'ou', 'service'],
|
|
['name', 'ou', 'service'],
|
|
['slug', 'ou', 'service__isnull'],
|
|
['name', 'ou', 'service__isnull'],
|
|
]
|
|
|
|
|
|
class RoleParenting(RoleParentingAbstractBase):
|
|
class Meta(RoleParentingAbstractBase.Meta):
|
|
verbose_name = _('role parenting relation')
|
|
verbose_name_plural = _('role parenting relations')
|
|
|
|
def __str__(self):
|
|
return '{} {}> {}'.format(self.parent.name, '-' if self.direct else '~', self.child.name)
|
|
|
|
|
|
class RoleAttribute(models.Model):
|
|
KINDS = (('string', _('string')),)
|
|
role = models.ForeignKey(
|
|
to=Role, verbose_name=_('role'), related_name='attributes', on_delete=models.CASCADE
|
|
)
|
|
name = models.CharField(max_length=64, verbose_name=_('name'))
|
|
kind = models.CharField(max_length=32, choices=KINDS, verbose_name=_('kind'))
|
|
value = models.TextField(verbose_name=_('value'))
|
|
|
|
class Meta:
|
|
verbose_name = 'role attribute'
|
|
verbose_name_plural = _('role attributes')
|
|
unique_together = (('role', 'name', 'kind', 'value'),)
|
|
|
|
def to_json(self):
|
|
return {'name': self.name, 'kind': self.kind, 'value': self.value}
|
|
|
|
|
|
GenericRelation(Permission, content_type_field='target_ct', object_id_field='target_id').contribute_to_class(
|
|
ContentType, 'admin_perms'
|
|
)
|
|
|
|
|
|
CHANGE_PASSWORD_OP = Operation.register(name=_('Change password'), slug='change_password')
|
|
RESET_PASSWORD_OP = Operation.register(name=_('Password reset'), slug='reset_password')
|
|
ACTIVATE_OP = Operation.register(name=_('Activation'), slug='activate')
|
|
CHANGE_EMAIL_OP = Operation.register(name=pgettext_lazy('operation', 'Change email'), slug='change_email')
|
|
MANAGE_MEMBERS_OP = Operation.register(name=_('Manage role members'), slug='manage_members')
|
|
MANAGE_AUTHORIZATIONS_OP = Operation.register(name=_('Manage service consents'), slug='manage_authorizations')
|