212 lines
7.2 KiB
Python
212 lines
7.2 KiB
Python
# authentic2 - versatile identity manager
|
|
# Copyright (C) 2022 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 datetime
|
|
import logging
|
|
import uuid
|
|
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import models
|
|
from django.shortcuts import render, reverse
|
|
from django.utils.formats import date_format
|
|
from django.utils.text import capfirst
|
|
from django.utils.translation import pgettext_lazy
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from authentic2 import views
|
|
from authentic2.a2_rbac.models import Role
|
|
from authentic2.manager.utils import label_from_role
|
|
from authentic2.utils.evaluate import condition_validator, evaluate_condition
|
|
|
|
from .query import AuthenticatorManager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BaseAuthenticator(models.Model):
|
|
uuid = models.CharField(max_length=255, unique=True, default=uuid.uuid4, editable=False)
|
|
name = models.CharField(_('Name'), blank=True, max_length=128)
|
|
slug = models.SlugField(unique=True)
|
|
ou = models.ForeignKey(
|
|
verbose_name=_('organizational unit'),
|
|
to='a2_rbac.OrganizationalUnit',
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.CASCADE,
|
|
)
|
|
order = models.IntegerField(_('Order'), default=0, editable=False)
|
|
enabled = models.BooleanField(default=False, editable=False)
|
|
show_condition = models.CharField(
|
|
_('Show condition'),
|
|
max_length=1024,
|
|
blank=True,
|
|
default='',
|
|
help_text=_(
|
|
'Django template controlling authenticator display. For example, "\'backoffice\' in '
|
|
'login_hint or remotre_addr == \'1.2.3.4\'" would hide the authenticator from normal users '
|
|
'except if they come from the specified IP address. Available variables include '
|
|
'service_ou_slug, service_slug, remote_addr, login_hint and headers.'
|
|
),
|
|
validators=[condition_validator],
|
|
)
|
|
button_description = models.CharField(
|
|
_('Login button description'),
|
|
max_length=256,
|
|
blank=True,
|
|
help_text=_(
|
|
'Description will be shown on login page above login button (unless already set by theme).'
|
|
),
|
|
)
|
|
button_label = models.CharField(_('Login button label'), max_length=64, default=_('Login'))
|
|
|
|
objects = models.Manager()
|
|
authenticators = AuthenticatorManager()
|
|
|
|
type = ''
|
|
related_models = []
|
|
related_object_form_class = None
|
|
manager_view_template_name = 'authentic2/authenticators/authenticator_detail.html'
|
|
unique = False
|
|
protected = False
|
|
description_fields = ['show_condition']
|
|
empty_field_labels = {'show_condition': pgettext_lazy('show condition', 'None')}
|
|
|
|
class Meta:
|
|
ordering = ('-enabled', 'order', 'name', 'slug', 'ou')
|
|
|
|
def __str__(self):
|
|
if self.name:
|
|
return '%s - %s' % (self._meta.verbose_name, self.name)
|
|
return str(self._meta.verbose_name)
|
|
|
|
@property
|
|
def manager_form_classes(self):
|
|
return [(_('General'), self.manager_form_class)]
|
|
|
|
def get_identifier(self):
|
|
return self.type if self.unique else '%s_%s' % (self.type, self.slug)
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('a2-manager-authenticator-detail', kwargs={'pk': self.pk})
|
|
|
|
def get_short_description(self):
|
|
return ''
|
|
|
|
def get_full_description(self):
|
|
for field in self.description_fields:
|
|
if hasattr(self, 'get_%s_display' % field):
|
|
value = getattr(self, 'get_%s_display' % field)()
|
|
else:
|
|
value = getattr(self, field)
|
|
|
|
value = value or self.empty_field_labels.get(field)
|
|
if not value:
|
|
continue
|
|
|
|
if isinstance(value, datetime.datetime):
|
|
value = date_format(value, 'DATETIME_FORMAT')
|
|
|
|
yield _('%(field)s: %(value)s') % {
|
|
'field': capfirst(self._meta.get_field(field).verbose_name),
|
|
'value': value,
|
|
}
|
|
|
|
def shown(self, ctx=()):
|
|
if not self.show_condition:
|
|
return True
|
|
ctx = dict(ctx, id=self.slug)
|
|
try:
|
|
return evaluate_condition(self.show_condition, ctx, on_raise=True)
|
|
except Exception as e:
|
|
logger.error(e)
|
|
return False
|
|
|
|
def has_valid_configuration(self):
|
|
for _, form_class in self.manager_form_classes:
|
|
try:
|
|
self.full_clean(exclude=getattr(form_class._meta, 'exclude', None))
|
|
except ValidationError:
|
|
return False
|
|
return True
|
|
|
|
|
|
class AuthenticatorRelatedObjectBase(models.Model):
|
|
authenticator = models.ForeignKey(BaseAuthenticator, on_delete=models.CASCADE)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
def get_journal_text(self):
|
|
return '%s (%s)' % (self._meta.verbose_name, self.pk)
|
|
|
|
@property
|
|
def model_name(self):
|
|
return self._meta.model_name
|
|
|
|
@property
|
|
def verbose_name_plural(self):
|
|
return self._meta.verbose_name_plural
|
|
|
|
|
|
class AddRoleAction(AuthenticatorRelatedObjectBase):
|
|
role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.CASCADE)
|
|
condition = models.CharField(_('Condition (unused)'), editable=False, max_length=256, blank=True)
|
|
mandatory = models.BooleanField(_('Mandatory (unused)'), editable=False, default=False)
|
|
|
|
class Meta:
|
|
default_related_name = 'add_role_actions'
|
|
verbose_name = _('Add a role')
|
|
verbose_name_plural = _('Add roles')
|
|
|
|
def __str__(self):
|
|
return label_from_role(self.role)
|
|
|
|
|
|
class LoginPasswordAuthenticator(BaseAuthenticator):
|
|
remember_me = models.PositiveIntegerField(
|
|
_('Remember me duration'),
|
|
blank=True,
|
|
null=True,
|
|
help_text=_(
|
|
'Session duration as seconds when using the remember me checkbox. Leave blank to hide the checkbox.'
|
|
),
|
|
)
|
|
include_ou_selector = models.BooleanField(_('Include OU selector in login form'), default=False)
|
|
|
|
type = 'password'
|
|
how = ['password', 'password-on-https']
|
|
unique = True
|
|
protected = True
|
|
|
|
class Meta:
|
|
verbose_name = _('Password')
|
|
|
|
@property
|
|
def manager_form_class(self):
|
|
from .forms import LoginPasswordAuthenticatorEditForm
|
|
|
|
return LoginPasswordAuthenticatorEditForm
|
|
|
|
def login(self, request, *args, **kwargs):
|
|
return views.login_password_login(request, self, *args, **kwargs)
|
|
|
|
def profile(self, request, *args, **kwargs):
|
|
return views.login_password_profile(request, *args, **kwargs)
|
|
|
|
def registration(self, request, *args, **kwargs):
|
|
context = kwargs.get('context', {})
|
|
return render(request, 'authentic2/login_password_registration_form.html', context)
|