authentic/src/authentic2/forms/authentication.py

130 lines
5.0 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 math
from django import forms
from django.utils.translation import ugettext_lazy as _, ugettext
from django.contrib.auth import forms as auth_forms
from django.utils import html
from authentic2.forms.fields import PasswordField
from ..a2_rbac.models import OrganizationalUnit as OU
from .. import app_settings, utils
from ..exponential_retry_timeout import ExponentialRetryTimeout
class AuthenticationForm(auth_forms.AuthenticationForm):
password = PasswordField(label=_('Password'))
remember_me = forms.BooleanField(
initial=False,
required=False,
label=_('Remember me'),
help_text=_('Do not ask for authentication next time'))
ou = forms.ModelChoiceField(
label=utils.lazy_label(_('Organizational unit'), lambda: app_settings.A2_LOGIN_FORM_OU_SELECTOR_LABEL),
required=True,
queryset=OU.objects.all())
def __init__(self, *args, **kwargs):
preferred_ous = kwargs.pop('preferred_ous', [])
super(AuthenticationForm, self).__init__(*args, **kwargs)
self.exponential_backoff = ExponentialRetryTimeout(
key_prefix='login-exp-backoff-',
duration=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION,
factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR)
if not app_settings.A2_USER_REMEMBER_ME:
del self.fields['remember_me']
if not app_settings.A2_LOGIN_FORM_OU_SELECTOR:
del self.fields['ou']
else:
if preferred_ous:
choices = self.fields['ou'].choices
new_choices = list(choices)[:1] + [
(ugettext('Preferred organizational units'), [
(ou.pk, ou.name) for ou in preferred_ous]),
(ugettext('All organizational units'), list(choices)[1:]),
]
self.fields['ou'].choices = new_choices
if self.request:
self.remote_addr = self.request.META['REMOTE_ADDR']
else:
self.remote_addr = '0.0.0.0'
def exp_backoff_keys(self):
return self.cleaned_data['username'], self.remote_addr
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
keys = None
if username and password:
keys = self.exp_backoff_keys()
seconds_to_wait = self.exponential_backoff.seconds_to_wait(*keys)
if seconds_to_wait > app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION:
seconds_to_wait -= app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION
msg = _('You made too many login errors recently, you must '
'wait <span class="js-seconds-until">%s</span> seconds '
'to try again.')
msg = msg % int(math.ceil(seconds_to_wait))
msg = html.mark_safe(msg)
raise forms.ValidationError(msg)
try:
self.clean_authenticate()
except Exception:
if keys:
self.exponential_backoff.failure(*keys)
raise
else:
if keys:
self.exponential_backoff.success(*keys)
return self.cleaned_data
def clean_authenticate(self):
# copied from django.contrib.auth.forms.AuthenticationForm to add support for ou selector
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
ou = self.cleaned_data.get('ou')
if username is not None and password:
self.user_cache = utils.authenticate(self.request, username=username, password=password, ou=ou)
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login',
params={'username': self.username_field.verbose_name},
)
else:
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data
@property
def media(self):
media = super(AuthenticationForm, self).media
media.add_js(['authentic2/js/js_seconds_until.js'])
if app_settings.A2_LOGIN_FORM_OU_SELECTOR:
media.add_js(['authentic2/js/ou_selector.js'])
return media