authenticators: migrate login password authenticator (#53902)

This commit is contained in:
Valentin Deniaud 2022-04-13 13:58:40 +02:00
parent 8532ac64af
commit 46c99d7816
17 changed files with 366 additions and 156 deletions

View File

@ -15,15 +15,29 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django import forms
from django.core.exceptions import ValidationError
from django.template import Template, TemplateSyntaxError, VariableDoesNotExist
from django.utils.translation import ugettext as _
from authentic2.forms.mixins import SlugMixin
from .models import BaseAuthenticator
from .models import BaseAuthenticator, LoginPasswordAuthenticator
class AuthenticatorFormMixin:
def clean_show_condition(self):
condition = self.cleaned_data['show_condition']
if condition:
try:
Template('{%% if %s %%}OK{%% endif %%}' % condition)
except (TemplateSyntaxError, VariableDoesNotExist) as e:
raise ValidationError(_('template syntax error: %s') % e)
return condition
class AuthenticatorAddForm(SlugMixin, forms.ModelForm):
field_order = ('authenticator', 'name', 'ou')
authenticators = {x.type: x for x in BaseAuthenticator.__subclasses__()}
authenticators = {x.type: x for x in BaseAuthenticator.__subclasses__() if not x.internal}
authenticator = forms.ChoiceField(choices=[(k, v._meta.verbose_name) for k, v in authenticators.items()])
@ -35,3 +49,9 @@ class AuthenticatorAddForm(SlugMixin, forms.ModelForm):
Authenticator = self.authenticators[self.cleaned_data['authenticator']]
self.instance = Authenticator(name=self.cleaned_data['name'], ou=self.cleaned_data['ou'])
return super().save()
class LoginPasswordAuthenticatorEditForm(AuthenticatorFormMixin, forms.ModelForm):
class Meta:
model = LoginPasswordAuthenticator
exclude = ('name', 'slug', 'ou')

View File

@ -0,0 +1,47 @@
# Generated by Django 2.2.28 on 2022-04-13 12:56
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authenticators', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='LoginPasswordAuthenticator',
fields=[
(
'baseauthenticator_ptr',
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to='authenticators.BaseAuthenticator',
),
),
(
'remember_me',
models.PositiveIntegerField(
blank=True,
help_text='Session duration as seconds when using the remember me checkbox. Leave blank to hide the checkbox.',
null=True,
verbose_name='Remember me duration',
),
),
(
'include_ou_selector',
models.BooleanField(default=False, verbose_name='Include OU selector in login form'),
),
],
options={
'verbose_name': 'Password',
},
bases=('authenticators.baseauthenticator',),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 2.2.28 on 2022-04-13 13:04
from django.db import migrations
from authentic2 import app_settings
def create_login_password_authenticator(apps, schema_editor):
kwargs_settings = getattr(app_settings, 'AUTH_FRONTENDS_KWARGS', {})
password_settings = kwargs_settings.get('password', {})
LoginPasswordAuthenticator = apps.get_model('authenticators', 'LoginPasswordAuthenticator')
LoginPasswordAuthenticator.objects.get_or_create(
slug='password-authenticator',
defaults={
'order': password_settings.get('priority', 0),
'show_condition': password_settings.get('show_condition', ''),
'enabled': app_settings.A2_AUTH_PASSWORD_ENABLE,
'remember_me': app_settings.A2_USER_REMEMBER_ME,
'include_ou_selector': app_settings.A2_LOGIN_FORM_OU_SELECTOR,
},
)
class Migration(migrations.Migration):
dependencies = [
('authenticators', '0002_loginpasswordauthenticator'),
]
operations = [
migrations.RunPython(create_login_password_authenticator, reverse_code=migrations.RunPython.noop),
]

View File

@ -23,6 +23,7 @@ from django.shortcuts import render, reverse
from django.utils.formats import date_format
from django.utils.translation import ugettext_lazy as _
from authentic2 import views
from authentic2.utils.evaluate import evaluate_condition
from .query import AuthenticatorManager
@ -60,6 +61,7 @@ class BaseAuthenticator(models.Model):
type = ''
manager_form_class = None
internal = False
description_fields = ['show_condition']
class Meta:
@ -106,3 +108,38 @@ class BaseAuthenticator(models.Model):
except Exception as e:
logger.error(e)
return False
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']
internal = 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)

View File

@ -9,7 +9,9 @@
<a href="{% url 'a2-manager-authenticator-toggle' pk=object.pk %}">{{ object.enabled|yesno:_("Disable,Enable") }}</a>
<a href="{% url 'a2-manager-authenticator-edit' pk=object.pk %}">{% trans "Edit" %}</a>
<ul class="extra-actions-menu">
<li><a rel="popup" href="{% url 'a2-manager-authenticator-delete' pk=object.pk %}">{% trans "Delete" %}</a></li>
{% if not object.internal %}
<li><a rel="popup" href="{% url 'a2-manager-authenticator-delete' pk=object.pk %}">{% trans "Delete" %}</a></li>
{% endif %}
</ul>
</span>
{% endblock %}

View File

@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
@ -79,6 +80,11 @@ class AuthenticatorDeleteView(AuthenticatorsMixin, DeleteView):
model = BaseAuthenticator
success_url = reverse_lazy('a2-manager-authenticators')
def dispatch(self, *args, **kwargs):
if self.get_object().internal:
raise PermissionDenied
return super().dispatch(*args, **kwargs)
delete = AuthenticatorDeleteView.as_view()

View File

@ -16,21 +16,7 @@
import logging
from django.db.models import Count
from django.shortcuts import render
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from authentic2.a2_rbac.models import OrganizationalUnit as OU
from authentic2.a2_rbac.models import Role
from authentic2.custom_user.models import User
from . import app_settings, views
from .forms import authentication as authentication_forms
from .utils import misc as utils_misc
from .utils.evaluate import evaluate_condition
from .utils.service import get_service
from .utils.views import csrf_token_check
logger = logging.getLogger(__name__)
@ -61,123 +47,3 @@ class BaseAuthenticator:
def get_identifier(self):
return self.id
class LoginPasswordAuthenticator(BaseAuthenticator):
id = 'password'
how = ['password', 'password-on-https']
submit_name = 'login-password-submit'
priority = 0
def enabled(self):
return app_settings.A2_AUTH_PASSWORD_ENABLE
def name(self):
return ugettext_lazy('Password')
def get_service_ous(self, service):
roles = Role.objects.filter(allowed_services=service).children()
if not roles:
return []
service_ou_ids = []
qs = (
User.objects.filter(roles__in=roles)
.values_list('ou')
.annotate(count=Count('ou'))
.order_by('-count')
)
for ou_id, dummy_count in qs:
if not ou_id:
continue
service_ou_ids.append(ou_id)
if not service_ou_ids:
return []
return OU.objects.filter(pk__in=service_ou_ids)
def get_preferred_ous(self, request):
service = get_service(request)
preferred_ous_cookie = utils_misc.get_remember_cookie(request, 'preferred-ous')
preferred_ous = []
if preferred_ous_cookie:
preferred_ous.extend(OU.objects.filter(pk__in=preferred_ous_cookie))
# for the special case of services open to only one OU, pre-select it
if service:
for ou in self.get_service_ous(service):
if ou in preferred_ous:
continue
preferred_ous.append(ou)
return preferred_ous
def login(self, request, *args, **kwargs):
context = kwargs.get('context', {})
is_post = request.method == 'POST' and self.submit_name in request.POST
data = request.POST if is_post else None
initial = {}
preferred_ous = []
request.failed_logins = {}
# Special handling when the form contains an OU selector
if app_settings.A2_LOGIN_FORM_OU_SELECTOR:
preferred_ous = self.get_preferred_ous(request)
if preferred_ous:
initial['ou'] = preferred_ous[0]
form = authentication_forms.AuthenticationForm(
request=request, data=data, initial=initial, preferred_ous=preferred_ous
)
if request.user.is_authenticated and request.login_token.get('action'):
form.initial['username'] = request.user.username or request.user.email
form.fields['username'].widget.attrs['readonly'] = True
form.fields['password'].widget.attrs['autofocus'] = True
else:
form.fields['username'].widget.attrs['autofocus'] = not (bool(context.get('block_index')))
if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION:
form.fields['username'].label = _('Username or email')
if app_settings.A2_USERNAME_LABEL:
form.fields['username'].label = app_settings.A2_USERNAME_LABEL
is_secure = request.is_secure
context['submit_name'] = self.submit_name
if is_post:
csrf_token_check(request, form)
if form.is_valid():
if is_secure:
how = 'password-on-https'
else:
how = 'password'
if form.cleaned_data.get('remember_me'):
request.session['remember_me'] = True
request.session.set_expiry(app_settings.A2_USER_REMEMBER_ME)
response = utils_misc.login(request, form.get_user(), how)
if 'ou' in form.fields:
utils_misc.prepend_remember_cookie(
request, response, 'preferred-ous', form.cleaned_data['ou'].pk
)
if hasattr(request, 'needs_password_change'):
del request.needs_password_change
return utils_misc.redirect(
request, 'password_change', params={'next': response.url}, resolve=True
)
return response
else:
username = form.cleaned_data.get('username', '').strip()
if request.failed_logins:
for user, failure_data in request.failed_logins.items():
request.journal.record(
'user.login.failure',
user=user,
reason=failure_data.get('reason', None),
username=username,
)
elif username:
request.journal.record('user.login.failure', username=username)
context['form'] = form
return render(request, 'authentic2/login_password_form.html', context)
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)

View File

@ -51,6 +51,7 @@ class AuthenticationForm(auth_forms.AuthenticationForm):
def __init__(self, *args, **kwargs):
preferred_ous = kwargs.pop('preferred_ous', [])
self.authenticator = kwargs.pop('authenticator')
super().__init__(*args, **kwargs)
@ -60,10 +61,10 @@ class AuthenticationForm(auth_forms.AuthenticationForm):
factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR,
)
if not app_settings.A2_USER_REMEMBER_ME:
if not self.authenticator.remember_me:
del self.fields['remember_me']
if not app_settings.A2_LOGIN_FORM_OU_SELECTOR:
if not self.authenticator.include_ou_selector:
del self.fields['ou']
else:
if preferred_ous:
@ -135,7 +136,7 @@ class AuthenticationForm(auth_forms.AuthenticationForm):
def media(self):
media = super().media
media = media + Media(js=['authentic2/js/js_seconds_until.js'])
if app_settings.A2_LOGIN_FORM_OU_SELECTOR:
if self.authenticator.include_ou_selector:
media = media + Media(js=['authentic2/js/ou_selector.js'])
return media

View File

@ -191,7 +191,7 @@ AUTH_FRONTENDS = (
'authentic2_auth_saml.authenticators.SAMLAuthenticator',
'authentic2_auth_oidc.authenticators.OIDCAuthenticator',
'authentic2_auth_fc.authenticators.FcAuthenticator',
) + plugins.register_plugins_authenticators(('authentic2.authenticators.LoginPasswordAuthenticator',))
)
###########################
# RBAC settings

View File

@ -164,9 +164,17 @@ def get_backends(setting_name='IDP_BACKENDS'):
'''Return the list of enabled cleaned backends.'''
backends = []
if setting_name == 'AUTH_FRONTENDS':
from authentic2.apps.authenticators.models import BaseAuthenticator
from authentic2.apps.authenticators.models import BaseAuthenticator, LoginPasswordAuthenticator
backends = list(BaseAuthenticator.authenticators.filter(enabled=True))
backends = list(
BaseAuthenticator.authenticators.filter(enabled=True).exclude(slug='password-authenticator')
)
password_backend, dummy = LoginPasswordAuthenticator.objects.get_or_create(
slug='password-authenticator',
defaults={'enabled': True},
)
if password_backend.enabled:
backends.append(password_backend)
for backend_path in getattr(app_settings, setting_name):
kwargs = {}

View File

@ -28,6 +28,7 @@ from django.contrib.auth import logout as auth_logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import PasswordChangeView as DjPasswordChangeView
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.db.models import Count
from django.db.models.query import Q
from django.db.transaction import atomic
from django.forms import CharField
@ -51,7 +52,9 @@ from django.views.generic.base import RedirectView, View
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from ratelimit.utils import is_ratelimited
from authentic2.a2_rbac.models import Role
from authentic2.custom_user.models import iter_attributes
from authentic2.forms import authentication as authentication_forms
from authentic2_idp_oidc.models import OIDCAuthorization
from . import app_settings, attribute_kinds, cbv, constants, decorators, hooks, models, validators
@ -67,6 +70,7 @@ from .utils import switch_user as utils_switch_user
from .utils.evaluate import make_condition_context
from .utils.service import get_service, set_home_url
from .utils.view_decorators import enable_view_restriction
from .utils.views import csrf_token_check
User = get_user_model()
@ -697,6 +701,107 @@ def logout(request, next_url=None, do_local=True, check_referer=True):
return response
def login_password_login(request, authenticator, *args, **kwargs):
def get_service_ous(service):
roles = Role.objects.filter(allowed_services=service).children()
if not roles:
return []
service_ou_ids = []
qs = (
User.objects.filter(roles__in=roles)
.values_list('ou')
.annotate(count=Count('ou'))
.order_by('-count')
)
for ou_id, dummy_count in qs:
if not ou_id:
continue
service_ou_ids.append(ou_id)
if not service_ou_ids:
return []
return OU.objects.filter(pk__in=service_ou_ids)
def get_preferred_ous(request):
service = get_service(request)
preferred_ous_cookie = utils_misc.get_remember_cookie(request, 'preferred-ous')
preferred_ous = []
if preferred_ous_cookie:
preferred_ous.extend(OU.objects.filter(pk__in=preferred_ous_cookie))
# for the special case of services open to only one OU, pre-select it
if service:
for ou in get_service_ous(service):
if ou in preferred_ous:
continue
preferred_ous.append(ou)
return preferred_ous
context = kwargs.get('context', {})
is_post = request.method == 'POST' and 'login-password-submit' in request.POST
data = request.POST if is_post else None
initial = {}
preferred_ous = []
request.failed_logins = {}
# Special handling when the form contains an OU selector
if authenticator.include_ou_selector:
preferred_ous = get_preferred_ous(request)
if preferred_ous:
initial['ou'] = preferred_ous[0]
form = authentication_forms.AuthenticationForm(
request=request, data=data, initial=initial, preferred_ous=preferred_ous, authenticator=authenticator
)
if request.user.is_authenticated and request.login_token.get('action'):
form.initial['username'] = request.user.username or request.user.email
form.fields['username'].widget.attrs['readonly'] = True
form.fields['password'].widget.attrs['autofocus'] = True
else:
form.fields['username'].widget.attrs['autofocus'] = not (bool(context.get('block_index')))
if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION:
form.fields['username'].label = _('Username or email')
if app_settings.A2_USERNAME_LABEL:
form.fields['username'].label = app_settings.A2_USERNAME_LABEL
is_secure = request.is_secure
context['submit_name'] = 'login-password-submit'
if is_post:
csrf_token_check(request, form)
if form.is_valid():
if is_secure:
how = 'password-on-https'
else:
how = 'password'
if form.cleaned_data.get('remember_me'):
request.session['remember_me'] = True
request.session.set_expiry(authenticator.remember_me)
response = utils_misc.login(request, form.get_user(), how)
if 'ou' in form.fields:
utils_misc.prepend_remember_cookie(
request, response, 'preferred-ous', form.cleaned_data['ou'].pk
)
if hasattr(request, 'needs_password_change'):
del request.needs_password_change
return utils_misc.redirect(
request, 'password_change', params={'next': response.url}, resolve=True
)
return response
else:
username = form.cleaned_data.get('username', '').strip()
if request.failed_logins:
for user, failure_data in request.failed_logins.items():
request.journal.record(
'user.login.failure',
user=user,
reason=failure_data.get('reason', None),
username=username,
)
elif username:
request.journal.record('user.login.failure', username=username)
context['form'] = form
return render(request, 'authentic2/login_password_form.html', context)
def login_password_profile(request, *args, **kwargs):
context = kwargs.pop('context', {})
can_change_password = utils_misc.user_can_change_password(request=request)

View File

@ -30,6 +30,7 @@ from django.utils.timezone import now
from authentic2.a2_rbac.models import OrganizationalUnit as OU
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
from authentic2.apps.journal.models import Event
from authentic2.custom_user.models import DeletedUser
from authentic2.models import Attribute
@ -76,7 +77,9 @@ def test_login_with_condition(settings, app, franceconnect):
def test_login_autorun(settings, app, franceconnect):
# hide password block
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
LoginPasswordAuthenticator.objects.update_or_create(
slug='password-authenticator', defaults={'enabled': False}
)
response = app.get('/login/')
assert response.location.startswith('https://fcp')

View File

@ -38,6 +38,7 @@ from jwcrypto.jwt import JWT
from authentic2.a2_rbac.models import OrganizationalUnit
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
from authentic2.custom_user.models import DeletedUser
from authentic2.models import Attribute, AttributeValue
from authentic2.utils.misc import last_authentication_event
@ -494,7 +495,9 @@ def test_login_autorun(oidc_provider, app, settings):
assert 'Server' in response
# hide password block
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
LoginPasswordAuthenticator.objects.update_or_create(
slug='password-authenticator', defaults={'enabled': False}
)
response = app.get('/login/', status=302)
assert response['Location'] == '/accounts/oidc/login/%s/' % oidc_provider.pk

View File

@ -24,6 +24,7 @@ from django.contrib.auth import get_user_model
from mellon.adapters import UserCreationError
from mellon.models import Issuer, UserSAMLIdentifier
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
from authentic2.custom_user.models import DeletedUser
from authentic2.models import Attribute
from authentic2_auth_saml.adapters import AuthenticAdapter, MappingError
@ -278,7 +279,9 @@ def test_login_autorun(db, app, settings):
{"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')}
]
# hide password block
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
LoginPasswordAuthenticator.objects.update_or_create(
slug='password-authenticator', defaults={'enabled': False}
)
response = app.get('/login/', status=302)
assert '/accounts/saml/login/?entityID=' in response['Location']

View File

@ -36,6 +36,7 @@ from ldaptools.slapd import Slapd, has_slapd
from authentic2 import models
from authentic2.a2_rbac.models import OrganizationalUnit, Role
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
from authentic2.backends import ldap_backend
from authentic2.models import Service
from authentic2.utils import crypto, switch_user
@ -1776,7 +1777,7 @@ def test_ou_selector(slapd, settings, app, ou1):
'use_tls': False,
}
]
settings.A2_LOGIN_FORM_OU_SELECTOR = True
LoginPasswordAuthenticator.objects.update(include_ou_selector=True)
# Check login to the wrong ou does not work
response = app.get('/login/')
@ -1806,7 +1807,7 @@ def test_ou_selector_default_ou(slapd, settings, app, ou1):
'use_tls': False,
}
]
settings.A2_LOGIN_FORM_OU_SELECTOR = True
LoginPasswordAuthenticator.objects.update(include_ou_selector=True)
# Check login to the wrong ou does not work
response = app.get('/login/')

View File

@ -20,6 +20,7 @@ import pytest
from django.contrib.auth import get_user_model
from authentic2 import models
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
from authentic2.utils.misc import get_token_login_url
from .utils import assert_event, login, set_service
@ -72,14 +73,14 @@ def test_show_condition(db, app, settings, caplog):
response = app.get('/login/')
assert 'name="login-password-submit"' in response
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'False'}}
LoginPasswordAuthenticator.objects.update(show_condition='False')
response = app.get('/login/')
# login form must not be displayed
assert 'name="login-password-submit"' not in response
assert len(caplog.records) == 0
# set a condition with error
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': '\'admin\' in unknown'}}
LoginPasswordAuthenticator.objects.update(show_condition='\'admin\' in unknown')
response = app.get('/login/')
assert 'name="login-password-submit"' in response
assert len(caplog.records) == 1
@ -88,7 +89,7 @@ def test_show_condition(db, app, settings, caplog):
def test_show_condition_service(db, rf, app, settings):
portal = models.Service.objects.create(pk=1, name='Service', slug='portal')
service = models.Service.objects.create(pk=2, name='Service', slug='service')
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'service_slug == \'portal\''}}
LoginPasswordAuthenticator.objects.update(show_condition='service_slug == \'portal\'')
response = app.get('/login/')
assert 'name="login-password-submit"' not in response
@ -104,9 +105,9 @@ def test_show_condition_service(db, rf, app, settings):
assert 'name="login-password-submit"' not in response
def test_show_condition_with_headers(app, settings):
def test_show_condition_with_headers(db, app, settings):
settings.A2_AUTH_OIDC_ENABLE = False # prevent db access by OIDC frontend
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': '\'X-Entrouvert\' in headers'}}
LoginPasswordAuthenticator.objects.update(show_condition='\'X-Entrouvert\' in headers')
response = app.get('/login/')
assert 'name="login-password-submit"' not in response
response = app.get('/login/', headers={'x-entrouvert': '1'})
@ -172,7 +173,7 @@ def test_session_expire(app, simple_user, freezer):
def test_session_remember_me_ok(app, settings, simple_user, freezer):
settings.A2_USER_REMEMBER_ME = 3600 * 24 * 30
LoginPasswordAuthenticator.objects.update(remember_me=3600 * 24 * 30)
freezer.move_to('2018-01-01')
# Verify session are longer
login(app, simple_user, remember_me=True)
@ -187,7 +188,7 @@ def test_session_remember_me_ok(app, settings, simple_user, freezer):
def test_session_remember_me_nok(app, settings, simple_user, freezer):
settings.A2_USER_REMEMBER_ME = 3600 * 24 * 30
LoginPasswordAuthenticator.objects.update(remember_me=3600 * 24 * 30)
freezer.move_to('2018-01-01')
# Verify session are longer
login(app, simple_user, remember_me=True)
@ -202,7 +203,7 @@ def test_session_remember_me_nok(app, settings, simple_user, freezer):
def test_ou_selector(app, settings, simple_user, ou1, ou2, user_ou1, role_ou1):
settings.A2_LOGIN_FORM_OU_SELECTOR = True
LoginPasswordAuthenticator.objects.update(include_ou_selector=True)
response = app.get('/login/')
# Check selector is here and there are no errors
assert not response.pyquery('.errorlist')
@ -359,3 +360,30 @@ def test_token_login(app, simple_user):
assert simple_user.first_name in resp.text
assert app.session['_auth_user_id'] == str(simple_user.pk)
assert_event('user.login', user=simple_user, session=app.session, how='token')
def test_password_authenticator_data_migration(migration, settings):
app = 'authenticators'
migrate_from = [(app, '0002_loginpasswordauthenticator')]
migrate_to = [(app, '0003_auto_20220413_1504')]
old_apps = migration.before(migrate_from)
LoginPasswordAuthenticator = old_apps.get_model(app, 'LoginPasswordAuthenticator')
assert not LoginPasswordAuthenticator.objects.exists()
settings.AUTH_FRONTENDS_KWARGS = {
"password": {"priority": -1, "show_condition": "'backoffice' not in login_hint"}
}
settings.A2_LOGIN_FORM_OU_SELECTOR = True
settings.A2_AUTH_PASSWORD_ENABLE = False
settings.A2_USER_REMEMBER_ME = 42
new_apps = migration.apply(migrate_to)
LoginPasswordAuthenticator = new_apps.get_model(app, 'LoginPasswordAuthenticator')
authenticator = LoginPasswordAuthenticator.objects.get()
assert authenticator.slug == 'password-authenticator'
assert authenticator.order == -1
assert authenticator.show_condition == "'backoffice' not in login_hint"
assert authenticator.enabled is False
assert authenticator.remember_me == 42
assert authenticator.include_ou_selector is True

View File

@ -27,3 +27,50 @@ def test_authenticators_authorization(app, simple_user, superuser):
resp = resp.click('Authenticators')
assert 'Authenticators' in resp.text
def test_authenticators_password(app, superuser):
resp = login(app, superuser, path='/manage/authenticators/')
# Password authenticator already exists
assert 'Password' in resp.text
resp = resp.click('Configure')
assert 'Click "Edit" to change configuration.' in resp.text
# cannot delete password authenticator
assert 'Delete' not in resp.text
app.get('/manage/authenticators/1/delete/', status=403)
resp = resp.click('Edit')
assert list(resp.form.fields) == [
'csrfmiddlewaretoken',
'order',
'show_condition',
'remember_me',
'include_ou_selector',
None,
]
resp.form['show_condition'] = '}'
resp = resp.form.submit()
assert 'template syntax error: Could not parse' in resp.text
resp.form['show_condition'] = "'backoffice' in login_hint or remotre_addr == '1.2.3.4'"
resp = resp.form.submit().follow()
assert 'Click "Edit" to change configuration.' not in resp.text
assert (
"Show condition: &#39;backoffice&#39; in login_hint or remotre_addr == &#39;1.2.3.4&#39;" in resp.text
)
resp = resp.click('Disable').follow()
assert 'Authenticator has been disabled.' in resp.text
resp = app.get('/manage/authenticators/')
assert 'class="section disabled"' in resp.text
resp = resp.click('Configure')
resp = resp.click('Enable').follow()
assert 'Authenticator has been enabled.' in resp.text
# cannot add another password authenticator
resp = app.get('/manage/authenticators/add/')
assert 'Password' not in resp.text