From ca015722694ad21a533ec8dca0101452d78007ca Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 23 Nov 2022 14:42:32 +0100 Subject: [PATCH 2/2] auth_fc: add flag to disable link by email (#68360) --- .../0008_fcauthenticator_link_by_email.py | 18 +++++++ src/authentic2_auth_fc/models.py | 10 +++- src/authentic2_auth_fc/views.py | 12 +++-- tests/auth_fc/conftest.py | 7 ++- tests/auth_fc/test_auth_fc.py | 49 ++++++++++++++----- 5 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 src/authentic2_auth_fc/migrations/0008_fcauthenticator_link_by_email.py diff --git a/src/authentic2_auth_fc/migrations/0008_fcauthenticator_link_by_email.py b/src/authentic2_auth_fc/migrations/0008_fcauthenticator_link_by_email.py new file mode 100644 index 00000000..2c5375c4 --- /dev/null +++ b/src/authentic2_auth_fc/migrations/0008_fcauthenticator_link_by_email.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2022-11-23 13:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentic2_auth_fc', '0007_auto_20220615_1002'), + ] + + operations = [ + migrations.AddField( + model_name='fcauthenticator', + name='link_by_email', + field=models.BooleanField(default=True, verbose_name='Link by email address'), + ), + ] diff --git a/src/authentic2_auth_fc/models.py b/src/authentic2_auth_fc/models.py index 304cc0bf..7b43d7c9 100644 --- a/src/authentic2_auth_fc/models.py +++ b/src/authentic2_auth_fc/models.py @@ -68,11 +68,19 @@ class FcAuthenticator(BaseAuthenticator): verbose_name=_('Scopes'), default=get_default_scopes, ) + link_by_email = models.BooleanField(_('Link by email address'), default=True) type = 'fc' how = ['france-connect'] unique = True - description_fields = ['show_condition', 'platform', 'client_id', 'client_secret', 'scopes'] + description_fields = [ + 'show_condition', + 'platform', + 'client_id', + 'client_secret', + 'scopes', + 'link_by_email', + ] class Meta: verbose_name = _('FranceConnect') diff --git a/src/authentic2_auth_fc/views.py b/src/authentic2_auth_fc/views.py index e9755a08..b0f93e56 100644 --- a/src/authentic2_auth_fc/views.py +++ b/src/authentic2_auth_fc/views.py @@ -60,7 +60,7 @@ logger = logging.getLogger(__name__) User = get_user_model() -class UserOutsideDefaultOu(Exception): +class EmailExistsError(Exception): pass @@ -429,9 +429,7 @@ class LoginOrLinkView(View): # try to create or find an user with this email try: user, created = self.get_or_create_user_with_email(email) - except UserOutsideDefaultOu: - user = None - except User.MultipleObjectsReturned: + except EmailExistsError: user = None if not user: messages.warning( @@ -549,11 +547,15 @@ class LoginOrLinkView(View): Lock.lock_email(email) try: user = qs.get_by_email(email) + if not self.authenticator.link_by_email: + raise EmailExistsError except User.DoesNotExist: return User.objects.create(ou=ou, email=email), True + except User.MultipleObjectsReturned: + raise EmailExistsError if user.ou != ou: - raise UserOutsideDefaultOu + raise EmailExistsError return user, False diff --git a/tests/auth_fc/conftest.py b/tests/auth_fc/conftest.py index 8177a3e6..2f6f5189 100644 --- a/tests/auth_fc/conftest.py +++ b/tests/auth_fc/conftest.py @@ -166,8 +166,8 @@ def service(db): @pytest.fixture -def franceconnect(settings, service, db): - FcAuthenticator.objects.create( +def authenticator(db): + return FcAuthenticator.objects.create( enabled=True, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, @@ -175,6 +175,9 @@ def franceconnect(settings, service, db): scopes=['profile', 'email'], ) + +@pytest.fixture +def franceconnect(settings, authenticator, service, db): mock_object = FranceConnectMock() with mock_object(): yield mock_object diff --git a/tests/auth_fc/test_auth_fc.py b/tests/auth_fc/test_auth_fc.py index a3852306..7646b45c 100644 --- a/tests/auth_fc/test_auth_fc.py +++ b/tests/auth_fc/test_auth_fc.py @@ -192,17 +192,44 @@ def test_create_expired(settings, app, franceconnect, hooks): assert User.objects.count() == 0 -def test_login_email_is_unique(settings, app, franceconnect, caplog): - settings.A2_EMAIL_IS_UNIQUE = True - user = User(email='john.doe@example.com', first_name='John', last_name='Doe', ou=get_default_ou()) - user.set_password('toto') - user.save() - franceconnect.user_info['email'] = user.email - - assert User.objects.count() == 1 - franceconnect.login_with_fc_fixed_params(app) - assert User.objects.count() == 1 - assert app.session['_auth_user_id'] == str(user.pk) +class TestLinkByEmail: + @pytest.fixture + def franceconnect(self, franceconnect): + franceconnect.callback_params = {'next': '/accounts/'} + return franceconnect + + def test_enabled(self, settings, app, franceconnect, authenticator, caplog): + authenticator.link_by_email = True + authenticator.save() + + user = User(email='john.doe@example.com', first_name='John', last_name='Doe', ou=get_default_ou()) + user.set_password('toto') + user.save() + franceconnect.user_info['email'] = user.email + + assert User.objects.count() == 1 + franceconnect.login_with_fc_fixed_params(app) + assert User.objects.count() == 1 + assert '_auth_user_id' in app.session + + def test_disabled(self, settings, app, franceconnect, authenticator, caplog): + authenticator.link_by_email = False + authenticator.save() + + user = User(email='john.doe@example.com', first_name='John', last_name='Doe', ou=get_default_ou()) + user.set_password('toto') + user.save() + franceconnect.user_info['email'] = user.email + + assert User.objects.count() == 1 + response = franceconnect.login_with_fc_fixed_params(app) + assert User.objects.count() == 1 + assert '_auth_user_id' not in app.session + + # no login, so we must have produced a logout request toward FC + response = franceconnect.handle_logout(app, response.location) + response = response.maybe_follow() + assert 'Your FranceConnect email address' in response.pyquery('.messages .warning').text() def test_link_after_login_with_password(app, franceconnect, simple_user): -- 2.37.2