authentic/src/authentic2/apps/authenticators/models.py

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)