From 86033d32ebd45214a106646562e75956e894ac1c Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Wed, 6 Jul 2022 17:18:21 +0200 Subject: [PATCH 2/2] migrate authenticator to database (#66876) --- src/authentic2_auth_fedict/__init__.py | 6 -- src/authentic2_auth_fedict/app_settings.py | 44 -------------- src/authentic2_auth_fedict/forms.py | 26 +++++++++ .../migrations/0001_initial.py | 36 ++++++++++++ .../migrations/0002_auto_20220706_1712.py | 35 +++++++++++ .../migrations/__init__.py | 0 src/authentic2_auth_fedict/models.py | 19 +++--- src/authentic2_auth_fedict/signals.py | 4 +- tests/settings.py | 1 - tests/test_all.py | 58 ++++++++++++++++++- tests/utils.py | 4 +- 11 files changed, 167 insertions(+), 66 deletions(-) delete mode 100644 src/authentic2_auth_fedict/app_settings.py create mode 100644 src/authentic2_auth_fedict/forms.py create mode 100644 src/authentic2_auth_fedict/migrations/0001_initial.py create mode 100644 src/authentic2_auth_fedict/migrations/0002_auto_20220706_1712.py create mode 100644 src/authentic2_auth_fedict/migrations/__init__.py diff --git a/src/authentic2_auth_fedict/__init__.py b/src/authentic2_auth_fedict/__init__.py index 5fc18e4..9186c8a 100644 --- a/src/authentic2_auth_fedict/__init__.py +++ b/src/authentic2_auth_fedict/__init__.py @@ -51,12 +51,6 @@ class Plugin: def get_authentication_backends(self): return ['authentic2_auth_fedict.backends.FedictBackend'] - def get_auth_frontends(self): - return ['authentic2_auth_fedict.authenticators.FedictAuthenticator'] - - def get_authenticators(self): - return ['authentic2_auth_fedict.authenticators.FedictAuthenticator'] - def redirect_logout_list(self, request, next_url=None): from mellon.views import logout diff --git a/src/authentic2_auth_fedict/app_settings.py b/src/authentic2_auth_fedict/app_settings.py deleted file mode 100644 index fc62c4c..0000000 --- a/src/authentic2_auth_fedict/app_settings.py +++ /dev/null @@ -1,44 +0,0 @@ -# authentic2_auth_fedict - Fedict authentication for Authentic -# Copyright (C) 2016 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 . - - -class AppSettings: - '''Thanks django-allauth''' - - __SENTINEL = object() - - def __init__(self, prefix): - self.prefix = prefix - - def _setting(self, name, dflt=__SENTINEL): - from django.conf import settings - from django.core.exceptions import ImproperlyConfigured - - v = getattr(settings, self.prefix + name, dflt) - if v is self.__SENTINEL: - raise ImproperlyConfigured('Missing setting %r' % (self.prefix + name)) - return v - - @property - def enable(self): - return self._setting('ENABLE', False) - - -import sys - -app_settings = AppSettings('A2_AUTH_FEDICT_') -app_settings.__name__ = __name__ -sys.modules[__name__] = app_settings diff --git a/src/authentic2_auth_fedict/forms.py b/src/authentic2_auth_fedict/forms.py new file mode 100644 index 0000000..a514750 --- /dev/null +++ b/src/authentic2_auth_fedict/forms.py @@ -0,0 +1,26 @@ +# authentic2_auth_fedict - Fedict authentication for Authentic +# 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 . + +from authentic2.apps.authenticators.forms import AuthenticatorFormMixin +from django import forms + +from .models import FedictAuthenticator + + +class FedictAuthenticatorForm(AuthenticatorFormMixin, forms.ModelForm): + class Meta: + model = FedictAuthenticator + exclude = ('name', 'slug', 'ou') diff --git a/src/authentic2_auth_fedict/migrations/0001_initial.py b/src/authentic2_auth_fedict/migrations/0001_initial.py new file mode 100644 index 0000000..8d5231e --- /dev/null +++ b/src/authentic2_auth_fedict/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 2.2.26 on 2022-07-06 15:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('authenticators', '0003_auto_20220413_1504'), + ] + + operations = [ + migrations.CreateModel( + name='FedictAuthenticator', + 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', + ), + ), + ], + options={ + 'verbose_name': 'Belgian eID', + }, + bases=('authenticators.baseauthenticator',), + ), + ] diff --git a/src/authentic2_auth_fedict/migrations/0002_auto_20220706_1712.py b/src/authentic2_auth_fedict/migrations/0002_auto_20220706_1712.py new file mode 100644 index 0000000..e168f39 --- /dev/null +++ b/src/authentic2_auth_fedict/migrations/0002_auto_20220706_1712.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.26 on 2022-07-06 15:12 + +from django.conf import settings +from django.db import migrations + + +def create_fedict_authenticator(apps, schema_editor): + if not getattr(settings, 'A2_AUTH_FEDICT_ENABLE', False): + return + + FedictAuthenticator = apps.get_model('authentic2_auth_fedict', 'FedictAuthenticator') + + kwargs_settings = getattr(settings, 'AUTH_FRONTENDS_KWARGS', {}) + authenticator_settings = kwargs_settings.get('fedict', {}) + + priority = authenticator_settings.get('priority') + priority = priority if priority is not None else -1 + + FedictAuthenticator.objects.create( + slug='fedict-authenticator', + order=priority, + show_condition=authenticator_settings.get('show_condition') or '', + enabled=True, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentic2_auth_fedict', '0001_initial'), + ] + + operations = [ + migrations.RunPython(create_fedict_authenticator, reverse_code=migrations.RunPython.noop), + ] diff --git a/src/authentic2_auth_fedict/migrations/__init__.py b/src/authentic2_auth_fedict/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/authentic2_auth_fedict/models.py b/src/authentic2_auth_fedict/models.py index d21f3da..70b6066 100644 --- a/src/authentic2_auth_fedict/models.py +++ b/src/authentic2_auth_fedict/models.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from authentic2.authenticators import BaseAuthenticator +from authentic2.apps.authenticators.models import BaseAuthenticator from django.shortcuts import render from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ @@ -25,18 +25,19 @@ try: except ImportError: from authentic2.utils.misc import redirect_to_login -from . import app_settings - class FedictAuthenticator(BaseAuthenticator): - id = 'fedict' - priority = -1 + type = 'fedict' + unique = True + + class Meta: + verbose_name = _('Belgian eID') - def enabled(self): - return app_settings.enable and list(get_idps()) + @property + def manager_form_class(self): + from .forms import FedictAuthenticatorForm - def name(self): - return _('Belgian eID') + return FedictAuthenticatorForm def login(self, request, *args, **kwargs): context = kwargs.get('context', {}).copy() diff --git a/src/authentic2_auth_fedict/signals.py b/src/authentic2_auth_fedict/signals.py index bd51631..d4f8f49 100644 --- a/src/authentic2_auth_fedict/signals.py +++ b/src/authentic2_auth_fedict/signals.py @@ -16,12 +16,12 @@ from authentic2.models import Attribute, AttributeValue -from . import app_settings from .adapters import AuthenticAdapter +from .models import FedictAuthenticator def on_user_logged_in(sender, request, user, **kwargs): - if not app_settings.enable: + if not FedictAuthenticator.objects.filter(enabled=True).exists(): return if user.backend == 'authentic2_auth_fedict.backends.FedictBackend': return diff --git a/tests/settings.py b/tests/settings.py index bf98fc7..ea5445e 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -18,7 +18,6 @@ if 'postgres' in DATABASES['default']['ENGINE']: LANGUAGE_CODE = 'en' A2_AUTH_SAML_ENABLE = False -A2_AUTH_FEDICT_ENABLE = True MELLON_ADAPTER = ["authentic2_auth_fedict.adapters.AuthenticAdapter"] MELLON_LOGIN_URL = "fedict-login" diff --git a/tests/test_all.py b/tests/test_all.py index 64b2f45..4d5d723 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -8,12 +8,15 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware +from django.db import connection +from django.db.migrations.executor import MigrationExecutor from django.test.client import RequestFactory from mellon.models import Issuer, UserSAMLIdentifier from utils import login from authentic2_auth_fedict.adapters import AuthenticAdapter from authentic2_auth_fedict.backends import FedictBackend +from authentic2_auth_fedict.models import FedictAuthenticator User = get_user_model() @@ -22,6 +25,11 @@ pytestmark = pytest.mark.django_db factory = RequestFactory() +@pytest.fixture +def authenticator(): + return FedictAuthenticator.objects.create(slug='fedict', enabled=True) + + @pytest.fixture def adapter(): return AuthenticAdapter() @@ -81,7 +89,7 @@ def test_custom_lookup_user(adapter, idp_conf, issuer, fedict_attributes): assert user.ou == get_default_ou() -def test_login_fedict_authenticator_displayed(app, settings, issuer): +def test_login_fedict_authenticator_displayed(app, settings, issuer, authenticator): response = app.get('/login/') assert 'Belgian eID' in response assert 'csam-login' in response @@ -175,7 +183,7 @@ def test_authenticate_eid_link_to_existing_user(app, settings, issuer, user): assert backend_user == user -def test_eid_unlink(app, settings, issuer, user): +def test_eid_unlink(app, settings, issuer, user, authenticator): assert len(UserSAMLIdentifier.objects.all()) == 0 # create link @@ -282,3 +290,49 @@ def test_provision_old_account_deleted(app, settings, issuer, user): # previous user as been deactivated assert User.objects.count() == count - 1 assert not User.objects.filter(uuid=user_uuid) + + +def test_fedict_authenticator_data_migration(settings): + app = 'authentic2_auth_fedict' + migrate_from = [(app, '0001_initial')] + migrate_to = [(app, '0002_auto_20220706_1712')] + + executor = MigrationExecutor(connection) + old_apps = executor.loader.project_state(migrate_from).apps + executor.migrate(migrate_from) + FedictAuthenticator = old_apps.get_model(app, 'FedictAuthenticator') + + settings.AUTH_FRONTENDS_KWARGS = { + "fedict": {"priority": 3, "show_condition": "'backoffice' not in login_hint"} + } + settings.A2_AUTH_FEDICT_ENABLE = True + + executor = MigrationExecutor(connection) + executor.migrate(migrate_to) + executor.loader.build_graph() + new_apps = executor.loader.project_state(migrate_to).apps + FedictAuthenticator = new_apps.get_model(app, 'FedictAuthenticator') + + authenticator = FedictAuthenticator.objects.get() + assert authenticator.slug == 'fedict-authenticator' + assert authenticator.order == 3 + assert authenticator.show_condition == "'backoffice' not in login_hint" + assert authenticator.enabled is True + + +def test_manager(app, admin): + resp = login(app, admin, path='/manage/authenticators/', index=0) + + resp = resp.click('Add new authenticator') + resp.form['authenticator'] = 'fedict' + resp = resp.form.submit().follow() + + authenticator = FedictAuthenticator.objects.get() + assert not authenticator.enabled + + # on edit page, submit + resp = resp.form.submit().follow() + resp = resp.click('Enable') + + authenticator.refresh_from_db() + assert authenticator.enabled diff --git a/tests/utils.py b/tests/utils.py index 42f9302..4fcff19 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,14 +2,14 @@ from authentic2.utils.misc import make_url from django.urls import reverse -def login(app, user, path=None, password=None, remember_me=None): +def login(app, user, path=None, password=None, remember_me=None, index=1): if path: real_path = make_url(path) login_page = app.get(real_path, status=302).maybe_follow() else: login_page = app.get(reverse('auth_login')) assert login_page.request.path == reverse('auth_login') - form = login_page.forms[1] + form = login_page.forms[index] form.set('username', user.username if hasattr(user, 'username') else user) # password is supposed to be the same as username form.set('password', password or user.username) -- 2.30.2