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

121 lines
4.3 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 json
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Max
from django.utils.translation import gettext as _
from authentic2 import app_settings
from authentic2.forms.mixins import SlugMixin
from authentic2.models import Attribute
from .models import BaseAuthenticator, LoginPasswordAuthenticator
class AuthenticatorsOrderForm(forms.Form):
order = forms.CharField(widget=forms.HiddenInput)
class AuthenticatorAddForm(SlugMixin, forms.ModelForm):
field_order = ('authenticator', 'name', 'ou')
authenticator = forms.ChoiceField(label=_('Authenticator'))
class Meta:
model = BaseAuthenticator
fields = ('name',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.authenticators = {
x.type: x for x in BaseAuthenticator.__subclasses__() if not x.unique or x.objects.count() < 1
}
self.fields['authenticator'].choices = [
(k, v._meta.verbose_name) for k, v in self.authenticators.items()
]
def clean(self):
if self.cleaned_data['authenticator'] in ('saml', 'oidc') and not self.cleaned_data.get('name'):
self.add_error('name', _('This field is required.'))
def save(self):
max_order = BaseAuthenticator.objects.aggregate(max=Max('order'))['max'] or 0
Authenticator = self.authenticators[self.cleaned_data['authenticator']]
self.instance = Authenticator(name=self.cleaned_data['name'], order=max_order + 1)
return super().save()
class AuthenticatorImportForm(forms.Form):
authenticator_json = forms.FileField(label=_('Authenticator export file'))
def clean_authenticator_json(self):
try:
return json.loads(self.cleaned_data['authenticator_json'].read().decode())
except ValueError:
raise ValidationError(_('File is not in the expected JSON format.'))
class LoginPasswordAuthenticatorAdvancedForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['phone_identifier_field'].choices = Attribute.objects.filter(
disabled=False,
multiple=False,
kind__in=('phone_number', 'fr_phone_number'),
).values_list('id', 'label')
# TODO drop temporary feature-flag app setting once phone number
# verification is enforced everywhere in /accounts/
if not app_settings.A2_ALLOW_PHONE_AUTHN_MANAGEMENT:
for field in (
'accept_email_authentication',
'accept_phone_authentication',
'phone_identifier_field',
):
del self.fields[field]
class Meta:
model = LoginPasswordAuthenticator
fields = (
'remember_me',
'include_ou_selector',
'password_regex',
'password_regex_error_msg',
'login_exponential_retry_timeout_duration',
'login_exponential_retry_timeout_factor',
'login_exponential_retry_timeout_max_duration',
'login_exponential_retry_timeout_min_duration',
'emails_ip_ratelimit',
'sms_ip_ratelimit',
'emails_address_ratelimit',
'sms_number_ratelimit',
'accept_email_authentication',
'accept_phone_authentication',
'phone_identifier_field',
)
class LoginPasswordAuthenticatorEditForm(forms.ModelForm):
class Meta:
model = LoginPasswordAuthenticator
exclude = ('name', 'slug', 'ou', 'button_label') + LoginPasswordAuthenticatorAdvancedForm.Meta.fields