migrate authenticator to database (#66876)

This commit is contained in:
Valentin Deniaud 2022-07-06 17:18:21 +02:00
parent 803d0cb3e5
commit 86033d32eb
11 changed files with 167 additions and 66 deletions

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -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 <http://www.gnu.org/licenses/>.
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')

View File

@ -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',),
),
]

View File

@ -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),
]

View File

@ -14,7 +14,7 @@
# 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/>.
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
def enabled(self):
return app_settings.enable and list(get_idps())
class Meta:
verbose_name = _('Belgian eID')
def name(self):
return _('Belgian eID')
@property
def manager_form_class(self):
from .forms import FedictAuthenticatorForm
return FedictAuthenticatorForm
def login(self, request, *args, **kwargs):
context = kwargs.get('context', {}).copy()

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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)