From 021d50fadeb755054ccc6104702e3c60293ec8cf Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 22 Apr 2018 15:31:41 +0200 Subject: [PATCH 01/47] An adapter to handle the 'end of scolarity' problem --- allauth_ens/adapter.py | 132 ++++++++++++++++++ allauth_ens/management/__init__.py | 0 allauth_ens/management/commands/__init__.py | 0 .../management/commands/deprecate_clippers.py | 14 ++ allauth_ens/providers/clipper/provider.py | 37 +---- example/adapter.py | 4 +- 6 files changed, 153 insertions(+), 34 deletions(-) create mode 100644 allauth_ens/adapter.py create mode 100644 allauth_ens/management/__init__.py create mode 100644 allauth_ens/management/commands/__init__.py create mode 100644 allauth_ens/management/commands/deprecate_clippers.py diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py new file mode 100644 index 0000000..e99287d --- /dev/null +++ b/allauth_ens/adapter.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +import ldap + +from allauth.account.utils import user_email, user_field, user_username +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from allauth.socialaccount.models import SocialAccount + +DEPARTMENTS_LIST = ( + ('phy', u'Physique'), + ('maths', u'Maths'), + ('bio', u'Biologie'), + ('chimie', u'Chimie'), + ('geol', u'Géosciences'), + ('dec', u'DEC'), + ('info', u'Informatique'), + ('litt', u'Littéraire'), + ('guests', u'Pensionnaires étrangers'), + ('pei', u'PEI'), +) + +def get_ldap_infos(clipper): + assert clipper.isalnum() + data = {'email':'{}@clipper.ens.fr'.format(clipper.strip().lower())} + try: + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, + ldap.OPT_X_TLS_NEVER) + l = ldap.initialize("ldaps://ldap.spi.ens.fr:636") + l.set_option(ldap.OPT_REFERRALS, 0) + l.set_option(ldap.OPT_PROTOCOL_VERSION, 3) + l.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) + l.set_option(ldap.OPT_X_TLS_DEMAND, True) + l.set_option(ldap.OPT_DEBUG_LEVEL, 255) + l.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) + l.set_option(ldap.OPT_TIMEOUT, 10) + + info = l.search_s('dc=spi,dc=ens,dc=fr', + ldap.SCOPE_SUBTREE, + ('(uid=%s)' % (clipper,)), + [str("cn"), + str("mailRoutingAddress"), + str("homeDirectory") ]) + + if len(info) > 0: + infos = info[0][1] + + # Name + data['name'] = infos.get('cn', [''])[0].decode("utf-8") + + # Parsing homeDirectory to get entrance year and departments + annee = '00' + promotion = 'Inconnue' + + if 'homeDirectory' in infos: + dirs = infos['homeDirectory'][0].split('/') + if dirs[1] == 'users': + annee = dirs[2] + dep = dirs[3] + dep = dict(DEPARTMENTS_LIST).get(dep.lower(), '') + promotion = u'%s %s' % (dep, annee) + data['annee'] = annee + data['promotion'] = promotion + + # Mail + pmail = infos.get('mailRoutingAddress', []) + if len(pmail) > 0 : + data['email'] = pmail[0] + + except ldap.LDAPError: + pass + + return data + + +class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): + """ + A class to manage the fact that people loose their account at the end of + their scolarity and that their clipper login might be reused later + """ + + def pre_social_login(self, request, sociallogin): + clipper = sociallogin.account.uid + try: + a = SocialAccount.objects.get(provider='old_clipper', + uid=clipper) + # An account with that uid was registered, but potentially + # deprecated at the beginning of the year + # We need to check that the user is still the same as before + ldap_data = get_ldap_infos(clipper) + self._ldap_data = ldap_data + if a.extra_data.get('annee', '-1') != ldap_data.get('annee', '00'): + # The admission year is different + # We need a new SocialAccount + return + + # The admission year is the same, we can update the model + a.provider = 'clipper' + a.save() + sociallogin.lookup() + except SocialAccount.DoesNotExist: + return + + def create_username(self, clipper, data): + return "{}:{}".format(clipper, data.get('annee', '00')) + + def populate_user(self, request, sociallogin, data): + clipper = sociallogin.account.uid + ldap_data = self._ldap_data if hasattr(self, '_ldap_data') else get_ldap_infos(clipper) + # Save extra data (only once) + sociallogin.account.extra_data = sociallogin.extra_data = ldap_data + username = self.create_username(clipper, data) + first_name = data.get('first_name') + last_name = data.get('last_name') + email = ldap_data.get('email') + name = ldap_data.get('name') + user = sociallogin.user + user_username(user, username or '') + user_email(user, email or '') + name_parts = (name or '').split(' ') + user_field(user, 'first_name', first_name or name_parts[0]) + user_field(user, 'last_name', last_name or ' '.join(name_parts[1:])) + print(user.username, user) + return user + +def deprecate_clippers(): + clippers = SocialAccount.objects.filter(provider='clipper') + c_uids = clippers.values_list('uid', flat=True) + + # Clear old clipper accounts that wer replaced by new ones (o avoid conflicts) + SocialAccount.objects.filter(provider='old_clipper', uid__in=c_uids).delete() + + # Deprecate accounts + clippers.update(provider='old_clipper') diff --git a/allauth_ens/management/__init__.py b/allauth_ens/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/allauth_ens/management/commands/__init__.py b/allauth_ens/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/allauth_ens/management/commands/deprecate_clippers.py b/allauth_ens/management/commands/deprecate_clippers.py new file mode 100644 index 0000000..a824fd8 --- /dev/null +++ b/allauth_ens/management/commands/deprecate_clippers.py @@ -0,0 +1,14 @@ +#coding: utf-8 +from django.core.management.base import BaseCommand, CommandError + +from allauth_ens.adapter import deprecate_clippers + +class Command(BaseCommand): + help = 'Deprecates clipper SocialAccounts so as to avoid conflicts' + + def add_arguments(self, parser): + pass + + def handle(self, *args, **options): + deprecate_clippers() + self.stdout.write(self.style.SUCCESS(u'Clippers deprecation successful')) diff --git a/allauth_ens/providers/clipper/provider.py b/allauth_ens/providers/clipper/provider.py index d14bee2..fa057b2 100644 --- a/allauth_ens/providers/clipper/provider.py +++ b/allauth_ens/providers/clipper/provider.py @@ -21,41 +21,14 @@ class ClipperProvider(CASProvider): uid, extra = data return '{}@clipper.ens.fr'.format(uid.strip().lower()) + def extract_uid(self, data): + uid, _ = data + uid = uid.lower().strip() + return uid + def extract_common_fields(self, data): - def get_names(clipper): - assert clipper.isalnum() - try: - ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, - ldap.OPT_X_TLS_NEVER) - l = ldap.initialize("ldaps://ldap.spi.ens.fr:636") - l.set_option(ldap.OPT_REFERRALS, 0) - l.set_option(ldap.OPT_PROTOCOL_VERSION, 3) - l.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) - l.set_option(ldap.OPT_X_TLS_DEMAND, True) - l.set_option(ldap.OPT_DEBUG_LEVEL, 255) - l.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) - l.set_option(ldap.OPT_TIMEOUT, 10) - - info = l.search_s('dc=spi,dc=ens,dc=fr', - ldap.SCOPE_SUBTREE, - ('(uid=%s)' % (clipper,)), - [str("cn"), ]) - - if len(info) > 0: - fullname = info[0][1].get('cn', [''])[0].decode("utf-8") - first_name, last_name = fullname.split(' ', 1) - return first_name, last_name - - except ldap.LDAPError: - pass - - return '', '' - common = super(ClipperProvider, self).extract_common_fields(data) - fn, ln = get_names(common['username']) common['email'] = self.extract_email(data) - common['name'] = fn - common['last_name'] = ln return common def extract_email_addresses(self, data): diff --git a/example/adapter.py b/example/adapter.py index 3e288e5..f1079f5 100644 --- a/example/adapter.py +++ b/example/adapter.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from allauth.account.adapter import DefaultAccountAdapter +from allauth_ens.adapter import LongTermClipperAccountAdapter from allauth.socialaccount.adapter import DefaultSocialAccountAdapter - class AccountAdapter(DefaultAccountAdapter): pass -class SocialAccountAdapter(DefaultSocialAccountAdapter): +class SocialAccountAdapter(LongTermClipperAccountAdapter): pass From cdb9c3c722784247f03da32ba5f56e5df845428d Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 22 Apr 2018 17:22:03 +0200 Subject: [PATCH 02/47] Mail address disambiguation, debug, only one ldap query --- allauth_ens/adapter.py | 67 ++++++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index e99287d..92fe990 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -2,7 +2,8 @@ import ldap from allauth.account.utils import user_email, user_field, user_username -from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from allauth.account.models import EmailAddress +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter, get_account_adapter from allauth.socialaccount.models import SocialAccount DEPARTMENTS_LIST = ( @@ -87,38 +88,74 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): # We need to check that the user is still the same as before ldap_data = get_ldap_infos(clipper) self._ldap_data = ldap_data - if a.extra_data.get('annee', '-1') != ldap_data.get('annee', '00'): + + if a.user.username != self.get_username(clipper, ldap_data): # The admission year is different # We need a new SocialAccount + # But before that, we need to invalidate the email address of + # the previous user + email = ldap_data.get('email') + print(email, 'deprecate') + u_mails = EmailAddress.objects.filter(user=a.user) + try: + clipper_mail = u_mails.get(email=email) + if clipper_mail.primary: + n_mails = u_mails.filter(primary=False) + if n_mails.exists(): + n_mails[0].set_as_primary() + else: + user_email(a.user, '') + a.user.save() + clipper_mail.delete() + except EmailAddress.DoesNotExist: + pass return - # The admission year is the same, we can update the model + # The admission year is the same, we can update the model and keep + # the previous SocialAccount instance a.provider = 'clipper' a.save() + + # Redo the thing that had failed just before sociallogin.lookup() + except SocialAccount.DoesNotExist: return - def create_username(self, clipper, data): - return "{}:{}".format(clipper, data.get('annee', '00')) + def get_username(self, clipper, data): + """ + Util function to generate a unique username, by default 'clipper@promo' + This is used to disambiguate and recognize if the person is the same + """ + return "{}@{}".format(clipper, data.get('annee', '00')) + + def save_user(self, request, sociallogin, form=None): + print("populate user", sociallogin.account.uid) + user = sociallogin.user + user.set_unusable_password() - def populate_user(self, request, sociallogin, data): clipper = sociallogin.account.uid - ldap_data = self._ldap_data if hasattr(self, '_ldap_data') else get_ldap_infos(clipper) - # Save extra data (only once) - sociallogin.account.extra_data = sociallogin.extra_data = ldap_data - username = self.create_username(clipper, data) - first_name = data.get('first_name') - last_name = data.get('last_name') + ldap_data = self._ldap_data if hasattr(self, '_ldap_data') \ + else get_ldap_infos(clipper) + + username = self.get_username(clipper, ldap_data) email = ldap_data.get('email') name = ldap_data.get('name') - user = sociallogin.user user_username(user, username or '') user_email(user, email or '') name_parts = (name or '').split(' ') - user_field(user, 'first_name', first_name or name_parts[0]) - user_field(user, 'last_name', last_name or ' '.join(name_parts[1:])) + user_field(user, 'first_name', name_parts[0]) + user_field(user, 'last_name', ' '.join(name_parts[1:])) print(user.username, user) + + # Ignore form + get_account_adapter().populate_username(request, user) + + # Save extra data (only once) + sociallogin.account.extra_data = sociallogin.extra_data = ldap_data + sociallogin.save(request) + sociallogin.account.save() + return user def deprecate_clippers(): From 5a78561d175a869f5212a2bec797ec31fc6183ee Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 22 Apr 2018 20:13:42 +0200 Subject: [PATCH 03/47] WIP tests --- allauth_ens/adapter.py | 32 ++++++++----- allauth_ens/tests.py | 103 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 15 deletions(-) diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index 92fe990..b7a22ab 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -5,6 +5,9 @@ from allauth.account.utils import user_email, user_field, user_username from allauth.account.models import EmailAddress from allauth.socialaccount.adapter import DefaultSocialAccountAdapter, get_account_adapter from allauth.socialaccount.models import SocialAccount +from django.conf import settings + +import six DEPARTMENTS_LIST = ( ('phy', u'Physique'), @@ -19,20 +22,25 @@ DEPARTMENTS_LIST = ( ('pei', u'PEI'), ) +def _init_ldap(): + server = getattr(settings, "LDAP_SERVER", "ldaps://ldap.spi.ens.fr:636") + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, + ldap.OPT_X_TLS_NEVER) + l = ldap.initialize(server) + l.set_option(ldap.OPT_REFERRALS, 0) + l.set_option(ldap.OPT_PROTOCOL_VERSION, 3) + l.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) + l.set_option(ldap.OPT_X_TLS_DEMAND, True) + l.set_option(ldap.OPT_DEBUG_LEVEL, 255) + l.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) + l.set_option(ldap.OPT_TIMEOUT, 10) + return l + def get_ldap_infos(clipper): assert clipper.isalnum() data = {'email':'{}@clipper.ens.fr'.format(clipper.strip().lower())} try: - ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, - ldap.OPT_X_TLS_NEVER) - l = ldap.initialize("ldaps://ldap.spi.ens.fr:636") - l.set_option(ldap.OPT_REFERRALS, 0) - l.set_option(ldap.OPT_PROTOCOL_VERSION, 3) - l.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) - l.set_option(ldap.OPT_X_TLS_DEMAND, True) - l.set_option(ldap.OPT_DEBUG_LEVEL, 255) - l.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) - l.set_option(ldap.OPT_TIMEOUT, 10) + l = _init_ldap() info = l.search_s('dc=spi,dc=ens,dc=fr', ldap.SCOPE_SUBTREE, @@ -163,7 +171,7 @@ def deprecate_clippers(): c_uids = clippers.values_list('uid', flat=True) # Clear old clipper accounts that wer replaced by new ones (o avoid conflicts) - SocialAccount.objects.filter(provider='old_clipper', uid__in=c_uids).delete() + SocialAccount.objects.filter(provider='clipper_inactive', uid__in=c_uids).delete() # Deprecate accounts - clippers.update(provider='old_clipper') + clippers.update(provider='clipper_inactive') diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index ef2a98c..ee69c3e 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -6,6 +6,17 @@ from django.contrib.sites.models import Site from django.core import mail from django.test import TestCase, override_settings +from mock import patch +from fakeldap import MockLDAP + + +from allauth_cas.test.testcases import CASTestCase, CASViewTestCase +from .adapter import deprecate_clippers +from allauth.socialaccount.models import SocialAccount + +_mock_ldap = MockLDAP() +ldap_patcher = patch('allauth_ens.adapter.ldap.initialize', lambda x: _mock_ldap) + if django.VERSION >= (1, 10): from django.urls import reverse else: @@ -27,11 +38,11 @@ def prevent_logout_pwd_change(client, user): session[HASH_SESSION_KEY] = user.get_session_auth_hash() session.save() - +""" class ViewsTests(TestCase): - """ + "" Checks (barely) that templates do not contain errors. - """ + "" def setUp(self): self.u = User.objects.create_user('user', 'user@mail.net', 'user') @@ -135,3 +146,89 @@ class ViewsTests(TestCase): def test_account_reset_password_from_key_done(self): r = self.client.get(reverse('account_reset_password_from_key_done')) self.assertEqual(r.status_code, 200) +""" + +class LongTermClipperTests(CASTestCase): + + def setUp(self): + ldap_patcher.start() + + def tearDown(self): + _mock_ldap.reset() + + def _setup_ldap(self, promo=12): + _mock_ldap.set_return_value('search_s', + ('dc=spi,dc=ens,dc=fr,uid=test'), + ( + ('cn', ('John Smith')), + ('mailRoutingAddress', ('test@clipper.ens.fr')), + ('homeDirectory', ("/users/%d/phy/test/" % promo)) + )) + + + def test_new_connexion(self): + self._setup_ldap() + + r = self.client_cas_login(self.client, provider_id="clipper", username="test") + u = r.context['user'] + + self.assertEqual(u.username, "test@12") + self.assertEqual(u.first_name, "John") + self.assertEqual(u.last_name, "Smith") + self.assertEqual(u.email, "test@clipper.ens.fr") + + sa = list(SocialAccount.objects.all())[-1] + self.assertEqual(sa.user.id, u.id) + + def test_second_connexion(self): + self._setup_ldap() + + self.client_cas_login(self.client, provider_id="clipper", username="test") + self.client.logout() + + nu = User.objects.count() + + self.client_cas_login(self.client, provider_id="clipper", username="test") + self.assertEqual(User.objects.count(), nu) + + def test_deprecation(self): + self._setup_ldap() + self.client_cas_login(self.client, provider_id="clipper", username="test") + deprecate_clippers() + + sa = SocialAccount.objects.all()[0] + self.assertEqual(sa.provider, "clipper_inactive") + + def test_reconnect_after_deprecation(self): + self._setup_ldap() + self.client_cas_login(self.client, provider_id="clipper", username="test") + nsa = SocialAccount.objects.count() + nu = User.objects.count() + self.client.logout() + + deprecate_clippers() + self.client_cas_login(self.client, provider_id="clipper", username="test") + + sa = SocialAccount.objects.all() + self.assertEqual(len(sa), nsa) + u = User.objects.all() + self.assertEqual(len(u), nu) + self.assertEqual(sa[-1].user.id, u[-1].id) + + def test_override_inactive_account(self): + self._setup_ldap(12) + self.client_cas_login(self.client, provider_id="clipper", username="test") + nsa = SocialAccount.objects.count() + nu = User.objects.count() + self.client.logout() + + deprecate_clippers() + + self._setup_ldap(13) + self.client_cas_login(self.client, provider_id="clipper", username="test") + + sa = SocialAccount.objects.all() + self.assertEqual(len(sa), nsa+1) + u = User.objects.all() + self.assertEqual(len(u), nu+1) + self.assertEqual(sa[-1].user.id, u[-1].id) From dc8873cafc68bc4159880c330b8321fcd8146d70 Mon Sep 17 00:00:00 2001 From: Evarin Date: Mon, 23 Apr 2018 00:01:27 +0200 Subject: [PATCH 04/47] Tests for adapter ready --- allauth_ens/adapter.py | 7 ++--- allauth_ens/tests.py | 69 ++++++++++++++++++++++++++---------------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index b7a22ab..c08382f 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -89,7 +89,7 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): def pre_social_login(self, request, sociallogin): clipper = sociallogin.account.uid try: - a = SocialAccount.objects.get(provider='old_clipper', + a = SocialAccount.objects.get(provider='clipper_inactive', uid=clipper) # An account with that uid was registered, but potentially # deprecated at the beginning of the year @@ -103,7 +103,6 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): # But before that, we need to invalidate the email address of # the previous user email = ldap_data.get('email') - print(email, 'deprecate') u_mails = EmailAddress.objects.filter(user=a.user) try: clipper_mail = u_mails.get(email=email) @@ -138,7 +137,6 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): return "{}@{}".format(clipper, data.get('annee', '00')) def save_user(self, request, sociallogin, form=None): - print("populate user", sociallogin.account.uid) user = sociallogin.user user.set_unusable_password() @@ -154,8 +152,7 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): name_parts = (name or '').split(' ') user_field(user, 'first_name', name_parts[0]) user_field(user, 'last_name', ' '.join(name_parts[1:])) - print(user.username, user) - + # Ignore form get_account_adapter().populate_username(request, user) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index ee69c3e..d64d229 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -157,13 +157,13 @@ class LongTermClipperTests(CASTestCase): _mock_ldap.reset() def _setup_ldap(self, promo=12): - _mock_ldap.set_return_value('search_s', - ('dc=spi,dc=ens,dc=fr,uid=test'), - ( - ('cn', ('John Smith')), - ('mailRoutingAddress', ('test@clipper.ens.fr')), - ('homeDirectory', ("/users/%d/phy/test/" % promo)) - )) + import ldap + _mock_ldap.directory['dc=spi,dc=ens,dc=fr']={ + 'uid': ['test'], + 'cn': ['John Smith'], + 'mailRoutingAddress' : ['test@clipper.ens.fr'], + 'homeDirectory': ["/users/%d/phy/test/" % promo], + } def test_new_connexion(self): @@ -180,6 +180,15 @@ class LongTermClipperTests(CASTestCase): sa = list(SocialAccount.objects.all())[-1] self.assertEqual(sa.user.id, u.id) + def test_connect_disconnect(self): + self._setup_ldap() + r0 = self.client_cas_login(self.client, provider_id="clipper", username="test") + self.assertIn("_auth_user_id", self.client.session) + self.assertIn('user', r0.context) + + r1 = self.client.logout() + self.assertNotIn("_auth_user_id", self.client.session) + def test_second_connexion(self): self._setup_ldap() @@ -201,34 +210,42 @@ class LongTermClipperTests(CASTestCase): def test_reconnect_after_deprecation(self): self._setup_ldap() - self.client_cas_login(self.client, provider_id="clipper", username="test") - nsa = SocialAccount.objects.count() - nu = User.objects.count() + r = self.client_cas_login(self.client, provider_id="clipper", + username="test") + user0 = r.context['user'] + n_sa0 = SocialAccount.objects.count() + n_u0 = User.objects.count() self.client.logout() deprecate_clippers() - self.client_cas_login(self.client, provider_id="clipper", username="test") - - sa = SocialAccount.objects.all() - self.assertEqual(len(sa), nsa) - u = User.objects.all() - self.assertEqual(len(u), nu) - self.assertEqual(sa[-1].user.id, u[-1].id) + + r = self.client_cas_login(self.client, provider_id="clipper", + username="test") + user1 = r.context['user'] + sa1 = list(SocialAccount.objects.all()) + n_u1 = User.objects.count() + self.assertEqual(len(sa1), n_sa0) + self.assertEqual(n_u1, n_u0) + self.assertEqual(user1.id, user0.id) def test_override_inactive_account(self): self._setup_ldap(12) - self.client_cas_login(self.client, provider_id="clipper", username="test") - nsa = SocialAccount.objects.count() - nu = User.objects.count() + r = self.client_cas_login(self.client, provider_id="clipper", + username="test") + user0 = r.context['user'] + n_sa0 = SocialAccount.objects.count() + n_u0 = User.objects.count() self.client.logout() deprecate_clippers() self._setup_ldap(13) - self.client_cas_login(self.client, provider_id="clipper", username="test") + r = self.client_cas_login(self.client, provider_id="clipper", + username="test") - sa = SocialAccount.objects.all() - self.assertEqual(len(sa), nsa+1) - u = User.objects.all() - self.assertEqual(len(u), nu+1) - self.assertEqual(sa[-1].user.id, u[-1].id) + user1 = r.context['user'] + sa1 = list(SocialAccount.objects.all()) + n_u1 = User.objects.count() + self.assertEqual(len(sa1), n_sa0+1) + self.assertEqual(n_u1, n_u0+1) + self.assertNotEqual(user1.id, user0.id) From 54963e9f915c922dc5ed4787e8f4ab5fbe2ee02b Mon Sep 17 00:00:00 2001 From: Evarin Date: Mon, 23 Apr 2018 00:22:50 +0200 Subject: [PATCH 05/47] Fix other tests --- allauth_ens/providers/clipper/tests.py | 22 +++++++++++----------- allauth_ens/tests.py | 25 +++++++++++++++---------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/allauth_ens/providers/clipper/tests.py b/allauth_ens/providers/clipper/tests.py index 657982c..32266ac 100644 --- a/allauth_ens/providers/clipper/tests.py +++ b/allauth_ens/providers/clipper/tests.py @@ -8,23 +8,23 @@ User = get_user_model() class ClipperProviderTests(CASTestCase): def setUp(self): - self.u = User.objects.create_user('user', 'user@mail.net', 'user') + pass def test_auto_signup(self): self.client_cas_login( - self.client, provider_id='clipper', username='clipper_uid') + self.client, provider_id='clipper', username='clipperuid') - u = User.objects.get(username='clipper_uid') - self.assertEqual(u.email, 'clipper_uid@clipper.ens.fr') + u = User.objects.get(username__contains='clipperuid') + self.assertEqual(u.email, 'clipperuid@clipper.ens.fr') class ClipperViewsTests(CASViewTestCase): def test_login_view(self): - r = self.client.get('/accounts/clipper/login/') + r = self.client.get('/account/clipper/login/') expected = ( "https://cas.eleves.ens.fr/login?service=http%3A%2F%2Ftestserver" - "%2Faccounts%2Fclipper%2Flogin%2Fcallback%2F" + "%2Faccount%2Fclipper%2Flogin%2Fcallback%2F" ) self.assertRedirects( r, expected, @@ -33,20 +33,20 @@ class ClipperViewsTests(CASViewTestCase): def test_callback_view(self): # Required to initialize a SocialLogin. - r = self.client.get('/accounts/clipper/login/') + r = self.client.get('/account/clipper/login/') # Tests. self.patch_cas_response(valid_ticket='__all__') - r = self.client.get('/accounts/clipper/login/callback/', { + r = self.client.get('/account/clipper/login/callback/', { 'ticket': '123456', }) - self.assertLoginSuccess(r) + self.assertLoginSuccess(r, "/user/") def test_logout_view(self): - r = self.client.get('/accounts/clipper/logout/') + r = self.client.get('/account/clipper/logout/') expected = ( "https://cas.eleves.ens.fr/logout?service=http%3A%2F%2Ftestserver" - "%2F" + "%2Fview%2F" ) self.assertRedirects( r, expected, diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index d64d229..96d95fb 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -38,14 +38,19 @@ def prevent_logout_pwd_change(client, user): session[HASH_SESSION_KEY] = user.get_session_auth_hash() session.save() -""" -class ViewsTests(TestCase): - "" - Checks (barely) that templates do not contain errors. - "" - def setUp(self): - self.u = User.objects.create_user('user', 'user@mail.net', 'user') +class ViewsTests(TestCase): + """ + Checks (barely) that templates do not contain errors. + """ + def setUp(self): + try: + self.u = User.objects.get(username="user") + self.u.email = "user@mail.net" + self.u.save() + except User.DoesNotExist: + self.u = User.objects.create_user('user', 'user@mail.net', 'user') + Site.objects.filter(pk=1).update(domain='testserver') def _login(self, client=None): @@ -55,14 +60,14 @@ class ViewsTests(TestCase): def _get_confirm_email_link(self, email_msg): m = re.search( - r'http://testserver(/accounts/confirm-email/.*/)', + r'http://testserver(/account/confirm-email/.*/)', email_msg.body, ) return m.group(1) def _get_reset_password_link(self, email_msg): m = re.search( - r'http://testserver(/accounts/password/reset/key/.*/)', + r'http://testserver(/account/password/reset/key/.*/)', email_msg.body, ) return m.group(1) @@ -146,7 +151,7 @@ class ViewsTests(TestCase): def test_account_reset_password_from_key_done(self): r = self.client.get(reverse('account_reset_password_from_key_done')) self.assertEqual(r.status_code, 200) -""" + class LongTermClipperTests(CASTestCase): From 1142db73f77ae3788ea590a007b6952e1085a8bd Mon Sep 17 00:00:00 2001 From: Evarin Date: Mon, 23 Apr 2018 00:35:57 +0200 Subject: [PATCH 06/47] Revert "Fix other tests" This reverts commit 54963e9f915c922dc5ed4787e8f4ab5fbe2ee02b. --- allauth_ens/providers/clipper/tests.py | 22 +++++++++++----------- allauth_ens/tests.py | 21 ++++++++------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/allauth_ens/providers/clipper/tests.py b/allauth_ens/providers/clipper/tests.py index 32266ac..657982c 100644 --- a/allauth_ens/providers/clipper/tests.py +++ b/allauth_ens/providers/clipper/tests.py @@ -8,23 +8,23 @@ User = get_user_model() class ClipperProviderTests(CASTestCase): def setUp(self): - pass + self.u = User.objects.create_user('user', 'user@mail.net', 'user') def test_auto_signup(self): self.client_cas_login( - self.client, provider_id='clipper', username='clipperuid') + self.client, provider_id='clipper', username='clipper_uid') - u = User.objects.get(username__contains='clipperuid') - self.assertEqual(u.email, 'clipperuid@clipper.ens.fr') + u = User.objects.get(username='clipper_uid') + self.assertEqual(u.email, 'clipper_uid@clipper.ens.fr') class ClipperViewsTests(CASViewTestCase): def test_login_view(self): - r = self.client.get('/account/clipper/login/') + r = self.client.get('/accounts/clipper/login/') expected = ( "https://cas.eleves.ens.fr/login?service=http%3A%2F%2Ftestserver" - "%2Faccount%2Fclipper%2Flogin%2Fcallback%2F" + "%2Faccounts%2Fclipper%2Flogin%2Fcallback%2F" ) self.assertRedirects( r, expected, @@ -33,20 +33,20 @@ class ClipperViewsTests(CASViewTestCase): def test_callback_view(self): # Required to initialize a SocialLogin. - r = self.client.get('/account/clipper/login/') + r = self.client.get('/accounts/clipper/login/') # Tests. self.patch_cas_response(valid_ticket='__all__') - r = self.client.get('/account/clipper/login/callback/', { + r = self.client.get('/accounts/clipper/login/callback/', { 'ticket': '123456', }) - self.assertLoginSuccess(r, "/user/") + self.assertLoginSuccess(r) def test_logout_view(self): - r = self.client.get('/account/clipper/logout/') + r = self.client.get('/accounts/clipper/logout/') expected = ( "https://cas.eleves.ens.fr/logout?service=http%3A%2F%2Ftestserver" - "%2Fview%2F" + "%2F" ) self.assertRedirects( r, expected, diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index 96d95fb..d64d229 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -38,19 +38,14 @@ def prevent_logout_pwd_change(client, user): session[HASH_SESSION_KEY] = user.get_session_auth_hash() session.save() - +""" class ViewsTests(TestCase): - """ + "" Checks (barely) that templates do not contain errors. - """ + "" def setUp(self): - try: - self.u = User.objects.get(username="user") - self.u.email = "user@mail.net" - self.u.save() - except User.DoesNotExist: - self.u = User.objects.create_user('user', 'user@mail.net', 'user') - + self.u = User.objects.create_user('user', 'user@mail.net', 'user') + Site.objects.filter(pk=1).update(domain='testserver') def _login(self, client=None): @@ -60,14 +55,14 @@ class ViewsTests(TestCase): def _get_confirm_email_link(self, email_msg): m = re.search( - r'http://testserver(/account/confirm-email/.*/)', + r'http://testserver(/accounts/confirm-email/.*/)', email_msg.body, ) return m.group(1) def _get_reset_password_link(self, email_msg): m = re.search( - r'http://testserver(/account/password/reset/key/.*/)', + r'http://testserver(/accounts/password/reset/key/.*/)', email_msg.body, ) return m.group(1) @@ -151,7 +146,7 @@ class ViewsTests(TestCase): def test_account_reset_password_from_key_done(self): r = self.client.get(reverse('account_reset_password_from_key_done')) self.assertEqual(r.status_code, 200) - +""" class LongTermClipperTests(CASTestCase): From 6fc012cb3926658168d6c548b6dccd5536adb872 Mon Sep 17 00:00:00 2001 From: Evarin Date: Mon, 23 Apr 2018 00:38:37 +0200 Subject: [PATCH 07/47] Fix my tests --- allauth_ens/tests.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index d64d229..6a86c6b 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -9,7 +9,6 @@ from django.test import TestCase, override_settings from mock import patch from fakeldap import MockLDAP - from allauth_cas.test.testcases import CASTestCase, CASViewTestCase from .adapter import deprecate_clippers from allauth.socialaccount.models import SocialAccount @@ -38,11 +37,10 @@ def prevent_logout_pwd_change(client, user): session[HASH_SESSION_KEY] = user.get_session_auth_hash() session.save() -""" class ViewsTests(TestCase): - "" + """ Checks (barely) that templates do not contain errors. - "" + """ def setUp(self): self.u = User.objects.create_user('user', 'user@mail.net', 'user') @@ -146,8 +144,8 @@ class ViewsTests(TestCase): def test_account_reset_password_from_key_done(self): r = self.client.get(reverse('account_reset_password_from_key_done')) self.assertEqual(r.status_code, 200) -""" +@override_settings(SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter') class LongTermClipperTests(CASTestCase): def setUp(self): From 9c3fc72ec09c45f77ce9f20f3f848ebca5d43d33 Mon Sep 17 00:00:00 2001 From: Evarin Date: Mon, 23 Apr 2018 01:13:03 +0200 Subject: [PATCH 08/47] One more test case --- allauth_ens/tests.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index 6a86c6b..cafac82 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -154,10 +154,10 @@ class LongTermClipperTests(CASTestCase): def tearDown(self): _mock_ldap.reset() - def _setup_ldap(self, promo=12): + def _setup_ldap(self, promo=12, username="test"): import ldap _mock_ldap.directory['dc=spi,dc=ens,dc=fr']={ - 'uid': ['test'], + 'uid': [username], 'cn': ['John Smith'], 'mailRoutingAddress' : ['test@clipper.ens.fr'], 'homeDirectory': ["/users/%d/phy/test/" % promo], @@ -247,3 +247,31 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(len(sa1), n_sa0+1) self.assertEqual(n_u1, n_u0+1) self.assertNotEqual(user1.id, user0.id) + + def test_multiple_deprecation(self): + self._setup_ldap(12) + self._setup_ldap(15, "truc") + r = self.client_cas_login(self.client, provider_id="clipper", + username="test") + self.client.logout() + r = self.client_cas_login(self.client, provider_id="clipper", + username="truc") + self.client.logout() + sa0 = SocialAccount.objects.count() + + deprecate_clippers() + + self._setup_ldap(13) + r = self.client_cas_login(self.client, provider_id="clipper", + username="test") + self.client.logout() + + sa1 = SocialAccount.objects.count() + + deprecate_clippers() + sa2 = SocialAccount.objects.count() + + # Older "test" inactive SocialAccount gets erased by new one + # while "truc" remains + self.assertEqual(sa0, sa2) + self.assertEqual(sa1, sa0+1) From b6f5acaa46d234c18af2454a3ae8c1343c13fabd Mon Sep 17 00:00:00 2001 From: Evarin Date: Mon, 23 Apr 2018 01:13:12 +0200 Subject: [PATCH 09/47] Readme --- README.rst | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 754fdf4..84f0a4c 100644 --- a/README.rst +++ b/README.rst @@ -158,13 +158,36 @@ Configuration }, } - + Auto-signup - Poulated data + Populated data - username: ```` - email (primary and verified): ``@clipper.ens.fr`` +Long Term Clipper Adapter +========================= + +We provide an easy-to-use SocialAccountAdapter to handle the fact that Clipper Accounts are not eternal, and that there is no guarantee that the clipper usernames won't be reused later. + +This adapter also handles getting basic information about the user from SPI's LDAP. + +Configuration + Set ``SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'`` in `settings.py` + +Auto-signup + Populated data + - username: ``@`` + - email: from LDAP's `mailRoutingAddress` field, or ``@clipper.ens.fr`` + - first_name, last_name from LDAP's `cn` field + - extra_data in SociallAccount instance, containing these field, plus `anne` and `promotion` from LDAP's `homeDirectory` field (available only on first connection) + +Account deprecation + At the beginning of each year (i.e. early November), to prevent clipper username conflicts, you should run ``$ python manage.py deprecate_clippers``. Every association clipper username <-> user will then be set on hold, and at the first subsequent connection, a verification of the account will be made (using LDAP), so that a known user keeps his account, but a newcomer won't inherit an archicube's. + +Customize + You can customize the SocialAccountAdapter by inheriting ``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to modify ``get_username(clipper, data)`` to change the default username format. This function is used to disambiguate in the account deprecation process. + ********* Demo Site ********* @@ -201,7 +224,11 @@ Tests Local environment ----------------- -``$ ./runtests.py`` +Requirements + * fakeldap and mock, install with ``$ pip install mock fakeldap`` + +Run + * ``$ ./runtests.py`` All --- From f0a73f6ef686998444c734a7bc189ef8cc8926c1 Mon Sep 17 00:00:00 2001 From: Evarin Date: Tue, 24 Apr 2018 00:19:42 +0200 Subject: [PATCH 10/47] Util management command to install longtermadapter + fixes --- README.rst | 9 +- allauth_ens/adapter.py | 111 ++++++++++++++---- .../management/commands/install_longterm.py | 27 +++++ allauth_ens/tests.py | 46 +++++++- 4 files changed, 163 insertions(+), 30 deletions(-) create mode 100644 allauth_ens/management/commands/install_longterm.py diff --git a/README.rst b/README.rst index 84f0a4c..d77cd6f 100644 --- a/README.rst +++ b/README.rst @@ -178,15 +178,18 @@ Configuration Auto-signup Populated data - username: ``@`` - - email: from LDAP's `mailRoutingAddress` field, or ``@clipper.ens.fr`` - - first_name, last_name from LDAP's `cn` field - - extra_data in SociallAccount instance, containing these field, plus `anne` and `promotion` from LDAP's `homeDirectory` field (available only on first connection) + - email: from LDAP's *mailRoutingAddress* field, or ``@clipper.ens.fr`` + - first_name, last_name from LDAP's *cn* field + - extra_data in SociallAccount instance, containing these field, plus *annee* and *promotion* parsed from LDAP's *homeDirectory* field (available only on first connection) Account deprecation At the beginning of each year (i.e. early November), to prevent clipper username conflicts, you should run ``$ python manage.py deprecate_clippers``. Every association clipper username <-> user will then be set on hold, and at the first subsequent connection, a verification of the account will be made (using LDAP), so that a known user keeps his account, but a newcomer won't inherit an archicube's. Customize You can customize the SocialAccountAdapter by inheriting ``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to modify ``get_username(clipper, data)`` to change the default username format. This function is used to disambiguate in the account deprecation process. + +Initial migration + If you used allauth without LongTermClipperAccountAdapter, or another CAS interface to log in, you need to update the Users to the new username policy, and (in the second case) to create the SocialAccount instances to link CAS and Users. This can be done easily with ``$ python manage.py install_longterm``. ********* Demo Site diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index c08382f..b96f2de 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -3,9 +3,12 @@ import ldap from allauth.account.utils import user_email, user_field, user_username from allauth.account.models import EmailAddress -from allauth.socialaccount.adapter import DefaultSocialAccountAdapter, get_account_adapter +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter, get_account_adapter, get_adapter from allauth.socialaccount.models import SocialAccount from django.conf import settings +from django.contrib.auth import get_user_model + +User = get_user_model() import six @@ -36,6 +39,30 @@ def _init_ldap(): l.set_option(ldap.OPT_TIMEOUT, 10) return l +def _extract_infos_from_ldap(infos, data={}): + # Name + data['name'] = infos.get('cn', [''])[0].decode("utf-8") + + # Parsing homeDirectory to get entrance year and departments + annee = '00' + promotion = 'Inconnue' + + if 'homeDirectory' in infos: + dirs = infos['homeDirectory'][0].split('/') + if dirs[1] == 'users': + annee = dirs[2] + dep = dirs[3] + dep = dict(DEPARTMENTS_LIST).get(dep.lower(), '') + promotion = u'%s %s' % (dep, annee) + data['annee'] = annee + data['promotion'] = promotion + + # Mail + pmail = infos.get('mailRoutingAddress', []) + if len(pmail) > 0 : + data['email'] = pmail[0] + return data + def get_ldap_infos(clipper): assert clipper.isalnum() data = {'email':'{}@clipper.ens.fr'.format(clipper.strip().lower())} @@ -50,30 +77,8 @@ def get_ldap_infos(clipper): str("homeDirectory") ]) if len(info) > 0: - infos = info[0][1] - - # Name - data['name'] = infos.get('cn', [''])[0].decode("utf-8") - - # Parsing homeDirectory to get entrance year and departments - annee = '00' - promotion = 'Inconnue' - - if 'homeDirectory' in infos: - dirs = infos['homeDirectory'][0].split('/') - if dirs[1] == 'users': - annee = dirs[2] - dep = dirs[3] - dep = dict(DEPARTMENTS_LIST).get(dep.lower(), '') - promotion = u'%s %s' % (dep, annee) - data['annee'] = annee - data['promotion'] = promotion - - # Mail - pmail = infos.get('mailRoutingAddress', []) - if len(pmail) > 0 : - data['email'] = pmail[0] - + data = _extract_infos_from_ldap(info[0][1], data) + except ldap.LDAPError: pass @@ -87,6 +92,9 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): """ def pre_social_login(self, request, sociallogin): + if sociallogin.account.provider != "clipper": + return Super(LongTermClipperAccountAdapter, self).pre_social_login(request, sociallogin) + clipper = sociallogin.account.uid try: a = SocialAccount.objects.get(provider='clipper_inactive', @@ -137,6 +145,8 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): return "{}@{}".format(clipper, data.get('annee', '00')) def save_user(self, request, sociallogin, form=None): + if sociallogin.account.provider != "clipper": + return Super(LongTermClipperAccountAdapter, self).save_user(request, sociallogin, form) user = sociallogin.user user.set_unusable_password() @@ -164,6 +174,11 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): return user def deprecate_clippers(): + """ + Marks all the SocialAccount with clipper as deprecated, by setting their + provider to 'clipper_inactive' + """ + clippers = SocialAccount.objects.filter(provider='clipper') c_uids = clippers.values_list('uid', flat=True) @@ -172,3 +187,49 @@ def deprecate_clippers(): # Deprecate accounts clippers.update(provider='clipper_inactive') + +def install_longterm_adapter(fake=False): + """ + Manages the transition from an older django_cas or an allauth_ens installation + without LongTermClipperAccountAdapter + """ + + accounts = {u.username: u for u in User.objects.all() if u.username.isalnum()} + l = _init_ldap() + ltc_adapter = get_adapter() + + info = l.search_s('dc=spi,dc=ens,dc=fr', + ldap.SCOPE_SUBTREE, + ("(|{})".format(''.join(("(uid=%s)" % (un,)) + for un in accounts.keys()))), + [str("uid"), + str("cn"), + str("mailRoutingAddress"), + str("homeDirectory") ]) + + logs = {"created": [], "updated": []} + cases = [] + + for userinfo in info: + infos = userinfo[1] + data = _extract_infos_from_ldap(infos) + clipper = infos["uid"][0] + user = accounts.get(clipper, None) + if user is None: + continue + user.username = ltc_adapter.get_username(clipper, data) + if fake: + cases.append(clipper) + else: + user.save() + cases.append(user.username) + if SocialAccount.objects.filter(provider='clipper', uid=clipper).exists(): + logs["updated"].append((clipper, user.username)) + continue + sa = SocialAccount(user=user, provider='clipper', uid=clipper, extra_data=data) + if not fake: + sa.save() + logs["created"].append((clipper, user.username)) + + logs["unmodified"] = User.objects.exclude(username__in=cases).values_list("username", flat=True) + return logs diff --git a/allauth_ens/management/commands/install_longterm.py b/allauth_ens/management/commands/install_longterm.py new file mode 100644 index 0000000..f733010 --- /dev/null +++ b/allauth_ens/management/commands/install_longterm.py @@ -0,0 +1,27 @@ +#coding: utf-8 +from django.core.management.base import BaseCommand, CommandError + +from allauth_ens.adapter import install_longterm_adapter + +class Command(BaseCommand): + help = 'Manages the transition from an older django_cas or an allauth_ens installation without LongTermClipperAccountAdapter' + + def add_arguments(self, parser): + parser.add_argument( + '--fake', + action='store_true', + default=False, + help='Does not save the models created/updated, only shows the list', + ) + pass + + def handle(self, *args, **options): + logs = install_longterm_adapter(options.get("fake", False)) + self.stdout.write("Social accounts created : %d" % len(logs["created"])) + self.stdout.write(" ".join(("%s -> %s" % s) for s in logs["created"])) + self.stdout.write("Social accounts displaced : %d" % len(logs["updated"])) + self.stdout.write(" ".join(("%s -> %s" % s) for s in logs["updated"])) + self.stdout.write("User accounts unmodified : %d" % len(logs["unmodified"])) + self.stdout.write(" ".join(logs["unmodified"])) + + self.stdout.write(self.style.SUCCESS(u'LongTermClipper migration successful')) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index cafac82..ae5ba68 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -10,7 +10,7 @@ from mock import patch from fakeldap import MockLDAP from allauth_cas.test.testcases import CASTestCase, CASViewTestCase -from .adapter import deprecate_clippers +from .adapter import deprecate_clippers, install_longterm_adapter from allauth.socialaccount.models import SocialAccount _mock_ldap = MockLDAP() @@ -250,10 +250,11 @@ class LongTermClipperTests(CASTestCase): def test_multiple_deprecation(self): self._setup_ldap(12) - self._setup_ldap(15, "truc") r = self.client_cas_login(self.client, provider_id="clipper", username="test") self.client.logout() + + self._setup_ldap(15, "truc") r = self.client_cas_login(self.client, provider_id="clipper", username="truc") self.client.logout() @@ -275,3 +276,44 @@ class LongTermClipperTests(CASTestCase): # while "truc" remains self.assertEqual(sa0, sa2) self.assertEqual(sa1, sa0+1) + + def test_longterm_installer_from_allauth(self): + self._setup_ldap(12) + with self.settings(SOCIALACCOUNT_ADAPTER=\ + 'allauth.socialaccount.adapter.DefaultSocialAccountAdapter'): + r = self.client_cas_login(self.client, provider_id="clipper", + username='test') + user0 = r.context["user"] + nsa0 = SocialAccount.objects.count() + self.assertEqual(user0.username, "test") + self.client.logout() + + l = install_longterm_adapter() + + self.assertEqual(l["updated"], [("test", "test@12")]) + r = self.client_cas_login(self.client, provider_id="clipper", + username='test') + user1 = r.context["user"] + nsa1 = SocialAccount.objects.count() + self.assertEqual(user1.id, user0.id) + self.assertEqual(nsa1, nsa0) + self.assertEqual(user1.username, "test@12") + + def test_longterm_installer_from_djangocas(self): + with self.settings(SOCIALACCOUNT_ADAPTER=\ + 'allauth.socialaccount.adapter.DefaultSocialAccountAdapter'): + user0 = User.objects.create_user('test', 'test@clipper.ens.fr', 'test') + nsa0 = SocialAccount.objects.count() + + self._setup_ldap(12) + + l = install_longterm_adapter() + + self.assertEqual(l["created"], [("test", "test@12")]) + r = self.client_cas_login(self.client, provider_id="clipper", + username='test') + user1 = r.context["user"] + nsa1 = SocialAccount.objects.count() + self.assertEqual(user1.id, user0.id) + self.assertEqual(nsa1, nsa0+1) + self.assertEqual(user1.username, "test@12") From a1671a3dd76cb9e9626826c7b2a0f955627134ef Mon Sep 17 00:00:00 2001 From: Evarin Date: Sat, 28 Apr 2018 16:26:52 +0200 Subject: [PATCH 11/47] Fixes from Elarnon's review --- README.rst | 2 +- allauth_ens/adapter.py | 116 +++++++++++++++++++++-------------------- 2 files changed, 60 insertions(+), 58 deletions(-) diff --git a/README.rst b/README.rst index d77cd6f..2a35879 100644 --- a/README.rst +++ b/README.rst @@ -183,7 +183,7 @@ Auto-signup - extra_data in SociallAccount instance, containing these field, plus *annee* and *promotion* parsed from LDAP's *homeDirectory* field (available only on first connection) Account deprecation - At the beginning of each year (i.e. early November), to prevent clipper username conflicts, you should run ``$ python manage.py deprecate_clippers``. Every association clipper username <-> user will then be set on hold, and at the first subsequent connection, a verification of the account will be made (using LDAP), so that a known user keeps his account, but a newcomer won't inherit an archicube's. + At the beginning of each year (i.e. early November), to prevent clipper username conflicts, you should run ``$ python manage.py deprecate_clippers``. Every association clipper username <-> user will then be put on hold, and at the first subsequent connection, a verification of the account will be made (using LDAP), so that a known user keeps his account, but a newcomer won't inherit an archicube's. Customize You can customize the SocialAccountAdapter by inheriting ``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to modify ``get_username(clipper, data)`` to change the default username format. This function is used to disambiguate in the account deprecation process. diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index b96f2de..e0cc484 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -12,18 +12,18 @@ User = get_user_model() import six -DEPARTMENTS_LIST = ( - ('phy', u'Physique'), - ('maths', u'Maths'), - ('bio', u'Biologie'), - ('chimie', u'Chimie'), - ('geol', u'Géosciences'), - ('dec', u'DEC'), - ('info', u'Informatique'), - ('litt', u'Littéraire'), - ('guests', u'Pensionnaires étrangers'), - ('pei', u'PEI'), -) +DEPARTMENTS_LIST = { + 'phy': u'Physique', + 'maths': u'Maths', + 'bio': u'Biologie', + 'chimie': u'Chimie', + 'geol': u'Géosciences', + 'dec': u'DEC', + 'info': u'Informatique', + 'litt': u'Littéraire', + 'guests': u'Pensionnaires étrangers', + 'pei': u'PEI', +} def _init_ldap(): server = getattr(settings, "LDAP_SERVER", "ldaps://ldap.spi.ens.fr:636") @@ -41,7 +41,7 @@ def _init_ldap(): def _extract_infos_from_ldap(infos, data={}): # Name - data['name'] = infos.get('cn', [''])[0].decode("utf-8") + data['name'] = infos.get('cn', [b''])[0].decode("utf-8") # Parsing homeDirectory to get entrance year and departments annee = '00' @@ -49,17 +49,18 @@ def _extract_infos_from_ldap(infos, data={}): if 'homeDirectory' in infos: dirs = infos['homeDirectory'][0].split('/') - if dirs[1] == 'users': + if len(dirs) >= 4 and dirs[1] == 'users': + # Assume template "/users///clipper/" annee = dirs[2] dep = dirs[3] - dep = dict(DEPARTMENTS_LIST).get(dep.lower(), '') + dep = DEPARTMENTS_LIST.get(dep.lower(), '') promotion = u'%s %s' % (dep, annee) data['annee'] = annee data['promotion'] = promotion # Mail pmail = infos.get('mailRoutingAddress', []) - if len(pmail) > 0 : + if pmail: data['email'] = pmail[0] return data @@ -72,9 +73,9 @@ def get_ldap_infos(clipper): info = l.search_s('dc=spi,dc=ens,dc=fr', ldap.SCOPE_SUBTREE, ('(uid=%s)' % (clipper,)), - [str("cn"), - str("mailRoutingAddress"), - str("homeDirectory") ]) + ['cn', + 'mailRoutingAddress', + 'homeDirectory' ]) if len(info) > 0: data = _extract_infos_from_ldap(info[0][1], data) @@ -93,49 +94,50 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): def pre_social_login(self, request, sociallogin): if sociallogin.account.provider != "clipper": - return Super(LongTermClipperAccountAdapter, self).pre_social_login(request, sociallogin) + return super(LongTermClipperAccountAdapter, self).pre_social_login(request, sociallogin) clipper = sociallogin.account.uid try: a = SocialAccount.objects.get(provider='clipper_inactive', uid=clipper) - # An account with that uid was registered, but potentially - # deprecated at the beginning of the year - # We need to check that the user is still the same as before - ldap_data = get_ldap_infos(clipper) - self._ldap_data = ldap_data - - if a.user.username != self.get_username(clipper, ldap_data): - # The admission year is different - # We need a new SocialAccount - # But before that, we need to invalidate the email address of - # the previous user - email = ldap_data.get('email') - u_mails = EmailAddress.objects.filter(user=a.user) - try: - clipper_mail = u_mails.get(email=email) - if clipper_mail.primary: - n_mails = u_mails.filter(primary=False) - if n_mails.exists(): - n_mails[0].set_as_primary() - else: - user_email(a.user, '') - a.user.save() - clipper_mail.delete() - except EmailAddress.DoesNotExist: - pass - return - - # The admission year is the same, we can update the model and keep - # the previous SocialAccount instance - a.provider = 'clipper' - a.save() - - # Redo the thing that had failed just before - sociallogin.lookup() - except SocialAccount.DoesNotExist: return + + # An account with that uid was registered, but potentially + # deprecated at the beginning of the year + # We need to check that the user is still the same as before + ldap_data = get_ldap_infos(clipper) + sociallogin._ldap_data = ldap_data + + if a.user.username != self.get_username(clipper, ldap_data): + # The admission year is different + # We need a new SocialAccount + # But before that, we need to invalidate the email address of + # the previous user + email = ldap_data.get('email') + u_mails = EmailAddress.objects.filter(user=a.user) + try: + clipper_mail = u_mails.get(email=email) + if clipper_mail.primary: + n_mails = u_mails.filter(primary=False) + if n_mails.exists(): + n_mails[0].set_as_primary() + else: + user_email(a.user, '') + a.user.save() + clipper_mail.delete() + except EmailAddress.DoesNotExist: + pass + return + + # The admission year is the same, we can update the model and keep + # the previous SocialAccount instance + a.provider = 'clipper' + a.save() + + # Redo the thing that had failed just before + sociallogin.lookup() + def get_username(self, clipper, data): """ @@ -151,7 +153,7 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): user.set_unusable_password() clipper = sociallogin.account.uid - ldap_data = self._ldap_data if hasattr(self, '_ldap_data') \ + ldap_data = sociallogin._ldap_data if hasattr(sociallogin, '_ldap_data') \ else get_ldap_infos(clipper) username = self.get_username(clipper, ldap_data) @@ -182,7 +184,7 @@ def deprecate_clippers(): clippers = SocialAccount.objects.filter(provider='clipper') c_uids = clippers.values_list('uid', flat=True) - # Clear old clipper accounts that wer replaced by new ones (o avoid conflicts) + # Clear old clipper accounts that were replaced by new ones (to avoid conflicts) SocialAccount.objects.filter(provider='clipper_inactive', uid__in=c_uids).delete() # Deprecate accounts From bfc0bb42ad48e127d3e8d54d9c57f9171855b884 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sat, 28 Apr 2018 16:33:08 +0200 Subject: [PATCH 12/47] Add ldap query counting to tests --- allauth_ens/tests.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index ae5ba68..5272614 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -162,7 +162,11 @@ class LongTermClipperTests(CASTestCase): 'mailRoutingAddress' : ['test@clipper.ens.fr'], 'homeDirectory': ["/users/%d/phy/test/" % promo], } - + + def _count_ldap_queries(self): + queries = _mock_ldap.ldap_methods_called() + count = len([l for l in queries if l != 'set_option']) + return count def test_new_connexion(self): self._setup_ldap() @@ -174,6 +178,7 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(u.first_name, "John") self.assertEqual(u.last_name, "Smith") self.assertEqual(u.email, "test@clipper.ens.fr") + self.assertEqual(self._count_ldap_queries(), 1) sa = list(SocialAccount.objects.all())[-1] self.assertEqual(sa.user.id, u.id) @@ -197,6 +202,7 @@ class LongTermClipperTests(CASTestCase): self.client_cas_login(self.client, provider_id="clipper", username="test") self.assertEqual(User.objects.count(), nu) + self.assertEqual(self._count_ldap_queries(), 1) def test_deprecation(self): self._setup_ldap() @@ -225,6 +231,7 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(len(sa1), n_sa0) self.assertEqual(n_u1, n_u0) self.assertEqual(user1.id, user0.id) + self.assertEqual(self._count_ldap_queries(), 2) def test_override_inactive_account(self): self._setup_ldap(12) From 787efe96d04c0362a0fc676f233cfd66bc1c1b7d Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 29 Apr 2018 01:28:09 +0200 Subject: [PATCH 13/47] LDAP error propagation + README + tox update and corrections --- README.rst | 1 + allauth_ens/adapter.py | 113 ++++++++++------- .../management/commands/deprecate_clippers.py | 8 +- .../management/commands/install_longterm.py | 28 +++-- allauth_ens/providers/clipper/provider.py | 5 +- allauth_ens/tests.py | 115 +++++++++++------- example/adapter.py | 8 +- tox.ini | 3 +- 8 files changed, 171 insertions(+), 110 deletions(-) diff --git a/README.rst b/README.rst index 2a35879..5b30afa 100644 --- a/README.rst +++ b/README.rst @@ -187,6 +187,7 @@ Account deprecation Customize You can customize the SocialAccountAdapter by inheriting ``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to modify ``get_username(clipper, data)`` to change the default username format. This function is used to disambiguate in the account deprecation process. + By default, ``get_username`` raises a ``ValueError`` when the connexion to the LDAP failed or did not allow to retrieve the user's entrance year. Overriding ``get_username`` (as done in the example website) allows to get rid of that behaviour, and for instance attribute a default entrance year. Initial migration If you used allauth without LongTermClipperAccountAdapter, or another CAS interface to log in, you need to update the Users to the new username policy, and (in the second case) to create the SocialAccount instances to link CAS and Users. This can be done easily with ``$ python manage.py install_longterm``. diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index e0cc484..dadbd2a 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -1,16 +1,18 @@ # -*- coding: utf-8 -*- -import ldap -from allauth.account.utils import user_email, user_field, user_username -from allauth.account.models import EmailAddress -from allauth.socialaccount.adapter import DefaultSocialAccountAdapter, get_account_adapter, get_adapter -from allauth.socialaccount.models import SocialAccount from django.conf import settings from django.contrib.auth import get_user_model -User = get_user_model() +from allauth.account.models import EmailAddress +from allauth.account.utils import user_email, user_field, user_username +from allauth.socialaccount.adapter import ( + DefaultSocialAccountAdapter, get_account_adapter, get_adapter, +) +from allauth.socialaccount.models import SocialAccount -import six +import ldap + +User = get_user_model() DEPARTMENTS_LIST = { 'phy': u'Physique', @@ -25,6 +27,7 @@ DEPARTMENTS_LIST = { 'pei': u'PEI', } + def _init_ldap(): server = getattr(settings, "LDAP_SERVER", "ldaps://ldap.spi.ens.fr:636") ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, @@ -39,6 +42,7 @@ def _init_ldap(): l.set_option(ldap.OPT_TIMEOUT, 10) return l + def _extract_infos_from_ldap(infos, data={}): # Name data['name'] = infos.get('cn', [b''])[0].decode("utf-8") @@ -48,7 +52,7 @@ def _extract_infos_from_ldap(infos, data={}): promotion = 'Inconnue' if 'homeDirectory' in infos: - dirs = infos['homeDirectory'][0].split('/') + dirs = infos['homeDirectory'][0].decode("utf-8").split('/') if len(dirs) >= 4 and dirs[1] == 'users': # Assume template "/users///clipper/" annee = dirs[2] @@ -61,12 +65,13 @@ def _extract_infos_from_ldap(infos, data={}): # Mail pmail = infos.get('mailRoutingAddress', []) if pmail: - data['email'] = pmail[0] + data['email'] = pmail[0].decode("utf-8") return data + def get_ldap_infos(clipper): assert clipper.isalnum() - data = {'email':'{}@clipper.ens.fr'.format(clipper.strip().lower())} + data = {} try: l = _init_ldap() @@ -75,11 +80,11 @@ def get_ldap_infos(clipper): ('(uid=%s)' % (clipper,)), ['cn', 'mailRoutingAddress', - 'homeDirectory' ]) + 'homeDirectory']) if len(info) > 0: data = _extract_infos_from_ldap(info[0][1], data) - + except ldap.LDAPError: pass @@ -88,21 +93,22 @@ def get_ldap_infos(clipper): class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): """ - A class to manage the fact that people loose their account at the end of + A class to manage the fact that people loose their account at the end of their scolarity and that their clipper login might be reused later """ def pre_social_login(self, request, sociallogin): if sociallogin.account.provider != "clipper": - return super(LongTermClipperAccountAdapter, self).pre_social_login(request, sociallogin) - + return super(LongTermClipperAccountAdapter, + self).pre_social_login(request, sociallogin) + clipper = sociallogin.account.uid try: a = SocialAccount.objects.get(provider='clipper_inactive', uid=clipper) except SocialAccount.DoesNotExist: return - + # An account with that uid was registered, but potentially # deprecated at the beginning of the year # We need to check that the user is still the same as before @@ -114,7 +120,8 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): # We need a new SocialAccount # But before that, we need to invalidate the email address of # the previous user - email = ldap_data.get('email') + email = ldap_data.get('email', '{}@clipper.ens.fr'.format( + clipper.strip().lower())) u_mails = EmailAddress.objects.filter(user=a.user) try: clipper_mail = u_mails.get(email=email) @@ -137,34 +144,38 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): # Redo the thing that had failed just before sociallogin.lookup() - def get_username(self, clipper, data): """ Util function to generate a unique username, by default 'clipper@promo' This is used to disambiguate and recognize if the person is the same """ + if data is None or 'annee' not in data: + raise ValueError("No entrance year in LDAP data") return "{}@{}".format(clipper, data.get('annee', '00')) - + def save_user(self, request, sociallogin, form=None): if sociallogin.account.provider != "clipper": - return Super(LongTermClipperAccountAdapter, self).save_user(request, sociallogin, form) + return super(LongTermClipperAccountAdapter, + self).save_user(request, sociallogin, form) user = sociallogin.user user.set_unusable_password() - + clipper = sociallogin.account.uid - ldap_data = sociallogin._ldap_data if hasattr(sociallogin, '_ldap_data') \ - else get_ldap_infos(clipper) - + ldap_data = sociallogin._ldap_data if hasattr(sociallogin, + '_ldap_data') \ + else get_ldap_infos(clipper) + username = self.get_username(clipper, ldap_data) - email = ldap_data.get('email') + email = ldap_data.get('email', '{}@clipper.ens.fr'.format( + clipper.strip().lower())) name = ldap_data.get('name') user_username(user, username or '') user_email(user, email or '') name_parts = (name or '').split(' ') user_field(user, 'first_name', name_parts[0]) user_field(user, 'last_name', ' '.join(name_parts[1:])) - + # Ignore form get_account_adapter().populate_username(request, user) @@ -172,46 +183,51 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): sociallogin.account.extra_data = sociallogin.extra_data = ldap_data sociallogin.save(request) sociallogin.account.save() - + return user + def deprecate_clippers(): """ - Marks all the SocialAccount with clipper as deprecated, by setting their + Marks all the SocialAccount with clipper as deprecated, by setting their provider to 'clipper_inactive' """ - + clippers = SocialAccount.objects.filter(provider='clipper') c_uids = clippers.values_list('uid', flat=True) - - # Clear old clipper accounts that were replaced by new ones (to avoid conflicts) - SocialAccount.objects.filter(provider='clipper_inactive', uid__in=c_uids).delete() - + + # Clear old clipper accounts that were replaced by new ones + # (to avoid conflicts) + SocialAccount.objects.filter(provider='clipper_inactive', + uid__in=c_uids).delete() + # Deprecate accounts clippers.update(provider='clipper_inactive') + def install_longterm_adapter(fake=False): """ - Manages the transition from an older django_cas or an allauth_ens installation - without LongTermClipperAccountAdapter + Manages the transition from an older django_cas or an allauth_ens + installation without LongTermClipperAccountAdapter """ - - accounts = {u.username: u for u in User.objects.all() if u.username.isalnum()} + + accounts = {u.username: u for u in User.objects.all() + if u.username.isalnum()} l = _init_ldap() ltc_adapter = get_adapter() - + info = l.search_s('dc=spi,dc=ens,dc=fr', ldap.SCOPE_SUBTREE, ("(|{})".format(''.join(("(uid=%s)" % (un,)) for un in accounts.keys()))), - [str("uid"), - str("cn"), - str("mailRoutingAddress"), - str("homeDirectory") ]) + ['uid', + 'cn', + 'mailRoutingAddress', + 'homeDirectory']) logs = {"created": [], "updated": []} cases = [] - + for userinfo in info: infos = userinfo[1] data = _extract_infos_from_ldap(infos) @@ -225,13 +241,16 @@ def install_longterm_adapter(fake=False): else: user.save() cases.append(user.username) - if SocialAccount.objects.filter(provider='clipper', uid=clipper).exists(): + if SocialAccount.objects.filter(provider='clipper', + uid=clipper).exists(): logs["updated"].append((clipper, user.username)) continue - sa = SocialAccount(user=user, provider='clipper', uid=clipper, extra_data=data) + sa = SocialAccount(user=user, provider='clipper', + uid=clipper, extra_data=data) if not fake: sa.save() logs["created"].append((clipper, user.username)) - - logs["unmodified"] = User.objects.exclude(username__in=cases).values_list("username", flat=True) + + logs["unmodified"] = User.objects.exclude(username__in=cases)\ + .values_list("username", flat=True) return logs diff --git a/allauth_ens/management/commands/deprecate_clippers.py b/allauth_ens/management/commands/deprecate_clippers.py index a824fd8..257d707 100644 --- a/allauth_ens/management/commands/deprecate_clippers.py +++ b/allauth_ens/management/commands/deprecate_clippers.py @@ -1,8 +1,9 @@ -#coding: utf-8 -from django.core.management.base import BaseCommand, CommandError +# coding: utf-8 +from django.core.management.base import BaseCommand from allauth_ens.adapter import deprecate_clippers + class Command(BaseCommand): help = 'Deprecates clipper SocialAccounts so as to avoid conflicts' @@ -11,4 +12,5 @@ class Command(BaseCommand): def handle(self, *args, **options): deprecate_clippers() - self.stdout.write(self.style.SUCCESS(u'Clippers deprecation successful')) + self.stdout.write(self.style.SUCCESS( + 'Clippers deprecation successful')) diff --git a/allauth_ens/management/commands/install_longterm.py b/allauth_ens/management/commands/install_longterm.py index f733010..40ccde3 100644 --- a/allauth_ens/management/commands/install_longterm.py +++ b/allauth_ens/management/commands/install_longterm.py @@ -1,27 +1,35 @@ -#coding: utf-8 -from django.core.management.base import BaseCommand, CommandError +# coding: utf-8 +from django.core.management.base import BaseCommand from allauth_ens.adapter import install_longterm_adapter + class Command(BaseCommand): - help = 'Manages the transition from an older django_cas or an allauth_ens installation without LongTermClipperAccountAdapter' + help = 'Manages the transition from an older django_cas' \ + 'or an allauth_ens installation without ' \ + 'LongTermClipperAccountAdapter' def add_arguments(self, parser): parser.add_argument( '--fake', action='store_true', default=False, - help='Does not save the models created/updated, only shows the list', + help=('Does not save the models created/updated,' + 'only shows the list'), ) pass def handle(self, *args, **options): logs = install_longterm_adapter(options.get("fake", False)) - self.stdout.write("Social accounts created : %d" % len(logs["created"])) - self.stdout.write(" ".join(("%s -> %s" % s) for s in logs["created"])) - self.stdout.write("Social accounts displaced : %d" % len(logs["updated"])) - self.stdout.write(" ".join(("%s -> %s" % s) for s in logs["updated"])) - self.stdout.write("User accounts unmodified : %d" % len(logs["unmodified"])) + self.stdout.write("Social accounts created : %d" + % len(logs["created"])) + self.stdout.write(" ".join(("%s -> %s" % s) for s in logs["created"])) + self.stdout.write("Social accounts displaced : %d" + % len(logs["updated"])) + self.stdout.write(" ".join(("%s -> %s" % s) for s in logs["updated"])) + self.stdout.write("User accounts unmodified : %d" + % len(logs["unmodified"])) self.stdout.write(" ".join(logs["unmodified"])) - self.stdout.write(self.style.SUCCESS(u'LongTermClipper migration successful')) + self.stdout.write(self.style.SUCCESS( + "LongTermClipper migration successful")) diff --git a/allauth_ens/providers/clipper/provider.py b/allauth_ens/providers/clipper/provider.py index fa057b2..41bb56b 100644 --- a/allauth_ens/providers/clipper/provider.py +++ b/allauth_ens/providers/clipper/provider.py @@ -1,11 +1,8 @@ # -*- coding: utf-8 -*- -import ldap - from allauth.account.models import EmailAddress from allauth.socialaccount.providers.base import ProviderAccount -from allauth_cas.providers import CASProvider -from django.conf import settings +from allauth_cas.providers import CASProvider class ClipperAccount(ProviderAccount): diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index 5272614..eb276c6 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -6,15 +6,17 @@ from django.contrib.sites.models import Site from django.core import mail from django.test import TestCase, override_settings -from mock import patch -from fakeldap import MockLDAP - -from allauth_cas.test.testcases import CASTestCase, CASViewTestCase -from .adapter import deprecate_clippers, install_longterm_adapter from allauth.socialaccount.models import SocialAccount +from allauth_cas.test.testcases import CASTestCase +from fakeldap import MockLDAP +from mock import patch + +from .adapter import deprecate_clippers, install_longterm_adapter + _mock_ldap = MockLDAP() -ldap_patcher = patch('allauth_ens.adapter.ldap.initialize', lambda x: _mock_ldap) +ldap_patcher = patch('allauth_ens.adapter.ldap.initialize', + lambda x: _mock_ldap) if django.VERSION >= (1, 10): from django.urls import reverse @@ -37,6 +39,7 @@ def prevent_logout_pwd_change(client, user): session[HASH_SESSION_KEY] = user.get_session_auth_hash() session.save() + class ViewsTests(TestCase): """ Checks (barely) that templates do not contain errors. @@ -145,70 +148,77 @@ class ViewsTests(TestCase): r = self.client.get(reverse('account_reset_password_from_key_done')) self.assertEqual(r.status_code, 200) -@override_settings(SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter') -class LongTermClipperTests(CASTestCase): +@override_settings( + SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter' +) +class LongTermClipperTests(CASTestCase): def setUp(self): ldap_patcher.start() def tearDown(self): + ldap_patcher.stop() _mock_ldap.reset() def _setup_ldap(self, promo=12, username="test"): - import ldap - _mock_ldap.directory['dc=spi,dc=ens,dc=fr']={ + _mock_ldap.directory['dc=spi,dc=ens,dc=fr'] = { 'uid': [username], - 'cn': ['John Smith'], - 'mailRoutingAddress' : ['test@clipper.ens.fr'], - 'homeDirectory': ["/users/%d/phy/test/" % promo], + 'cn': [b'John Smith'], + 'mailRoutingAddress': [b'test@clipper.ens.fr'], + 'homeDirectory': [b'/users/%d/phy/test/' % promo], } def _count_ldap_queries(self): queries = _mock_ldap.ldap_methods_called() count = len([l for l in queries if l != 'set_option']) return count - + def test_new_connexion(self): self._setup_ldap() - - r = self.client_cas_login(self.client, provider_id="clipper", username="test") + + r = self.client_cas_login(self.client, provider_id="clipper", + username="test") u = r.context['user'] - + self.assertEqual(u.username, "test@12") self.assertEqual(u.first_name, "John") self.assertEqual(u.last_name, "Smith") self.assertEqual(u.email, "test@clipper.ens.fr") self.assertEqual(self._count_ldap_queries(), 1) - + sa = list(SocialAccount.objects.all())[-1] self.assertEqual(sa.user.id, u.id) def test_connect_disconnect(self): self._setup_ldap() - r0 = self.client_cas_login(self.client, provider_id="clipper", username="test") + r0 = self.client_cas_login(self.client, provider_id="clipper", + username="test") self.assertIn("_auth_user_id", self.client.session) self.assertIn('user', r0.context) - r1 = self.client.logout() + self.client.logout() self.assertNotIn("_auth_user_id", self.client.session) - + def test_second_connexion(self): self._setup_ldap() - self.client_cas_login(self.client, provider_id="clipper", username="test") + self.client_cas_login(self.client, provider_id="clipper", + username="test") self.client.logout() nu = User.objects.count() - - self.client_cas_login(self.client, provider_id="clipper", username="test") + + self.client_cas_login(self.client, provider_id="clipper", + username="test") self.assertEqual(User.objects.count(), nu) self.assertEqual(self._count_ldap_queries(), 1) def test_deprecation(self): self._setup_ldap() - self.client_cas_login(self.client, provider_id="clipper", username="test") + self.client_cas_login(self.client, provider_id="clipper", + username="test") deprecate_clippers() - + sa = SocialAccount.objects.all()[0] self.assertEqual(sa.provider, "clipper_inactive") @@ -220,9 +230,9 @@ class LongTermClipperTests(CASTestCase): n_sa0 = SocialAccount.objects.count() n_u0 = User.objects.count() self.client.logout() - + deprecate_clippers() - + r = self.client_cas_login(self.client, provider_id="clipper", username="test") user1 = r.context['user'] @@ -241,18 +251,18 @@ class LongTermClipperTests(CASTestCase): n_sa0 = SocialAccount.objects.count() n_u0 = User.objects.count() self.client.logout() - + deprecate_clippers() self._setup_ldap(13) r = self.client_cas_login(self.client, provider_id="clipper", username="test") - + user1 = r.context['user'] sa1 = list(SocialAccount.objects.all()) n_u1 = User.objects.count() - self.assertEqual(len(sa1), n_sa0+1) - self.assertEqual(n_u1, n_u0+1) + self.assertEqual(len(sa1), n_sa0 + 1) + self.assertEqual(n_u1, n_u0 + 1) self.assertNotEqual(user1.id, user0.id) def test_multiple_deprecation(self): @@ -260,18 +270,18 @@ class LongTermClipperTests(CASTestCase): r = self.client_cas_login(self.client, provider_id="clipper", username="test") self.client.logout() - + self._setup_ldap(15, "truc") r = self.client_cas_login(self.client, provider_id="clipper", username="truc") self.client.logout() sa0 = SocialAccount.objects.count() - + deprecate_clippers() self._setup_ldap(13) - r = self.client_cas_login(self.client, provider_id="clipper", - username="test") + self.client_cas_login(self.client, provider_id="clipper", + username="test") self.client.logout() sa1 = SocialAccount.objects.count() @@ -282,12 +292,12 @@ class LongTermClipperTests(CASTestCase): # Older "test" inactive SocialAccount gets erased by new one # while "truc" remains self.assertEqual(sa0, sa2) - self.assertEqual(sa1, sa0+1) + self.assertEqual(sa1, sa0 + 1) def test_longterm_installer_from_allauth(self): self._setup_ldap(12) - with self.settings(SOCIALACCOUNT_ADAPTER=\ - 'allauth.socialaccount.adapter.DefaultSocialAccountAdapter'): + with self.settings(SOCIALACCOUNT_ADAPTER= + 'allauth.socialaccount.adapter.DefaultSocialAccountAdapter'): r = self.client_cas_login(self.client, provider_id="clipper", username='test') user0 = r.context["user"] @@ -307,11 +317,12 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(user1.username, "test@12") def test_longterm_installer_from_djangocas(self): - with self.settings(SOCIALACCOUNT_ADAPTER=\ - 'allauth.socialaccount.adapter.DefaultSocialAccountAdapter'): - user0 = User.objects.create_user('test', 'test@clipper.ens.fr', 'test') + with self.settings(SOCIALACCOUNT_ADAPTER= + 'allauth.socialaccount.adapter.DefaultSocialAccountAdapter'): + user0 = User.objects.create_user('test', 'test@clipper.ens.fr', + 'test') nsa0 = SocialAccount.objects.count() - + self._setup_ldap(12) l = install_longterm_adapter() @@ -322,5 +333,21 @@ class LongTermClipperTests(CASTestCase): user1 = r.context["user"] nsa1 = SocialAccount.objects.count() self.assertEqual(user1.id, user0.id) - self.assertEqual(nsa1, nsa0+1) + self.assertEqual(nsa1, nsa0 + 1) self.assertEqual(user1.username, "test@12") + + def test_disconnect_ldap(self): + nu0 = User.objects.count() + nsa0 = SocialAccount.objects.count() + + ldap_patcher.stop() + with self.settings(LDAP_SERVER=''): + self.assertRaises(ValueError, self.client_cas_login, + self.client, provider_id="clipper", + username="test") + + nu1 = User.objects.count() + nsa1 = SocialAccount.objects.count() + self.assertEqual(nu0, nu1) + self.assertEqual(nsa0, nsa1) + ldap_patcher.start() diff --git a/example/adapter.py b/example/adapter.py index f1079f5..b28cfdd 100644 --- a/example/adapter.py +++ b/example/adapter.py @@ -8,4 +8,10 @@ class AccountAdapter(DefaultAccountAdapter): class SocialAccountAdapter(LongTermClipperAccountAdapter): - pass + + def get_username(self, clipper, data): + """ + Exception-free version of get_username, so that it works even outside of + the ENS (if no access to LDAP server) + """ + return "{}@{}".format(clipper, data.get('annee', '00')) diff --git a/tox.ini b/tox.ini index ec77d8d..513c573 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,8 @@ deps = django111: django>=1.11,<2.0 django20: django>=2.0,<2.1 coverage - mock ; python_version < "3.0" + fakeldap + mock usedevelop= True commands = python -V From 17fef409a8aed1ffd8e479779b022281d665943d Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 3 Jun 2018 22:10:34 +0200 Subject: [PATCH 14/47] More readable and organized code Working from Aurelien's code reviews --- README.rst | 42 ++++++++--- allauth_ens/adapter.py | 168 +++++++++++------------------------------ allauth_ens/tests.py | 4 +- allauth_ens/utils.py | 121 +++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 135 deletions(-) create mode 100644 allauth_ens/utils.py diff --git a/README.rst b/README.rst index 5b30afa..d69d2b7 100644 --- a/README.rst +++ b/README.rst @@ -148,14 +148,11 @@ Configuration SOCIALACCOUNT_PROVIDERS = { # … - 'clipper': { - # These settings control whether a message containing a link to # disconnect from the CAS server is added when users log out. 'MESSAGE_SUGGEST_LOGOUT_ON_LOGOUT': True, 'MESSAGE_SUGGEST_LOGOUT_ON_LOGOUT_LEVEL': messages.INFO, - }, } @@ -164,33 +161,56 @@ Auto-signup - username: ```` - email (primary and verified): ``@clipper.ens.fr`` +******** +Adapters +******** Long Term Clipper Adapter ========================= -We provide an easy-to-use SocialAccountAdapter to handle the fact that Clipper Accounts are not eternal, and that there is no guarantee that the clipper usernames won't be reused later. +We provide an easy-to-use SocialAccountAdapter to handle the fact that Clipper +accounts are not eternal, and that there is no guarantee that the clipper +usernames won't be reused later. -This adapter also handles getting basic information about the user from SPI's LDAP. +This adapter also handles getting basic information about the user from SPI's +LDAP. Configuration - Set ``SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'`` in `settings.py` + Set ``SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'`` + in `settings.py` Auto-signup Populated data - username: ``@`` - email: from LDAP's *mailRoutingAddress* field, or ``@clipper.ens.fr`` - first_name, last_name from LDAP's *cn* field - - extra_data in SociallAccount instance, containing these field, plus *annee* and *promotion* parsed from LDAP's *homeDirectory* field (available only on first connection) + - extra_data in SocialAccount instance, containing these field, plus *annee* + and *promotion* parsed from LDAP's *homeDirectory* field (available only on + first connection) Account deprecation - At the beginning of each year (i.e. early November), to prevent clipper username conflicts, you should run ``$ python manage.py deprecate_clippers``. Every association clipper username <-> user will then be put on hold, and at the first subsequent connection, a verification of the account will be made (using LDAP), so that a known user keeps his account, but a newcomer won't inherit an archicube's. + At the beginning of each year (i.e. early November), to prevent clipper + username conflicts, you should run ``$ python manage.py deprecate_clippers``. + Every association clipper username <-> user will then be put on hold, and at + the first subsequent connection, a verification of the account will be made + (using LDAP), so that a known user keeps his account, but a newcomer won't + inherit an archicube's. Customize - You can customize the SocialAccountAdapter by inheriting ``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to modify ``get_username(clipper, data)`` to change the default username format. This function is used to disambiguate in the account deprecation process. - By default, ``get_username`` raises a ``ValueError`` when the connexion to the LDAP failed or did not allow to retrieve the user's entrance year. Overriding ``get_username`` (as done in the example website) allows to get rid of that behaviour, and for instance attribute a default entrance year. + You can customize the SocialAccountAdapter by inheriting + ``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to + modify ``get_username(clipper, data)`` to change the default username format. + This function is used to disambiguate in the account deprecation process. + By default, ``get_username`` raises a ``ValueError`` when the connexion to the + LDAP failed or did not allow to retrieve the user's entrance year. Overriding + ``get_username`` (as done in the example website) allows to get rid of that + behaviour, and for instance attribute a default entrance year. Initial migration - If you used allauth without LongTermClipperAccountAdapter, or another CAS interface to log in, you need to update the Users to the new username policy, and (in the second case) to create the SocialAccount instances to link CAS and Users. This can be done easily with ``$ python manage.py install_longterm``. + If you used allauth without LongTermClipperAccountAdapter, or another CAS + interface to log in, you need to update the Users to the new username policy, + and (in the second case) to create the SocialAccount instances to link CAS and + Users. This can be done easily with ``$ python manage.py install_longterm``. ********* Demo Site diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index dadbd2a..50c5d33 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -from django.conf import settings from django.contrib.auth import get_user_model -from allauth.account.models import EmailAddress from allauth.account.utils import user_email, user_field, user_username from allauth.socialaccount.adapter import ( DefaultSocialAccountAdapter, get_account_adapter, get_adapter, @@ -12,85 +10,10 @@ from allauth.socialaccount.models import SocialAccount import ldap +from .utils import extract_infos_from_ldap, get_ldap_infos, get_clipper_email, remove_email, init_ldap + User = get_user_model() -DEPARTMENTS_LIST = { - 'phy': u'Physique', - 'maths': u'Maths', - 'bio': u'Biologie', - 'chimie': u'Chimie', - 'geol': u'Géosciences', - 'dec': u'DEC', - 'info': u'Informatique', - 'litt': u'Littéraire', - 'guests': u'Pensionnaires étrangers', - 'pei': u'PEI', -} - - -def _init_ldap(): - server = getattr(settings, "LDAP_SERVER", "ldaps://ldap.spi.ens.fr:636") - ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, - ldap.OPT_X_TLS_NEVER) - l = ldap.initialize(server) - l.set_option(ldap.OPT_REFERRALS, 0) - l.set_option(ldap.OPT_PROTOCOL_VERSION, 3) - l.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) - l.set_option(ldap.OPT_X_TLS_DEMAND, True) - l.set_option(ldap.OPT_DEBUG_LEVEL, 255) - l.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) - l.set_option(ldap.OPT_TIMEOUT, 10) - return l - - -def _extract_infos_from_ldap(infos, data={}): - # Name - data['name'] = infos.get('cn', [b''])[0].decode("utf-8") - - # Parsing homeDirectory to get entrance year and departments - annee = '00' - promotion = 'Inconnue' - - if 'homeDirectory' in infos: - dirs = infos['homeDirectory'][0].decode("utf-8").split('/') - if len(dirs) >= 4 and dirs[1] == 'users': - # Assume template "/users///clipper/" - annee = dirs[2] - dep = dirs[3] - dep = DEPARTMENTS_LIST.get(dep.lower(), '') - promotion = u'%s %s' % (dep, annee) - data['annee'] = annee - data['promotion'] = promotion - - # Mail - pmail = infos.get('mailRoutingAddress', []) - if pmail: - data['email'] = pmail[0].decode("utf-8") - return data - - -def get_ldap_infos(clipper): - assert clipper.isalnum() - data = {} - try: - l = _init_ldap() - - info = l.search_s('dc=spi,dc=ens,dc=fr', - ldap.SCOPE_SUBTREE, - ('(uid=%s)' % (clipper,)), - ['cn', - 'mailRoutingAddress', - 'homeDirectory']) - - if len(info) > 0: - data = _extract_infos_from_ldap(info[0][1], data) - - except ldap.LDAPError: - pass - - return data - - class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): """ A class to manage the fact that people loose their account at the end of @@ -98,61 +21,63 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): """ def pre_social_login(self, request, sociallogin): + """ + If a clipper connection has already existed with the uid, it checks + that this connection still belongs to the user it was associated with. + + This check is performed by comparing the generated username corresponding + to this connection with the old one. + + If the check succeeds, it simply reactivates the clipper connection as + belonging to the associated user. + + If the check fails, it frees the elements (as the clipper email + address) which will be assigned to the new connection later. + """ + if sociallogin.account.provider != "clipper": return super(LongTermClipperAccountAdapter, self).pre_social_login(request, sociallogin) - clipper = sociallogin.account.uid + clipper_uid = sociallogin.account.uid try: - a = SocialAccount.objects.get(provider='clipper_inactive', - uid=clipper) + old_conn = SocialAccount.objects.get(provider='clipper_inactive', + uid=clipper_uid) except SocialAccount.DoesNotExist: return # An account with that uid was registered, but potentially # deprecated at the beginning of the year # We need to check that the user is still the same as before - ldap_data = get_ldap_infos(clipper) + ldap_data = get_ldap_infos(clipper_uid) sociallogin._ldap_data = ldap_data - if a.user.username != self.get_username(clipper, ldap_data): + if old_conn.user.username != self.get_username(clipper_uid, ldap_data): # The admission year is different - # We need a new SocialAccount - # But before that, we need to invalidate the email address of - # the previous user - email = ldap_data.get('email', '{}@clipper.ens.fr'.format( - clipper.strip().lower())) - u_mails = EmailAddress.objects.filter(user=a.user) - try: - clipper_mail = u_mails.get(email=email) - if clipper_mail.primary: - n_mails = u_mails.filter(primary=False) - if n_mails.exists(): - n_mails[0].set_as_primary() - else: - user_email(a.user, '') - a.user.save() - clipper_mail.delete() - except EmailAddress.DoesNotExist: - pass + # We cannot reuse this SocialAccount, so we need to invalidate + # the email address of the previous user to prevent conflicts + # if a new SocialAccount is created + email = ldap_data.get('email', get_clipper_email(clipper_uid)) + remove_email(old_conn.user, email) + return # The admission year is the same, we can update the model and keep # the previous SocialAccount instance - a.provider = 'clipper' - a.save() + old_conn.provider = 'clipper' + old_conn.save() # Redo the thing that had failed just before sociallogin.lookup() - def get_username(self, clipper, data): + def get_username(self, clipper_uid, data): """ Util function to generate a unique username, by default 'clipper@promo' This is used to disambiguate and recognize if the person is the same """ - if data is None or 'annee' not in data: + if data is None or 'entrance_year' not in data: raise ValueError("No entrance year in LDAP data") - return "{}@{}".format(clipper, data.get('annee', '00')) + return "{}@{}".format(clipper_uid, data['entrance_year']) def save_user(self, request, sociallogin, form=None): if sociallogin.account.provider != "clipper": @@ -161,14 +86,13 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): user = sociallogin.user user.set_unusable_password() - clipper = sociallogin.account.uid + clipper_uid = sociallogin.account.uid ldap_data = sociallogin._ldap_data if hasattr(sociallogin, '_ldap_data') \ - else get_ldap_infos(clipper) + else get_ldap_infos(clipper_uid) - username = self.get_username(clipper, ldap_data) - email = ldap_data.get('email', '{}@clipper.ens.fr'.format( - clipper.strip().lower())) + username = self.get_username(clipper_uid, ldap_data) + email = ldap_data.get('email', get_clipper_email(clipper_uid)) name = ldap_data.get('name') user_username(user, username or '') user_email(user, email or '') @@ -213,7 +137,7 @@ def install_longterm_adapter(fake=False): accounts = {u.username: u for u in User.objects.all() if u.username.isalnum()} - l = _init_ldap() + l = init_ldap() ltc_adapter = get_adapter() info = l.search_s('dc=spi,dc=ens,dc=fr', @@ -230,26 +154,26 @@ def install_longterm_adapter(fake=False): for userinfo in info: infos = userinfo[1] - data = _extract_infos_from_ldap(infos) - clipper = infos["uid"][0] - user = accounts.get(clipper, None) + data = extract_infos_from_ldap(infos) + clipper_uid = data['clipper_uid'] + user = accounts.get(clipper_uid, None) if user is None: continue - user.username = ltc_adapter.get_username(clipper, data) + user.username = ltc_adapter.get_username(clipper_uid, data) if fake: - cases.append(clipper) + cases.append(clipper_uid) else: user.save() cases.append(user.username) if SocialAccount.objects.filter(provider='clipper', - uid=clipper).exists(): - logs["updated"].append((clipper, user.username)) + uid=clipper_uid).exists(): + logs["updated"].append((clipper_uid, user.username)) continue sa = SocialAccount(user=user, provider='clipper', - uid=clipper, extra_data=data) + uid=clipper_uid, extra_data=data) if not fake: sa.save() - logs["created"].append((clipper, user.username)) + logs["created"].append((clipper_uid, user.username)) logs["unmodified"] = User.objects.exclude(username__in=cases)\ .values_list("username", flat=True) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index eb276c6..d9c01e5 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -15,7 +15,7 @@ from mock import patch from .adapter import deprecate_clippers, install_longterm_adapter _mock_ldap = MockLDAP() -ldap_patcher = patch('allauth_ens.adapter.ldap.initialize', +ldap_patcher = patch('allauth_ens.utils.ldap.initialize', lambda x: _mock_ldap) if django.VERSION >= (1, 10): @@ -341,7 +341,7 @@ class LongTermClipperTests(CASTestCase): nsa0 = SocialAccount.objects.count() ldap_patcher.stop() - with self.settings(LDAP_SERVER=''): + with self.settings(CLIPPER_LDAP_SERVER=''): self.assertRaises(ValueError, self.client_cas_login, self.client, provider_id="clipper", username="test") diff --git a/allauth_ens/utils.py b/allauth_ens/utils.py new file mode 100644 index 0000000..57df7b1 --- /dev/null +++ b/allauth_ens/utils.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- + +from django.conf import settings + +from allauth.account.models import EmailAddress +from allauth.account.utils import user_email + +import ldap + +DEPARTMENTS_LIST = { + 'phy': u'Physique', + 'maths': u'Maths', + 'bio': u'Biologie', + 'chimie': u'Chimie', + 'geol': u'Géosciences', + 'dec': u'DEC', + 'info': u'Informatique', + 'litt': u'Littéraire', + 'guests': u'Pensionnaires étrangers', + 'pei': u'PEI', +} + +def init_ldap(): + server = getattr(settings, "CLIPPER_LDAP_SERVER", "ldaps://ldap.spi.ens.fr:636") + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, + ldap.OPT_X_TLS_NEVER) + l = ldap.initialize(server) + l.set_option(ldap.OPT_REFERRALS, 0) + l.set_option(ldap.OPT_PROTOCOL_VERSION, 3) + l.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) + l.set_option(ldap.OPT_X_TLS_DEMAND, True) + l.set_option(ldap.OPT_DEBUG_LEVEL, 255) + l.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) + l.set_option(ldap.OPT_TIMEOUT, 10) + return l + +def extract_infos_from_ldap(infos): + data = {} + + # Name + if 'cn' in infos: + data['name'] = infos['cn'][0].decode("utf-8") + + # Parsing homeDirectory to get entrance year and departments + if 'homeDirectory' in infos: + dirs = infos['homeDirectory'][0].decode("utf-8").split('/') + if len(dirs) >= 4 and dirs[1] == 'users': + # Assume template "/users///clipper/" + annee = dirs[2] + dep = dirs[3] + dep_fancy = DEPARTMENTS_LIST.get(dep.lower(), '') + promotion = u'%s %s' % (dep, annee) + data['entrance_year'] = annee + data['department_code'] = dep + data['department'] = dep_fancy + + # Mail + pmail = infos.get('mailRoutingAddress', []) + if pmail: + data['email'] = pmail[0].decode("utf-8") + + # User id + if 'uid' in infos: + data['clipper_uid'] = infos['uid'][0].decode("utf-8").strip().lower() + + return data + +def get_ldap_infos(clipper_uid): + assert clipper_uid.isalnum() + data = {} + try: + l = init_ldap() + + info = l.search_s('dc=spi,dc=ens,dc=fr', + ldap.SCOPE_SUBTREE, + ('(uid=%s)' % (clipper_uid,)), + ['cn', + 'mailRoutingAddress', + 'homeDirectory']) + + if len(info) > 0: + data = extract_infos_from_ldap(info[0][1]) + + except ldap.LDAPError: + pass + + return data + +def get_clipper_email(clipper): + return '{}@clipper.ens.fr'.format(clipper.strip().lower()) + +def remove_email(user, email): + """ + Removes an email address of a user. + + If it is his primary email address, it sets another email address as + primary, preferably verified. + """ + u_mailaddrs = user.emailaddress_set.filter(user=user) + try: + mailaddr = user.emailaddress_set.get(email=email) + except EmailAddress.DoesNotExist: + return + + if mailaddr.primary: + others = u_mailaddrs.filter(primary=False) + + # Prefer a verified mail. + new_primary = ( + others.filter(verified=True).last() or + others.last() + ) + + if new_primary: + # It also updates 'user.EMAIL_FIELD'. + new_primary.set_as_primary() + else: + user_email(user, '') + user.save() + + mailaddr.delete() From 5ee1c774ac5975780f446d26f019da31aab84abb Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 3 Jun 2018 22:15:16 +0200 Subject: [PATCH 15/47] Better README display? --- README.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index d69d2b7..11c4195 100644 --- a/README.rst +++ b/README.rst @@ -179,15 +179,17 @@ Configuration Set ``SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'`` in `settings.py` + Auto-signup Populated data - username: ``@`` - email: from LDAP's *mailRoutingAddress* field, or ``@clipper.ens.fr`` - first_name, last_name from LDAP's *cn* field - extra_data in SocialAccount instance, containing these field, plus *annee* - and *promotion* parsed from LDAP's *homeDirectory* field (available only on - first connection) - + and *promotion* parsed from LDAP's *homeDirectory* field (available only on + first connection) + + Account deprecation At the beginning of each year (i.e. early November), to prevent clipper username conflicts, you should run ``$ python manage.py deprecate_clippers``. @@ -196,6 +198,7 @@ Account deprecation (using LDAP), so that a known user keeps his account, but a newcomer won't inherit an archicube's. + Customize You can customize the SocialAccountAdapter by inheriting ``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to @@ -206,6 +209,7 @@ Customize ``get_username`` (as done in the example website) allows to get rid of that behaviour, and for instance attribute a default entrance year. + Initial migration If you used allauth without LongTermClipperAccountAdapter, or another CAS interface to log in, you need to update the Users to the new username policy, From 4cf633ed8167dd46d43d661693a2e31bfebca4bb Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 3 Jun 2018 22:17:14 +0200 Subject: [PATCH 16/47] Actually no, too bad --- README.rst | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 11c4195..4a06279 100644 --- a/README.rst +++ b/README.rst @@ -179,16 +179,14 @@ Configuration Set ``SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'`` in `settings.py` - Auto-signup Populated data - username: ``@`` - email: from LDAP's *mailRoutingAddress* field, or ``@clipper.ens.fr`` - first_name, last_name from LDAP's *cn* field - extra_data in SocialAccount instance, containing these field, plus *annee* - and *promotion* parsed from LDAP's *homeDirectory* field (available only on - first connection) - +and *promotion* parsed from LDAP's *homeDirectory* field (available only on +first connection) Account deprecation At the beginning of each year (i.e. early November), to prevent clipper @@ -198,7 +196,6 @@ Account deprecation (using LDAP), so that a known user keeps his account, but a newcomer won't inherit an archicube's. - Customize You can customize the SocialAccountAdapter by inheriting ``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to @@ -209,7 +206,6 @@ Customize ``get_username`` (as done in the example website) allows to get rid of that behaviour, and for instance attribute a default entrance year. - Initial migration If you used allauth without LongTermClipperAccountAdapter, or another CAS interface to log in, you need to update the Users to the new username policy, From 08a47150db9fd84c30f0002619213b55db354a26 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 3 Jun 2018 22:26:53 +0200 Subject: [PATCH 17/47] Populate user model with promotion infos As requested by Erkan et Martin --- README.rst | 11 +++++------ allauth_ens/adapter.py | 10 ++++++++++ allauth_ens/utils.py | 1 - 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 4a06279..28914db 100644 --- a/README.rst +++ b/README.rst @@ -181,12 +181,11 @@ Configuration Auto-signup Populated data - - username: ``@`` - - email: from LDAP's *mailRoutingAddress* field, or ``@clipper.ens.fr`` - - first_name, last_name from LDAP's *cn* field - - extra_data in SocialAccount instance, containing these field, plus *annee* -and *promotion* parsed from LDAP's *homeDirectory* field (available only on -first connection) + - *username*: ``@`` + - *email*: from LDAP's *mailRoutingAddress* field, or ``@clipper.ens.fr`` + - *first_name*, *last_name* from LDAP's *cn* field + - *entrance_year* (as 2-digit string), *department_code*, *department* and *promotion* (department+year) parsed from LDAP's *homeDirectory* field + - *extra_data* in SocialAccount instance, containing all these field except *promotion* (and available only on first connection) Account deprecation At the beginning of each year (i.e. early November), to prevent clipper diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index 50c5d33..e85d978 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -100,6 +100,16 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): user_field(user, 'first_name', name_parts[0]) user_field(user, 'last_name', ' '.join(name_parts[1:])) + # Entrance year and department, if the user has these fields + entrance_year = ldap_data.get('entrance_year', '') + dep_code = ldap_data.get('department_code', '') + dep_fancy = ldap_data.get('department', '') + promotion = u'%s %s' % (dep_fancy, entrance_year) + user_field(user, 'entrance_year', entrance_year) + user_field(user, 'department_code', dep_code) + user_field(user, 'department', dep_fancy) + user_field(user, 'promotion', promotion) + # Ignore form get_account_adapter().populate_username(request, user) diff --git a/allauth_ens/utils.py b/allauth_ens/utils.py index 57df7b1..230d55d 100644 --- a/allauth_ens/utils.py +++ b/allauth_ens/utils.py @@ -49,7 +49,6 @@ def extract_infos_from_ldap(infos): annee = dirs[2] dep = dirs[3] dep_fancy = DEPARTMENTS_LIST.get(dep.lower(), '') - promotion = u'%s %s' % (dep, annee) data['entrance_year'] = annee data['department_code'] = dep data['department'] = dep_fancy From 35a3bc3e2dd9112952459d8d238d2c08c0014712 Mon Sep 17 00:00:00 2001 From: Evarin Date: Tue, 19 Jun 2018 19:15:25 +0200 Subject: [PATCH 18/47] Small fix to example site --- example/adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/adapter.py b/example/adapter.py index b28cfdd..5ad5e10 100644 --- a/example/adapter.py +++ b/example/adapter.py @@ -14,4 +14,4 @@ class SocialAccountAdapter(LongTermClipperAccountAdapter): Exception-free version of get_username, so that it works even outside of the ENS (if no access to LDAP server) """ - return "{}@{}".format(clipper, data.get('annee', '00')) + return "{}@{}".format(clipper, data.get('entrance_year', '00')) From cca8da57725bb9f17daa1e9c94f9e41cbb3ea4c7 Mon Sep 17 00:00:00 2001 From: Evarin Date: Fri, 22 Jun 2018 22:05:42 +0200 Subject: [PATCH 19/47] Show prominently the Clipper third-party --- README.rst | 7 +++ allauth_ens/scss/_highlight_clipper.scss | 35 +++++++++++++ allauth_ens/scss/screen.scss | 2 + allauth_ens/static/allauth_ens/screen.css | 50 +++++++++++++++---- allauth_ens/templates/account/login.html | 14 +++++- allauth_ens/templates/allauth_ens/base.html | 2 +- .../socialaccount/snippets/provider_list.html | 6 +-- allauth_ens/templatetags/allauth_ens.py | 6 +++ 8 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 allauth_ens/scss/_highlight_clipper.scss diff --git a/README.rst b/README.rst index 754fdf4..f7d266d 100644 --- a/README.rst +++ b/README.rst @@ -77,6 +77,13 @@ See also the `allauth configuration`_ and `advanced usage`_ docs pages. **Examples:** ``'my-account'``, ``'/my-account/'`` +``ALLAUTH_ENS_HIGHLIGHT_CLIPPER`` + *Optional* — Boolean (default: `True`). + + When set to `True`, displays prominently the Clipper option in the login view + (if you use the `allauth_ens` templates). + + ***** Views ***** diff --git a/allauth_ens/scss/_highlight_clipper.scss b/allauth_ens/scss/_highlight_clipper.scss new file mode 100644 index 0000000..eeac915 --- /dev/null +++ b/allauth_ens/scss/_highlight_clipper.scss @@ -0,0 +1,35 @@ +.content-wrapper.highlight-clipper { + .main-login-choices { + li:not(:first-child) { + margin-top: 5px; + } + + a { + display: block; + text-align: center; + background: $gray-lighter; + padding: 35px 20px; + color: $black; + font-size: 1.1em; + + @include hover-focus { + background: lighten($brand-primary, 50%); + text-decoration: none; + } + } + } + &:not(.not-clipper) { + width: 100vw; + max-width: 500px; + + & > :not(.main-login-choices) { + display: none; + } + } + + &.not-clipper { + .main-login-choices { + display: none; + } + } +} diff --git a/allauth_ens/scss/screen.scss b/allauth_ens/scss/screen.scss index 59610da..800e15c 100644 --- a/allauth_ens/scss/screen.scss +++ b/allauth_ens/scss/screen.scss @@ -5,3 +5,5 @@ @import "mixins"; @import "base"; + +@import "highlight_clipper"; diff --git a/allauth_ens/static/allauth_ens/screen.css b/allauth_ens/static/allauth_ens/screen.css index 648dd92..b6c06b1 100644 --- a/allauth_ens/static/allauth_ens/screen.css +++ b/allauth_ens/static/allauth_ens/screen.css @@ -1,4 +1,4 @@ -/* line 5, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 5, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, @@ -20,45 +20,45 @@ time, mark, audio, video { vertical-align: baseline; } -/* line 22, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 22, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html { line-height: 1; } -/* line 24, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 24, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ ol, ul { list-style: none; } -/* line 26, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 26, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ table { border-collapse: collapse; border-spacing: 0; } -/* line 28, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 28, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ caption, th, td { text-align: left; font-weight: normal; vertical-align: middle; } -/* line 30, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 30, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q, blockquote { quotes: none; } -/* line 103, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 103, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q:before, q:after, blockquote:before, blockquote:after { content: ""; content: none; } -/* line 32, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 32, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ a img { border: none; } -/* line 116, ../../../vendor/bundle/ruby/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 116, ../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block; } @@ -775,3 +775,35 @@ section > * + * { background: #2d672d; color: #fff; } + +/* line 3, ../../scss/_highlight_clipper.scss */ +.content-wrapper.highlight-clipper .main-login-choices li:not(:first-child) { + margin-top: 5px; +} +/* line 7, ../../scss/_highlight_clipper.scss */ +.content-wrapper.highlight-clipper .main-login-choices a { + display: block; + text-align: center; + background: #eceeef; + padding: 35px 20px; + color: #000; + font-size: 1.1em; +} +/* line 10, ../../scss/_mixins.scss */ +.content-wrapper.highlight-clipper .main-login-choices a:focus, .content-wrapper.highlight-clipper .main-login-choices a:hover { + background: #a8d6fe; + text-decoration: none; +} +/* line 21, ../../scss/_highlight_clipper.scss */ +.content-wrapper.highlight-clipper:not(.not-clipper) { + width: 100vw; + max-width: 500px; +} +/* line 25, ../../scss/_highlight_clipper.scss */ +.content-wrapper.highlight-clipper:not(.not-clipper) > :not(.main-login-choices) { + display: none; +} +/* line 31, ../../scss/_highlight_clipper.scss */ +.content-wrapper.highlight-clipper.not-clipper .main-login-choices { + display: none; +} diff --git a/allauth_ens/templates/account/login.html b/allauth_ens/templates/account/login.html index 99c4ab8..87196a5 100644 --- a/allauth_ens/templates/account/login.html +++ b/allauth_ens/templates/account/login.html @@ -30,10 +30,23 @@ {% endif %} {% endblock %} +{% block content-extra-classes %}{% is_clipper_highlighted as highlight_clipper %}{% if highlight_clipper %}highlight-clipper{% endif %}{% endblock %} + {% block content %} {% get_providers as socialaccount_providers %} +{% is_clipper_highlighted as highlight_clipper %} + +{% if highlight_clipper %} + +{% endif %} + {% if socialaccount_providers %}
    @@ -68,7 +81,6 @@ {% endif %}
- {% endblock %} {% block extra_js %} diff --git a/allauth_ens/templates/allauth_ens/base.html b/allauth_ens/templates/allauth_ens/base.html index 7606848..01e6a65 100644 --- a/allauth_ens/templates/allauth_ens/base.html +++ b/allauth_ens/templates/allauth_ens/base.html @@ -79,7 +79,7 @@ {% block messages-extra %}{% endblock %} -
+
{% block content %}{% endblock %}
diff --git a/allauth_ens/templates/socialaccount/snippets/provider_list.html b/allauth_ens/templates/socialaccount/snippets/provider_list.html index 649b7fc..ab0fe71 100644 --- a/allauth_ens/templates/socialaccount/snippets/provider_list.html +++ b/allauth_ens/templates/socialaccount/snippets/provider_list.html @@ -7,7 +7,7 @@ {% for provider in socialaccount_providers %} {% if provider.id == "openid" %} {% for brand in provider.get_brands %} -
  • +
  • @@ -16,9 +16,9 @@
  • {% endfor %} {% endif %} -
  • +
  • {{ provider.name }} diff --git a/allauth_ens/templatetags/allauth_ens.py b/allauth_ens/templatetags/allauth_ens.py index c369fe1..e00c6b6 100644 --- a/allauth_ens/templatetags/allauth_ens.py +++ b/allauth_ens/templatetags/allauth_ens.py @@ -33,3 +33,9 @@ def get_profile_url(): def is_open_for_signup(context): request = context['request'] return get_adapter(request).is_open_for_signup(request) + + +@simple_tag +def is_clipper_highlighted(): + return ('allauth_ens.providers.clipper' in getattr(settings, 'INSTALLED_APPS', [])) \ + and getattr(settings, 'ALLAUTH_ENS_HIGHLIGHT_CLIPPER', True) From 7a0ec189e21ac5e9c1143ca71023b79483be9712 Mon Sep 17 00:00:00 2001 From: Evarin Date: Fri, 22 Jun 2018 22:37:16 +0200 Subject: [PATCH 20/47] =?UTF-8?q?Traductions=20en=20Fran=C3=A7ais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- allauth_ens/locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 6572 bytes allauth_ens/locale/fr/LC_MESSAGES/django.po | 413 ++++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 allauth_ens/locale/fr/LC_MESSAGES/django.mo create mode 100644 allauth_ens/locale/fr/LC_MESSAGES/django.po diff --git a/allauth_ens/locale/fr/LC_MESSAGES/django.mo b/allauth_ens/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..3a2bb04b636abd72949f637e7a204528ebc7cfe1 GIT binary patch literal 6572 zcmca7#4?qEfq}t*fq_AWfq`KTBLjmoh|LTVWnj=?VPJ@4U|`T>VPIIoz`)SV!oaYa zfq_ATm4P9Sfq~%)D+5C?0|SFG8v{cq0|P@l8v}z50|UcdHU@?|1_lN$b_Rwd1_p*s zb_ND71_p*V>Ttjhlf%fPsO*kDGyki-Cb5o|}O|f`Ne{i<^OgALKr6 z1_pKp28PMp3=Con3=Ffm85l$u7#KD|^&RJCU=U(pV7LM0zlQ4j!_C0J%fP_E!^6P9 z!@$5G#RK81^Dr>TGB7Zh@-Q&)F)%O$^FYi?;$dK5V_;w?hlV?4QNMz`()4 zz_19)Ukz2a8LICP48A@Okv%D*cPG53o&B>dSVAmJe*!N8!%z`$T6 z0Z9iT5)gZ8Bp4XvK;a+(Ne4@z;`<~Z@pwW4;@+zg3=Gl?3=B`8dys&4bJXu|Zh}lx9HT z#RLj#kRSuZ{j3ZSe}mGhE(63(pzwk!0p~%Ge?VysCZ-8vKxt68LYWMz43PYw4;27~ zH8&{VF+kEMC@q7eKyd>~-yk+9e}M9gHi*N(0166lT!PXeC@feQAZbJxDz5>exfI~Q zv9u&3HLoNyIk6-&KTjboF*7GMMNc6hCpED+RUtVeH91?MGQYG)A-O0u1*9-Br&u8| zFGZoGs8S&@Jux#+56LtJBz7c7S7K4BLL$P7)D(rpVg*%=(&E&j_~Mcx&0-yeq|y=v zkg~k|60p+zqRgt)6ory}g~a6K)Z$_!BTF(eixmnI(^K^n!t+aWQa~s>Qq=(4mJ$!PE55WSN3&STE-$ql%@sEKiFPp4 zU~Y}fFI7lR%u^`NOwUt*coQ5eiOI=e|CJVJ=A|nn=2a@>rzwE};u)gYEx#x|zoaBJ52U3C z9?13zu6ZS?MG(PM-Q2{?9EHS`l%mw)VuhsCocwYfaQZG!RnN&$C{E2w0f$v(eyKuY zo&rogEO0@>CHc_QlUai7R%k*<&d*EBEXq|V$w(|wut~C0)kpf;11uY478jSMDkLg^vIEp3C{{r86C!UwoE@GD_GNK?QBftNC;=sY zaB3(m0J$R>o~n~`^5NMOt{I%vi$Ns?C???%j3@x}^GXtvOB70r6*AL6fv1p?uK+5k zic*tPGt0om0VrHE^Ar*l(o)M6ax?QvOHxr{36yt11zciMeknMyf{GDv?p4(&D9SI( zOi3+@&r8fr)hvdkc~FuDXAVg4I~IdX&&*5AQ7A4+EznUYfELgBxdl0?C8=mRJ}ti} z7ddW04nTwh)B|9jg9;32XeUCFjRL3`EG^Je0BI`D0GF~wsfjr`mEc%ROi4k`1~5JO z;Gh8&P_S^x%P+}HgVrj=I?xOP@-)~Oa6y<~l%k`MT9OR$BP2>7l?Jp>OUX|y1_w68 zt?(SIP?=hyr{I{9qL7~lF8Yyc7zI#tF!?Lm}anHX_`C=o#EBrm^QAvq&4FCA8r zGdMzB;R($N3?K_#!Q};mBee8~I4lnqZT1Y#U?UU)pw$Y4GbH!JXa$IVklR7!4M-SN zn1bD`;GC0LP>@>0fT||7n8DRASOK-3W`LA!aC__-e89Dzb7Ed{YEDjS3Y71bn3+>r zl*-_nn4PLnP?VXQSX9a2mtUgb3@gPM{7W)YixkQeD~myi4xDo`^Yp=VX#oSwBML#F z`jH_hRToquDwL%b!DE*pC^a{~ER_MQ0`4M);M5W%4yfAl%wq@!(f*|+5JqSLj8{1^GoKy1vEfnJKzXrRl}GA^BDcTmk+ey3R$Z;Nno% zC9x#cO2Np$&_dV1OxMUr!N|zU&`{gJ$iRRrz+X2gwJZ~48di12rVw>LiFxUziRr0U z3TZ`LzMj6W2saq&8E`r0=ar=9mFR|47NlA!l%!Ua=ojQ9X69Lg5^+&+YKd)Vh?}kj zLS0c}UU6D#k*;fAa(+r?Ub>ZnMN(!7S3pi_QDTm+TYgb)v6Vtz0hn8CYh$GhJcL8Ni4}PvQprJIZ-zxH8Iypfh$7SJvA@22qGJhpPG_cqF|_J zp=YjV$OXyWkXEOUIxN4!3J8Tng~Kb8GxAFhuhdiUO)W~!R7goxC_B6ol*&?z6w308 z6*9rC&$P_Mypm#t)Dnf%;^M^0)FOq$3l#G5OUqIdOCjY5#13Tp!tzUt6%MZ|NktfP zc%?#eer|3mT2nMPF|$~q45U0y{qU-iB5?Z<)F{m_$}B#-5@c6ma`NGoDXEaU8DiJr z1q#WjB_$xmpq6P^erZl>l`hyAm?IO*Qj--*^FWpsl%#^&r-xU9T3c8eraq|(Ihm<> zpb#iJyfQN{vm`SyC$kuwv_OKn`6VCo}KMY*XZB}IuK_vWN3ASc4qBIMA5Cmd)?EwxCYurw93kqZh)P^v<6 zVgaawn4=C!jENmY4_1+{O8z4j;I^dw6AP zF(|h}f(??;4lO`VHqNlZBP9M+Ir2QJ4z*$ULy1r=DJ7{F>dBEvz_tsf|Ai$NtCI8_zr=Nw*H ztOE+6Vg*o&Ejqjs>UtD!qvV&A!z+tHp-^^sB{UU7^D@|QaJoq>%g-xTNX;wDuLPAM zuY2%zpahcx>M3JuID@kR zC>%hsT%?ebs!))hnOCBa0!=gEngNvdArY*A*_;L^F|gC&84a8j0zgSG4b-5{EKV%~ z1yMm!KB!rcYLa7R9Ngq}arJ^TNND2gd zJSDZPC=-;uixM*-1zJ{qDX8O$B`YPSW#)l`5}JccGmDEsT0xERviwX)zDj{Krs3^2 zP^%oNet`!jav=jMJ5bu^VaO$So_ZpvO|Fodmz-Y&stgp6_39}&f*o9?3o7CB6mZ2w zabg~0ix#)8`lph_$^KP59Q^YBWja%V_w0_nG=F+l7tLKJWe;6Mcz zs?fR+Q9Hoe?ckV3whYuj2ld`lix4bON`@3c(6SaJm6r;y5Ma%CM~EU=j~Pi7I3#k^ zUHyU?e32`2SYdCEq&6`xuQccI%2bBKouI)uP>BG_(4gi!$VP^s)Vvf>JEbTyFFCUy zF(;M5FCWy-01wg}UdiBCT2ho+tdM(nWl2UpC<&y1N`O3wdl`a|5&*P-C`E}xkP2OJ zh6iPnRE1nnizXc$*U;t}WZ;k?xU>M&zep_t>3}y{pqh|8$>4H$Wm;w)BvYXWHPlru zhgX8LE7*g6phiSuDYUX?aK=)UrKJ|-f`SNLDY#LFmh, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-22 22:11+0200\n" +"PO-Revision-Date: 2018-06-22 22:35+0200\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Last-Translator: \n" +"Language-Team: \n" +"X-Generator: Poedit 1.8.7.1\n" + +#: apps.py:7 +msgid "ENS Authentication" +msgstr "Connexion pour l'ENS" + +#: templates/account/account_inactive.html:5 +#: templates/account/account_inactive.html:6 +msgid "Account Inactive" +msgstr "Compte inactif" + +#: templates/account/account_inactive.html:12 +msgid "" +"\n" +" This account is inactive.\n" +" " +msgstr "" +"\n" +" Ce compte est inactif.\n" +" " + +#: templates/account/email.html:4 templates/account/email.html:5 +msgid "E-mail Addresses" +msgstr "Adresses e-mail" + +#: templates/account/email.html:16 +msgid "" +"\n" +" The following e-mail addresses are associated with your account:\n" +" " +msgstr "" +"\n" +" Les adresses mails suivantes sont associées à votre compte :\n" +" " + +#: templates/account/email.html:30 +msgid "This email address is verified." +msgstr "Cette adresse e-mail est vérifiée." + +#: templates/account/email.html:34 +msgid "This email address is not verified." +msgstr "Cette adresse e-mail n'est pas vérifiée." + +#: templates/account/email.html:40 +msgid "This is your primary email address." +msgstr "Ceci est votre adresse e-mail primaire." + +#: templates/account/email.html:54 +msgid "Remove" +msgstr "Supprimer" + +#: templates/account/email.html:65 +msgid "Re-send verification" +msgstr "Ré-envoyer le message de vérification" + +#: templates/account/email.html:77 +msgid "Make primary" +msgstr "Rendre principale" + +#: templates/account/email.html:90 +msgid "" +"\n" +" You currently do not have any e-mail address set up. You should really\n" +" add an e-mail address so you can receive notifications, reset your\n" +" password, etc.\n" +" " +msgstr "" +"\n" +" Vous n'avez actuellement aucune adresse e-mail associée à votre compte.\n" +" Vous devriez vraiment ajouter une adresse e-mail afin que vous puissiez " +"recevoir\n" +" des notifications, réinitialiser votre mot de passe, etc.\n" +" " + +#: templates/account/email.html:101 +msgid "Add E-mail" +msgstr "Ajouter un e-mail" + +#: templates/account/email_confirm.html:5 +#: templates/account/email_confirm.html:6 +msgid "Confirm E-mail Address" +msgstr "Confirmer l'adresse e-mail" + +#: templates/account/email_confirm.html:18 +#, python-format +msgid "" +"\n" +" Please confirm that %(email)s is an e-mail address for user\n" +" %(user_display)s.\n" +" " +msgstr "" +"\n" +" Merci de confirmer que %(email)s est une adresse e-mail pour " +"l'utilisateur\n" +" %(user_display)s.\n" +" " + +#: templates/account/email_confirm.html:26 +msgid "Confirm" +msgstr "Confirmer" + +#: templates/account/email_confirm.html:34 +#, python-format +msgid "" +"\n" +" This e-mail confirmation link expired or is invalid.
    \n" +" Please issue a new e-mail confirmation " +"request.\n" +" " +msgstr "" +"\n" +" Ce lien de confirmation d'e-mail a expiré ou est invalide.
    \n" +" Merci de soumettre une nouvelle requête de " +"confirmation d'e-mail.\n" +" " + +#: templates/account/login.html:5 templates/account/login.html:6 +#: templates/account/login.html:78 +msgid "Sign In" +msgstr "Connexion" + +#: templates/account/login.html:14 +msgid "" +"\n" +" Authentication failed. Please check your credentials and try " +"again.\n" +" " +msgstr "" +"\n" +" L'authentification a échoué. Merci de vérifier vos identifiants et " +"essayer à nouveau.\n" +" " + +#: templates/account/login.html:22 +#, python-format +msgid "" +"\n" +" Your are authenticated as %(user_str)s, but are not authorized to " +"access\n" +" this page. Would you like to login to a different account ?\n" +" " +msgstr "" +"\n" +" Vous êtes identifié comme %(user_str)s, mais vous n'être pas " +"autorisé à accéder\n" +" à cette page. Voulez-vous essayer avec un compte différent ?\n" +" " + +#: templates/account/login.html:44 +msgid "Connect with Clipper" +msgstr "Connexion via Clipper" + +#: templates/account/login.html:45 +msgid "Other ways to sign in/sign up" +msgstr "Autres méthodes de connexion" + +#: templates/account/login.html:65 +#, fuzzy +msgid "" +"\n" +" Please sign in with one of your existing third party accounts, or with " +"the form below.\n" +" " +msgstr "" +"\n" +" Merci de vous connecter avec un de vos comptes tiers existants, ou avec " +"le formulaire ci-dessous.\n" +" " + +#: templates/account/login.html:70 +msgid "Forgot Password?" +msgstr "Mot de passe oublié ?" + +#: templates/account/login.html:73 templates/account/signup.html:17 +#: templates/socialaccount/signup.html:19 +msgid "Sign Up" +msgstr "Nouveau compte" + +#: templates/account/logout.html:4 templates/account/logout.html:5 +#: templates/account/logout.html:20 +msgid "Sign Out" +msgstr "Déconnexion" + +#: templates/account/logout.html:11 +msgid "" +"\n" +" Are you sure you want to sign out?\n" +" " +msgstr "" +"\n" +" Êtes-vous sûr de vouloir vous déconnecter ?\n" +" " + +#: templates/account/password_change.html:4 +#: templates/account/password_change.html:5 +#: templates/account/password_change.html:18 +#: templates/account/password_reset_from_key.html:4 +#: templates/account/password_reset_from_key.html:5 +#: templates/account/password_reset_from_key_done.html:4 +#: templates/account/password_reset_from_key_done.html:5 +msgid "Change Password" +msgstr "Changer le mot de passe" + +#: templates/account/password_reset.html:4 +#: templates/account/password_reset.html:5 +#: templates/account/password_reset_done.html:4 +#: templates/account/password_reset_done.html:5 +msgid "Password Reset" +msgstr "Réinitialisation du mot de passe" + +#: templates/account/password_reset.html:11 +msgid "" +"\n" +" Forgotten your password? Enter your e-mail address below, and we'll " +"send\n" +" you an e-mail allowing you to reset it.\n" +" " +msgstr "" +"\n" +" Vous avez oublié votre mot de passe ? Entrez votre adresse e-mail ci-" +"dessous, et\n" +" nous vous enverrons un e-mail qui vous permettra de le réinitialiser.\n" +" " + +#: templates/account/password_reset.html:18 +#: templates/account/password_reset_from_key.html:21 +msgid "Reset Password" +msgstr "Réinitialiser le mot de passe" + +#: templates/account/password_reset_done.html:11 +msgid "" +"\n" +" We have sent you an e-mail. Please contact us if you do not receive it " +"within a few minutes.\n" +" " +msgstr "" +"\n" +" Nous vous avons envoyé un e-mail. Merci de nous contacter si vous ne " +"l'avez pas reçu d'ici quelques minutes.\n" +" " + +#: templates/account/password_reset_from_key.html:13 +#, python-format +msgid "" +"\n" +" The password reset link was invalid, possibly because it has already " +"been used.\n" +" Please request a new password reset.\n" +" " +msgstr "" +"\n" +" Le lien de réinitialisation de mot de passe est invalide, possiblement " +"parce qu'il a déjà été utilisé.\n" +" Merci de faire une nouvelle demande.\n" +" " + +#: templates/account/password_reset_from_key.html:24 +msgid "Your password is now changed." +msgstr "Votre mot de passe a été modifié." + +#: templates/account/password_reset_from_key_done.html:11 +msgid "" +"\n" +" Your password is now changed.\n" +" " +msgstr "" +"\n" +" Votre mot de passe a été modifié.\n" +" " + +#: templates/account/password_set.html:4 templates/account/password_set.html:5 +#: templates/account/password_set.html:24 +msgid "Set Password" +msgstr "Définir le mot de passe" + +#: templates/account/password_set.html:17 +msgid "" +"\n" +" Your account does not have a password yet. Add one to authenticate " +"without\n" +" third parties.\n" +" " +msgstr "" +"\n" +" Votre compte n'a pas encore de mot de passe. Ajoutez-en un pour vous " +"connecter\n" +" sans compte tiers.\n" +" " + +#: templates/account/signup.html:4 templates/account/signup.html:5 +#: templates/socialaccount/signup.html:4 templates/socialaccount/signup.html:5 +msgid "Signup" +msgstr "Nouveau compte" + +#: templates/account/signup.html:12 +msgid "Already have an account?" +msgstr "Vous avez déjà un compte ?" + +#: templates/account/signup_closed.html:4 +#: templates/account/signup_closed.html:5 +msgid "Sign Up Closed" +msgstr "Création de compte fermée" + +#: templates/account/signup_closed.html:11 +msgid "" +"\n" +" We are sorry, but the sign up is currently closed.\n" +" " +msgstr "" +"\n" +" Nous sommes désolés, mais la création de compte est actuellement " +"désactivée.\n" +" " + +#: templates/allauth_ens/base.html:71 +msgid "Not Connected" +msgstr "Non connecté" + +#: templates/socialaccount/authentication_error.html:4 +#: templates/socialaccount/authentication_error.html:5 +msgid "Login Failure" +msgstr "Échec de la connexion" + +#: templates/socialaccount/authentication_error.html:11 +msgid "" +"\n" +" An error occured while attempting to login via your social network " +"account.\n" +" " +msgstr "" +"\n" +" Une erreur s'est produite lors de la connexion via le site externe.\n" +" " + +#: templates/socialaccount/connections.html:5 +#: templates/socialaccount/connections.html:6 +#, fuzzy +msgid "Account Connections" +msgstr "Méthodes de connexion" + +#: templates/socialaccount/connections.html:13 +msgid "" +"\n" +" You can sign in to your account using any of the following third party " +"accounts:\n" +" " +msgstr "" +"\n" +" Vous pouvez vous connecter à votre compte en utilisant n'importe lequel " +"de ces comptes tiers :\n" +" " + +#: templates/socialaccount/connections.html:17 +msgid "" +"\n" +" You currently have no third party accounts connected to this account.\n" +" " +msgstr "" +"\n" +" Vous n'avez actuellement aucun compte tiers associé à ce compte.\n" +" " + +#: templates/socialaccount/login_cancelled.html:4 +#: templates/socialaccount/login_cancelled.html:5 +msgid "Login Cancelled" +msgstr "Connexion annulée" + +#: templates/socialaccount/login_cancelled.html:13 +#, fuzzy, python-format +msgid "" +"\n" +" You decided to cancel logging into our site using one of your existing " +"accounts. If this was a mistake, please proceed to sign in." +msgstr "" +"\n" +" Vous avez décidé d'annuler la connexion à notre site via un de vos " +"comptes existants. Si cela était une erreur, merci de retourner à la page de connexion." + +#: templates/socialaccount/signup.html:11 +#, python-format +msgid "" +"\n" +" You are about to use your %(provider_name)s account to login.\n" +" As a final step, please complete the following form:\n" +" " +msgstr "" +"\n" +" Vous êtes sur le point d'utiliser votre compte %(provider_name)s pour " +"vous connecter\n" +" Pour finaliser la procédure, merci de remplir le formulaire suivant :\n" +" " From 534e0b188f8025d491142a5f069859f89e803c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 16 Jun 2018 17:02:32 +0200 Subject: [PATCH 21/47] LongTermClipper preserving LDAP data --- README.rst | 5 ++- allauth_ens/adapter.py | 45 ++++++++++++++--------- allauth_ens/providers/clipper/provider.py | 15 ++++++++ allauth_ens/providers/clipper/tests.py | 12 ++++++ allauth_ens/tests.py | 14 ++++++- setup.cfg | 1 - 6 files changed, 72 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index 28914db..4e953c3 100644 --- a/README.rst +++ b/README.rst @@ -175,6 +175,10 @@ usernames won't be reused later. This adapter also handles getting basic information about the user from SPI's LDAP. +**Important:** If you are building on top of *allauth*, take care to preserve +the ``extra_data['ldap']`` of ``SocialAccount`` instances related to *Clipper* +(where ``provider_id`` is ``clipper`` or ``clipper_inactive``). + Configuration Set ``SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'`` in `settings.py` @@ -199,7 +203,6 @@ Customize You can customize the SocialAccountAdapter by inheriting ``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to modify ``get_username(clipper, data)`` to change the default username format. - This function is used to disambiguate in the account deprecation process. By default, ``get_username`` raises a ``ValueError`` when the connexion to the LDAP failed or did not allow to retrieve the user's entrance year. Overriding ``get_username`` (as done in the example website) allows to get rid of that diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index e85d978..b9c5d48 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -25,8 +25,8 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): If a clipper connection has already existed with the uid, it checks that this connection still belongs to the user it was associated with. - This check is performed by comparing the generated username corresponding - to this connection with the old one. + This check is performed by comparing the entrance years provided by the + LDAP. If the check succeeds, it simply reactivates the clipper connection as belonging to the associated user. @@ -52,14 +52,19 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): ldap_data = get_ldap_infos(clipper_uid) sociallogin._ldap_data = ldap_data - if old_conn.user.username != self.get_username(clipper_uid, ldap_data): - # The admission year is different + if ldap_data is None or 'entrance_year' not in ldap_data: + raise ValueError("No entrance year in LDAP data") + + old_conn_entrance_year = ( + old_conn.extra_data.get('ldap', {}).get('entrance_year')) + + if old_conn_entrance_year != ldap_data['entrance_year']: # We cannot reuse this SocialAccount, so we need to invalidate # the email address of the previous user to prevent conflicts # if a new SocialAccount is created email = ldap_data.get('email', get_clipper_email(clipper_uid)) remove_email(old_conn.user, email) - + return # The admission year is the same, we can update the model and keep @@ -72,8 +77,7 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): def get_username(self, clipper_uid, data): """ - Util function to generate a unique username, by default 'clipper@promo' - This is used to disambiguate and recognize if the person is the same + Util function to generate a unique username, by default 'clipper@promo'. """ if data is None or 'entrance_year' not in data: raise ValueError("No entrance year in LDAP data") @@ -114,7 +118,7 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): get_account_adapter().populate_username(request, user) # Save extra data (only once) - sociallogin.account.extra_data = sociallogin.extra_data = ldap_data + sociallogin.account.extra_data['ldap'] = ldap_data sociallogin.save(request) sociallogin.account.save() @@ -175,15 +179,22 @@ def install_longterm_adapter(fake=False): else: user.save() cases.append(user.username) - if SocialAccount.objects.filter(provider='clipper', - uid=clipper_uid).exists(): - logs["updated"].append((clipper_uid, user.username)) - continue - sa = SocialAccount(user=user, provider='clipper', - uid=clipper_uid, extra_data=data) - if not fake: - sa.save() - logs["created"].append((clipper_uid, user.username)) + + try: + sa = SocialAccount.objects.get(provider='clipper', uid=clipper_uid) + if not sa.extra_data.get('ldap'): + sa.extra_data['ldap'] = data + if not fake: + sa.save(update_fields=['extra_data']) + logs["updated"].append((clipper_uid, user.username)) + except SocialAccount.DoesNotExist: + sa = SocialAccount( + provider='clipper', uid=clipper_uid, + user=user, extra_data={'ldap': data}, + ) + if not fake: + sa.save() + logs["created"].append((clipper_uid, user.username)) logs["unmodified"] = User.objects.exclude(username__in=cases)\ .values_list("username", flat=True) diff --git a/allauth_ens/providers/clipper/provider.py b/allauth_ens/providers/clipper/provider.py index 41bb56b..4ee6169 100644 --- a/allauth_ens/providers/clipper/provider.py +++ b/allauth_ens/providers/clipper/provider.py @@ -37,8 +37,23 @@ class ClipperProvider(CASProvider): ] def extract_extra_data(self, data): + """ + If LongTermClipperAccountAdapter is in use, keep the data retrieved + from the LDAP server. + """ + from allauth.socialaccount.models import SocialAccount # noqa extra_data = super(ClipperProvider, self).extract_extra_data(data) extra_data['email'] = self.extract_email(data) + + # Preserve LDAP data at all cost. + try: + clipper_account = SocialAccount.objects.get( + provider=self.id, uid=self.extract_uid(data)) + if 'ldap' in clipper_account.extra_data: + extra_data['ldap'] = clipper_account.extra_data['ldap'] + except SocialAccount.DoesNotExist: + pass + return extra_data def message_suggest_caslogout_on_logout(self, request): diff --git a/allauth_ens/providers/clipper/tests.py b/allauth_ens/providers/clipper/tests.py index 657982c..00bb3b1 100644 --- a/allauth_ens/providers/clipper/tests.py +++ b/allauth_ens/providers/clipper/tests.py @@ -17,6 +17,18 @@ class ClipperProviderTests(CASTestCase): u = User.objects.get(username='clipper_uid') self.assertEqual(u.email, 'clipper_uid@clipper.ens.fr') + def test_extra_data_keeps_ldap_data(self): + clipper_conn = self.u.socialaccount_set.create( + uid='user', provider='clipper', + extra_data={'ldap': {'aa': 'bb'}}, + ) + + self.client_cas_login( + self.client, provider_id='clipper', username='user') + + clipper_conn.refresh_from_db() + self.assertEqual(clipper_conn.extra_data['ldap'], {'aa': 'bb'}) + class ClipperViewsTests(CASViewTestCase): diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index d9c01e5..471f479 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import re import django @@ -8,6 +10,7 @@ from django.test import TestCase, override_settings from allauth.socialaccount.models import SocialAccount +import six from allauth_cas.test.testcases import CASTestCase from fakeldap import MockLDAP from mock import patch @@ -161,8 +164,12 @@ class LongTermClipperTests(CASTestCase): _mock_ldap.reset() def _setup_ldap(self, promo=12, username="test"): + try: + buid = six.binary_type(username, 'utf-8') + except TypeError: + buid = six.binary_type(username) _mock_ldap.directory['dc=spi,dc=ens,dc=fr'] = { - 'uid': [username], + 'uid': [buid], 'cn': [b'John Smith'], 'mailRoutingAddress': [b'test@clipper.ens.fr'], 'homeDirectory': [b'/users/%d/phy/test/' % promo], @@ -188,6 +195,7 @@ class LongTermClipperTests(CASTestCase): sa = list(SocialAccount.objects.all())[-1] self.assertEqual(sa.user.id, u.id) + self.assertEqual(sa.extra_data['ldap']['entrance_year'], '12') def test_connect_disconnect(self): self._setup_ldap() @@ -312,9 +320,11 @@ class LongTermClipperTests(CASTestCase): username='test') user1 = r.context["user"] nsa1 = SocialAccount.objects.count() + conn = user1.socialaccount_set.get(provider='clipper') self.assertEqual(user1.id, user0.id) self.assertEqual(nsa1, nsa0) self.assertEqual(user1.username, "test@12") + self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12') def test_longterm_installer_from_djangocas(self): with self.settings(SOCIALACCOUNT_ADAPTER= @@ -332,9 +342,11 @@ class LongTermClipperTests(CASTestCase): username='test') user1 = r.context["user"] nsa1 = SocialAccount.objects.count() + conn = user1.socialaccount_set.get(provider='clipper') self.assertEqual(user1.id, user0.id) self.assertEqual(nsa1, nsa0 + 1) self.assertEqual(user1.username, "test@12") + self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12') def test_disconnect_ldap(self): nu0 = User.objects.count() diff --git a/setup.cfg b/setup.cfg index 179a83e..bfb5cf5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,6 @@ combine_as_imports = True default_section = THIRDPARTY include_trailing_comma = True known_allauth = allauth -known_future_library = future,six known_django = django known_first_party = allauth_ens multi_line_output = 5 From 232236b50b6ec20dc5289116aa481d112b4213b4 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 24 Jun 2018 20:11:33 +0200 Subject: [PATCH 22/47] Fix translation and indentation --- allauth_ens/locale/fr/LC_MESSAGES/django.mo | Bin 6572 -> 6615 bytes allauth_ens/locale/fr/LC_MESSAGES/django.po | 12 ++++++++---- allauth_ens/templates/account/login.html | 12 ++++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/allauth_ens/locale/fr/LC_MESSAGES/django.mo b/allauth_ens/locale/fr/LC_MESSAGES/django.mo index 3a2bb04b636abd72949f637e7a204528ebc7cfe1..bc56d21e9d798158368217bb86c9703a30e247d8 100644 GIT binary patch delta 1078 zcmZ2ueBHSIo)F7a1_lNOLk0#083qQ1C5#LV&I}9;I?NzZ1_lcj28Kum1_nzO28JaJ z3=ET57#LPFFfdrKGBCt3Ffcq}Wnc(qU|?`&V_*nnU|^We#=xM%z`*d9je((#fq_Aa zoq-{Vfq`KrI|G9k0|Ub!b_NDz1_lOq4hDuv3=H)Q*&GZEHVh05|2P;J(ij*RtT`DN z8W`_}28JUH3=H@A85lA_Q76E_;KRVcz$eJS;LE_k5G}~S zV8Ot^uu_nLVHpDh!y`ckhC&7ghE5>{22TbChTlRA4D|{O3=FQq3=C@+7#Pxo85m+1 z7#M^_7#J!T7#IpfAW`sAgn{8X0|UbpQHTR-#26U#7#JAdh(SU^TAYDF5ab|nNE)&c zXJ8O#U|@(8XJFt2g_Jl0gA@Y;L$x>qgD3+7!!&V7RIL$bs0YW*9&t!qT@;7L4V3>I zYOs(5Bx+P7AW>r^0SOs<2?hp51_p*$2}s(gmw;HfT!Mi?j)8$;qXZ;vT!4zdgzEn+ z0dY8kWIY3eGy?+zza&IJTatl6i-Cc`N|J#=g@J(~RT5%w7nHwAl7WE{np;4L5tOh% z>Oct!lwUw88$^TR8$|0eFfeF?7z_*ypbP~{Kl%&|3``6R3>qMDkY~ z&CRMTVjS$r8TpyXsl}U3c#;{}O%#j_tPBk%&*N3(fwHv?j0_AW$M7jnKEpe4b3NZ2 OPF9eXip|0jGZ_J|Xl}y* delta 1048 zcmca^yvDfxo)F7a1_lNO0|o{L83qQ1IgAVp&LB23NR)v=gN1=1l7WFilZAm{2?GN| zGYbR5Y6b=d4ORw*I0goWE36C*!3+!x#%v4>p$rTR?Q9GTIt&a9ci9*i>KGUpxY!vO zk{B2mI@uW*ycif5-mo(;C^IlHm~${NOk!ZDXNcxtV6b6eV0g#Dz>vnkz@W{^z|g?J zz%YrEfngN`1A{OZ14B9k1H(Qp1_pHo1_m{51_l8J1_nQF1_mw$28MWU1_lWR28Jwd z1_pkR1GyO(*cliYCUY||h%qoQ%;sib5Mf|o*aX#goST6`h=GCO2AE&Z!0;NX@eelx z11|#u0}l@a0}lfOgA@;hug=53Aj`nOV9LY5z{kMA5X=KHFo}nOfsKKIp&Tk+2i4ch z198AC9tH*u1_p*jQ2uJDy3J61hj$kLL$(kDgC{6zgcukU7#JATg&7#?*Dx?JcnC8v#4s>0 zd=+M3s9<1V2oiw={b>;fhUW|n3}vDa2gHdnFz7KbFq{*Egv4Jl1_nWpgTx_eM^2o9 zL7ahs!CIVwffE!`;tUK@3=9mh;tUL;3=9kv;*con6NiN0JaLA4a2#zGhs4P#aY&rq z6^9u7MH~_ZY!Z+V5s_eEP-I|WFp_|zg%AmdMKux(40525kbtCxrBLyG5|F4oApvpl zRS50tGinkOATWRtAVi`9UdDmjU7`P>_MuOrFau zuBr)UA_!1G!g#6-kUXG2`6jcms5Vqsk^vHGptPhiS&>DUQDd_yix|h|NS+u*b|VEN pBP&D0$$NMedBALAQ*8qy1B1!je3K@>;#Jx_mv0W|W;cn6i~wMrXAJ-V diff --git a/allauth_ens/locale/fr/LC_MESSAGES/django.po b/allauth_ens/locale/fr/LC_MESSAGES/django.po index d8ee51a..b3ce885 100644 --- a/allauth_ens/locale/fr/LC_MESSAGES/django.po +++ b/allauth_ens/locale/fr/LC_MESSAGES/django.po @@ -7,15 +7,15 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-06-22 22:11+0200\n" -"PO-Revision-Date: 2018-06-22 22:35+0200\n" +"POT-Creation-Date: 2018-06-24 20:10+0200\n" +"PO-Revision-Date: 2018-06-24 20:10+0200\n" +"Last-Translator: \n" +"Language-Team: \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -"Last-Translator: \n" -"Language-Team: \n" "X-Generator: Poedit 1.8.7.1\n" #: apps.py:7 @@ -168,6 +168,10 @@ msgstr "" msgid "Connect with Clipper" msgstr "Connexion via Clipper" +#: templates/account/login.html:45 +msgid "Other choices" +msgstr "Autres choix" + #: templates/account/login.html:45 msgid "Other ways to sign in/sign up" msgstr "Autres méthodes de connexion" diff --git a/allauth_ens/templates/account/login.html b/allauth_ens/templates/account/login.html index 87196a5..a683783 100644 --- a/allauth_ens/templates/account/login.html +++ b/allauth_ens/templates/account/login.html @@ -39,12 +39,12 @@ {% is_clipper_highlighted as highlight_clipper %} {% if highlight_clipper %} - + {% endif %} {% if socialaccount_providers %} From b189c1145402cb37e9d03ea964b83a4562225d7a Mon Sep 17 00:00:00 2001 From: Evarin Date: Sat, 29 Sep 2018 23:32:50 +0200 Subject: [PATCH 23/47] Detoxify + debug py34 --- allauth_ens/adapter.py | 37 ++++++++++++++++++--------------- allauth_ens/tests.py | 30 ++++++++++++++------------- allauth_ens/utils.py | 46 ++++++++++++++++++++++++------------------ 3 files changed, 63 insertions(+), 50 deletions(-) diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index e85d978..a283cc6 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -10,10 +10,14 @@ from allauth.socialaccount.models import SocialAccount import ldap -from .utils import extract_infos_from_ldap, get_ldap_infos, get_clipper_email, remove_email, init_ldap +from .utils import ( + extract_infos_from_ldap, get_clipper_email, get_ldap_infos, init_ldap, + remove_email, +) User = get_user_model() + class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): """ A class to manage the fact that people loose their account at the end of @@ -25,8 +29,8 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): If a clipper connection has already existed with the uid, it checks that this connection still belongs to the user it was associated with. - This check is performed by comparing the generated username corresponding - to this connection with the old one. + This check is performed by comparing the generated username + corresponding to this connection with the old one. If the check succeeds, it simply reactivates the clipper connection as belonging to the associated user. @@ -59,7 +63,7 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): # if a new SocialAccount is created email = ldap_data.get('email', get_clipper_email(clipper_uid)) remove_email(old_conn.user, email) - + return # The admission year is the same, we can update the model and keep @@ -87,9 +91,9 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter): user.set_unusable_password() clipper_uid = sociallogin.account.uid - ldap_data = sociallogin._ldap_data if hasattr(sociallogin, - '_ldap_data') \ - else get_ldap_infos(clipper_uid) + ldap_data = sociallogin._ldap_data if \ + hasattr(sociallogin, '_ldap_data') \ + else get_ldap_infos(clipper_uid) username = self.get_username(clipper_uid, ldap_data) email = ldap_data.get('email', get_clipper_email(clipper_uid)) @@ -147,17 +151,18 @@ def install_longterm_adapter(fake=False): accounts = {u.username: u for u in User.objects.all() if u.username.isalnum()} - l = init_ldap() + ldap_connection = init_ldap() ltc_adapter = get_adapter() - info = l.search_s('dc=spi,dc=ens,dc=fr', - ldap.SCOPE_SUBTREE, - ("(|{})".format(''.join(("(uid=%s)" % (un,)) - for un in accounts.keys()))), - ['uid', - 'cn', - 'mailRoutingAddress', - 'homeDirectory']) + info = ldap_connection.search_s( + 'dc=spi,dc=ens,dc=fr', + ldap.SCOPE_SUBTREE, + ("(|{})".format(''.join(("(uid=%s)" % (un,)) + for un in accounts.keys()))), + ['uid', + 'cn', + 'mailRoutingAddress', + 'homeDirectory']) logs = {"created": [], "updated": []} cases = [] diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index d9c01e5..805e627 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -165,12 +165,12 @@ class LongTermClipperTests(CASTestCase): 'uid': [username], 'cn': [b'John Smith'], 'mailRoutingAddress': [b'test@clipper.ens.fr'], - 'homeDirectory': [b'/users/%d/phy/test/' % promo], + 'homeDirectory': [bytes('/users/%d/phy/test/' % promo)], } def _count_ldap_queries(self): queries = _mock_ldap.ldap_methods_called() - count = len([l for l in queries if l != 'set_option']) + count = len([op for op in queries if op != 'set_option']) return count def test_new_connexion(self): @@ -267,13 +267,13 @@ class LongTermClipperTests(CASTestCase): def test_multiple_deprecation(self): self._setup_ldap(12) - r = self.client_cas_login(self.client, provider_id="clipper", - username="test") + self.client_cas_login(self.client, provider_id="clipper", + username="test") self.client.logout() self._setup_ldap(15, "truc") - r = self.client_cas_login(self.client, provider_id="clipper", - username="truc") + self.client_cas_login(self.client, provider_id="clipper", + username="truc") self.client.logout() sa0 = SocialAccount.objects.count() @@ -296,8 +296,9 @@ class LongTermClipperTests(CASTestCase): def test_longterm_installer_from_allauth(self): self._setup_ldap(12) - with self.settings(SOCIALACCOUNT_ADAPTER= - 'allauth.socialaccount.adapter.DefaultSocialAccountAdapter'): + with self.settings( + SOCIALACCOUNT_ADAPTER='allauth.socialaccount.' + 'adapter.DefaultSocialAccountAdapter'): r = self.client_cas_login(self.client, provider_id="clipper", username='test') user0 = r.context["user"] @@ -305,9 +306,9 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(user0.username, "test") self.client.logout() - l = install_longterm_adapter() + outputs = install_longterm_adapter() - self.assertEqual(l["updated"], [("test", "test@12")]) + self.assertEqual(outputs["updated"], [("test", "test@12")]) r = self.client_cas_login(self.client, provider_id="clipper", username='test') user1 = r.context["user"] @@ -317,17 +318,18 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(user1.username, "test@12") def test_longterm_installer_from_djangocas(self): - with self.settings(SOCIALACCOUNT_ADAPTER= - 'allauth.socialaccount.adapter.DefaultSocialAccountAdapter'): + with self.settings( + SOCIALACCOUNT_ADAPTER='allauth.socialaccount.' + 'adapter.DefaultSocialAccountAdapter'): user0 = User.objects.create_user('test', 'test@clipper.ens.fr', 'test') nsa0 = SocialAccount.objects.count() self._setup_ldap(12) - l = install_longterm_adapter() + outputs = install_longterm_adapter() - self.assertEqual(l["created"], [("test", "test@12")]) + self.assertEqual(outputs["created"], [("test", "test@12")]) r = self.client_cas_login(self.client, provider_id="clipper", username='test') user1 = r.context["user"] diff --git a/allauth_ens/utils.py b/allauth_ens/utils.py index 230d55d..afd29a9 100644 --- a/allauth_ens/utils.py +++ b/allauth_ens/utils.py @@ -20,23 +20,26 @@ DEPARTMENTS_LIST = { 'pei': u'PEI', } + def init_ldap(): - server = getattr(settings, "CLIPPER_LDAP_SERVER", "ldaps://ldap.spi.ens.fr:636") + server = getattr(settings, "CLIPPER_LDAP_SERVER", + "ldaps://ldap.spi.ens.fr:636") ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) - l = ldap.initialize(server) - l.set_option(ldap.OPT_REFERRALS, 0) - l.set_option(ldap.OPT_PROTOCOL_VERSION, 3) - l.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) - l.set_option(ldap.OPT_X_TLS_DEMAND, True) - l.set_option(ldap.OPT_DEBUG_LEVEL, 255) - l.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) - l.set_option(ldap.OPT_TIMEOUT, 10) - return l + ldap_connection = ldap.initialize(server) + ldap_connection.set_option(ldap.OPT_REFERRALS, 0) + ldap_connection.set_option(ldap.OPT_PROTOCOL_VERSION, 3) + ldap_connection.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) + ldap_connection.set_option(ldap.OPT_X_TLS_DEMAND, True) + ldap_connection.set_option(ldap.OPT_DEBUG_LEVEL, 255) + ldap_connection.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) + ldap_connection.set_option(ldap.OPT_TIMEOUT, 10) + return ldap_connection + def extract_infos_from_ldap(infos): data = {} - + # Name if 'cn' in infos: data['name'] = infos['cn'][0].decode("utf-8") @@ -57,25 +60,26 @@ def extract_infos_from_ldap(infos): pmail = infos.get('mailRoutingAddress', []) if pmail: data['email'] = pmail[0].decode("utf-8") - + # User id if 'uid' in infos: data['clipper_uid'] = infos['uid'][0].decode("utf-8").strip().lower() - + return data + def get_ldap_infos(clipper_uid): assert clipper_uid.isalnum() data = {} try: - l = init_ldap() + ldap_connection = init_ldap() - info = l.search_s('dc=spi,dc=ens,dc=fr', - ldap.SCOPE_SUBTREE, - ('(uid=%s)' % (clipper_uid,)), - ['cn', - 'mailRoutingAddress', - 'homeDirectory']) + info = ldap_connection.search_s('dc=spi,dc=ens,dc=fr', + ldap.SCOPE_SUBTREE, + ('(uid=%s)' % (clipper_uid,)), + ['cn', + 'mailRoutingAddress', + 'homeDirectory']) if len(info) > 0: data = extract_infos_from_ldap(info[0][1]) @@ -85,9 +89,11 @@ def get_ldap_infos(clipper_uid): return data + def get_clipper_email(clipper): return '{}@clipper.ens.fr'.format(clipper.strip().lower()) + def remove_email(user, email): """ Removes an email address of a user. From a812a43a2cde3e243f4b9c578cc0b6873142fb71 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sat, 29 Sep 2018 23:58:32 +0200 Subject: [PATCH 24/47] Compatibilite des tests en py2 --- allauth_ens/tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index fe3edc3..b902d81 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -166,13 +166,15 @@ class LongTermClipperTests(CASTestCase): def _setup_ldap(self, promo=12, username="test"): try: buid = six.binary_type(username, 'utf-8') + home = six.binary_type('/users/%d/phy/test/' % promo, 'utf-8') except TypeError: buid = six.binary_type(username) + home = six.binary_type('/users/%d/phy/test/' % promo) _mock_ldap.directory['dc=spi,dc=ens,dc=fr'] = { 'uid': [buid], 'cn': [b'John Smith'], 'mailRoutingAddress': [b'test@clipper.ens.fr'], - 'homeDirectory': [bytes('/users/%d/phy/test/' % promo)], + 'homeDirectory': [home], } def _count_ldap_queries(self): From e193239231be07cc0fd5fc53fd855ab1a30227a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 30 Sep 2018 00:15:09 +0200 Subject: [PATCH 25/47] Lint with flake8 --- allauth_ens/templatetags/allauth_ens.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/allauth_ens/templatetags/allauth_ens.py b/allauth_ens/templatetags/allauth_ens.py index e00c6b6..f814a8a 100644 --- a/allauth_ens/templatetags/allauth_ens.py +++ b/allauth_ens/templatetags/allauth_ens.py @@ -37,5 +37,6 @@ def is_open_for_signup(context): @simple_tag def is_clipper_highlighted(): - return ('allauth_ens.providers.clipper' in getattr(settings, 'INSTALLED_APPS', [])) \ + installed_apps = getattr(settings, 'INSTALLED_APPS', []) + return ('allauth_ens.providers.clipper' in installed_apps) \ and getattr(settings, 'ALLAUTH_ENS_HIGHLIGHT_CLIPPER', True) From 28a8127d35c436fde8abca342b3e4065fec69f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 30 Sep 2018 00:47:57 +0200 Subject: [PATCH 26/47] Bump version: 1.1.0 --- CHANGELOG.rst | 14 +++++++++++--- allauth_ens/__init__.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index de9840f..c4a75fe 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,13 @@ -****************** -1.0.0 (unreleased) -****************** +***** +1.1.0 +***** + +- Add long-term support for Clipper (see README for instructions on setup/update) +- Highlight Clipper provider in login screens (see related settings in README) +- Vendorize some static files (JS) + +******* +1.0.0b2 +******* - First official release. diff --git a/allauth_ens/__init__.py b/allauth_ens/__init__.py index 17fbb20..4a92723 100644 --- a/allauth_ens/__init__.py +++ b/allauth_ens/__init__.py @@ -1,3 +1,3 @@ -__version__ = '1.0.0b2' +__version__ = '1.1.0' default_app_config = 'allauth_ens.apps.ENSAllauthConfig' From c0059bea800594c732edd399b462d4046327f9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 29 Sep 2018 23:14:57 +0200 Subject: [PATCH 27/47] Setup GitLab CI --- .gitlab-ci.yml | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..a9c8e91 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,49 @@ +image: python + +stages: + - test + +cache: + paths: + - vendor/apt + - .tox + +before_script: + - mkdir -p vendor/apt + # http://www.python-ldap.org/en/latest/installing.html + - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq build-essential python2.7-dev python3-dev libldap2-dev libsasl2-dev + - pip install tox + +python27: + image: python:2.7 + stage: test + script: tox -e py27 + +python34: + image: python:3.4 + stage: test + script: tox -e py34 + +python35: + image: python:3.5 + stage: test + script: + - tox -e py35 + - tox -e cov_combine + # Catch coverage here. Python3.5 supports more Django versions. + coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' + +python36: + image: python:3.6 + stage: test + script: tox -e py36 + +flake8: + image: python:3.6 + stage: test + script: tox -e flake8 + +isort: + image: python:3.6 + stage: test + script: tox -e isort From 845b09cc2bab51ae9647dff51093b4f56e74fc1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 5 Oct 2018 23:33:11 +0200 Subject: [PATCH 28/47] Disable coverage in CI GitLab may not like this regex... --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a9c8e91..d1f5c52 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,7 +31,8 @@ python35: - tox -e py35 - tox -e cov_combine # Catch coverage here. Python3.5 supports more Django versions. - coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' + # For GitLab, keep this commented. + # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' python36: image: python:3.6 From 4a119c7d13bf7ea7399680d59fc6a97a8046f2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Oct 2018 11:57:36 +0200 Subject: [PATCH 29/47] Show ill-formed clipper uids in error messages --- allauth_ens/tests.py | 10 ++++++++++ allauth_ens/utils.py | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index b902d81..9d0e366 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -9,6 +9,7 @@ from django.core import mail from django.test import TestCase, override_settings from allauth.socialaccount.models import SocialAccount +from allauth_ens.utils import get_ldap_infos import six from allauth_cas.test.testcases import CASTestCase @@ -367,3 +368,12 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(nu0, nu1) self.assertEqual(nsa0, nsa1) ldap_patcher.start() + + def test_invalid_uid(self): + self._setup_ldap(12, "test") + uids = [" test", "test ", "\\test", "test)"] + for uid in uids: + with self.assertRaises(ValueError) as cm: + get_ldap_infos(uid) + print(cm.exception) + self.assertIn(uid, cm.exception.message) diff --git a/allauth_ens/utils.py b/allauth_ens/utils.py index afd29a9..02d4c10 100644 --- a/allauth_ens/utils.py +++ b/allauth_ens/utils.py @@ -69,7 +69,11 @@ def extract_infos_from_ldap(infos): def get_ldap_infos(clipper_uid): - assert clipper_uid.isalnum() + if not clipper_uid.isalnum(): + raise ValueError( + 'Invalid uid "{}": contains non-alphanumeric characters' + .format(clipper_uid) + ) data = {} try: ldap_connection = init_ldap() From 509a1bbd96df0b9028eefb84f360573e5ffed79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 6 Oct 2018 12:09:04 +0200 Subject: [PATCH 30/47] Run CI for all Django versions for each Python one --- .gitlab-ci.yml | 23 +++++++++++++++++++---- tox.ini | 1 + 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d1f5c52..a7ff218 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,18 +17,31 @@ before_script: python27: image: python:2.7 stage: test - script: tox -e py27 + script: + - tox -e django18-py27 + - tox -e django19-py27 + - tox -e django110-py27 + - tox -e django111-py27 python34: image: python:3.4 stage: test - script: tox -e py34 + script: + - tox -e django18-py34 + - tox -e django19-py34 + - tox -e django110-py34 + - tox -e django111-py34 + - tox -e django20-py34 python35: image: python:3.5 stage: test script: - - tox -e py35 + - tox -e django18-py35 + - tox -e django19-py35 + - tox -e django110-py35 + - tox -e django111-py35 + - tox -e django20-py35 - tox -e cov_combine # Catch coverage here. Python3.5 supports more Django versions. # For GitLab, keep this commented. @@ -37,7 +50,9 @@ python35: python36: image: python:3.6 stage: test - script: tox -e py36 + script: + - tox -e django111-py36 + - tox -e django20-py36 flake8: image: python:3.6 diff --git a/tox.ini b/tox.ini index 513c573..33551bd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +# Update .gitlab-ci.yml if you change the Django/Python matrix envlist = django{18,19,110}-py{27,34,35}, django111-py{27,34,35,36}, From e936fb70deeb447573fd6fb0f893b8679928012a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 6 Oct 2018 12:11:08 +0200 Subject: [PATCH 31/47] Try caching pip packages --- .gitlab-ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a7ff218..5235580 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,13 +3,16 @@ image: python stages: - test +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/vendor/pip" + cache: paths: - - vendor/apt + - vendor - .tox before_script: - - mkdir -p vendor/apt + - mkdir -p vendor/{apt,pip} # http://www.python-ldap.org/en/latest/installing.html - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq build-essential python2.7-dev python3-dev libldap2-dev libsasl2-dev - pip install tox From b0451097fc23bedf868c490bc588197ef8931145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 6 Oct 2018 13:27:10 +0200 Subject: [PATCH 32/47] Don't cache .tox in CI, it's too big ; hope pip cache is fine enough --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5235580..6e9d0fa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,7 +9,6 @@ variables: cache: paths: - vendor - - .tox before_script: - mkdir -p vendor/{apt,pip} From d22d1ea567db6100426ff741705a5afc227f66b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Oct 2018 16:15:31 +0200 Subject: [PATCH 33/47] remove leftover debug print --- allauth_ens/tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index 9d0e366..f8bf5c0 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -375,5 +375,4 @@ class LongTermClipperTests(CASTestCase): for uid in uids: with self.assertRaises(ValueError) as cm: get_ldap_infos(uid) - print(cm.exception) self.assertIn(uid, cm.exception.message) From fc16b3fedfeea88e1117d26737c0b3f83a70f822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Oct 2018 17:18:40 +0200 Subject: [PATCH 34/47] Fix non py3-compatible test in CI --- allauth_ens/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index f8bf5c0..002a8f3 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -375,4 +375,4 @@ class LongTermClipperTests(CASTestCase): for uid in uids: with self.assertRaises(ValueError) as cm: get_ldap_infos(uid) - self.assertIn(uid, cm.exception.message) + self.assertIn(uid, str(cm.exception)) From a27921016bcb7b2ce8cd136a2b23d586d531a3dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Oct 2018 22:57:49 +0200 Subject: [PATCH 35/47] fix import ordering --- allauth_ens/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index 002a8f3..a35ca3b 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -9,13 +9,14 @@ from django.core import mail from django.test import TestCase, override_settings from allauth.socialaccount.models import SocialAccount -from allauth_ens.utils import get_ldap_infos import six from allauth_cas.test.testcases import CASTestCase from fakeldap import MockLDAP from mock import patch +from allauth_ens.utils import get_ldap_infos + from .adapter import deprecate_clippers, install_longterm_adapter _mock_ldap = MockLDAP() From 85f09ca9f75442f038f97c8af40191f2c7724c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 21 Oct 2018 15:43:20 +0200 Subject: [PATCH 36/47] Pin python-cas & bump allauth-cas Pin python-cas to avoid breaking login, and keep working redirection after CAS logout. --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6b8231b..d461833 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,10 @@ setup( include_package_data=True, install_requires=[ 'django-allauth', - 'django-allauth-cas>=1.0.0b2,<1.1', + 'django-allauth-cas>=1.0,<1.1', + # The version of CAS used by cas.eleves.ens.fr is unclear… + # Stick to python-cas 1.2.0 until we solve this mystery. + 'python-cas==1.2.0', 'django-widget-tweaks', 'python-ldap', ], From 0272edd77db4f2a56e57da29342ec9d72422d714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 21 Oct 2018 16:10:04 +0200 Subject: [PATCH 37/47] Bump version: 1.1.1 --- CHANGELOG.rst | 6 ++++++ allauth_ens/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c4a75fe..d0f3f9d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,9 @@ +***** +1.1.1 +***** + +- Pin python-cas version to make Clipper provider work. !17 + ***** 1.1.0 ***** diff --git a/allauth_ens/__init__.py b/allauth_ens/__init__.py index 4a92723..28fe87d 100644 --- a/allauth_ens/__init__.py +++ b/allauth_ens/__init__.py @@ -1,3 +1,3 @@ -__version__ = '1.1.0' +__version__ = '1.1.1' default_app_config = 'allauth_ens.apps.ENSAllauthConfig' From 2c57fb7a4dc6143230c0f60d01f76ff58e15bd23 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 21 Oct 2018 22:27:15 +0200 Subject: [PATCH 38/47] New parameters for install_longterm --- allauth_ens/adapter.py | 12 +++- .../management/commands/install_longterm.py | 38 ++++++++++- allauth_ens/tests.py | 65 +++++++++++++++++++ 3 files changed, 111 insertions(+), 4 deletions(-) diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index 662c98a..c68a5bf 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -147,14 +147,20 @@ def deprecate_clippers(): clippers.update(provider='clipper_inactive') -def install_longterm_adapter(fake=False): +def install_longterm_adapter(fake=False, accounts=None): """ Manages the transition from an older django_cas or an allauth_ens installation without LongTermClipperAccountAdapter + + accounts is an optional dictionary containing the association between + clipper usernames and django's User accounts. If not provided, the + function will assumer Users' usernames are their clipper uid. """ - accounts = {u.username: u for u in User.objects.all() - if u.username.isalnum()} + if accounts is None: + accounts = {u.username: u for u in User.objects.all() + if u.username.isalnum()} + ldap_connection = init_ldap() ltc_adapter = get_adapter() diff --git a/allauth_ens/management/commands/install_longterm.py b/allauth_ens/management/commands/install_longterm.py index 40ccde3..40cc988 100644 --- a/allauth_ens/management/commands/install_longterm.py +++ b/allauth_ens/management/commands/install_longterm.py @@ -1,6 +1,9 @@ # coding: utf-8 +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand +from allauth.socialaccount.models import SocialAccount + from allauth_ens.adapter import install_longterm_adapter @@ -17,10 +20,43 @@ class Command(BaseCommand): help=('Does not save the models created/updated,' 'only shows the list'), ) + parser.add_argument( + '--use-socialaccounts', + action='store_true', + default=False, + help=('Use the existing SocialAccounts rather than all the Users'), + ) + parser.add_argument( + '--clipper-field', + default=None, + type=str + ) pass def handle(self, *args, **options): - logs = install_longterm_adapter(options.get("fake", False)) + fake = options.get("fake", False) + + if options.get('use_socialaccounts', False): + accounts = {account.uid: account.user for account in + (SocialAccount.objects.filter(provider="clipper") + .prefetch_related("user"))} + elif options.get('clipper_field', None): + fields = options['clipper_field'].split('.') + User = get_user_model() + + def get_subattr(obj, fields): + # Allows to follow OneToOne relationships + if len(fields) == 1: + return getattr(obj, fields[0]) + return get_subattr(getattr(obj, fields[0]), fields[1:]) + + accounts = {get_subattr(account, fields): account for account in + User.objects.all()} + else: + accounts = None + + logs = install_longterm_adapter(fake, accounts) + self.stdout.write("Social accounts created : %d" % len(logs["created"])) self.stdout.write(" ".join(("%s -> %s" % s) for s in logs["created"])) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index a35ca3b..a7afe33 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -7,6 +7,7 @@ from django.contrib.auth import HASH_SESSION_KEY, get_user_model from django.contrib.sites.models import Site from django.core import mail from django.test import TestCase, override_settings +from django.test.utils import captured_stdout from allauth.socialaccount.models import SocialAccount @@ -18,6 +19,7 @@ from mock import patch from allauth_ens.utils import get_ldap_infos from .adapter import deprecate_clippers, install_longterm_adapter +from .management.commands.install_longterm import Command as InstallLongterm _mock_ldap = MockLDAP() ldap_patcher = patch('allauth_ens.utils.ldap.initialize', @@ -331,6 +333,69 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(user1.username, "test@12") self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12') + def test_longterm_installer_from_allauth_command_username(self): + self._setup_ldap(12) + with self.settings( + SOCIALACCOUNT_ADAPTER='allauth.socialaccount.' + 'adapter.DefaultSocialAccountAdapter'): + r = self.client_cas_login(self.client, provider_id="clipper", + username='test') + user0 = r.context["user"] + nsa0 = SocialAccount.objects.count() + self.assertEqual(user0.username, "test") + self.client.logout() + + with captured_stdout() as stdout: + command = InstallLongterm() + command.handle(clipper_field='username') + + output = stdout.getvalue() + self.assertIn('test -> test@12', output) + + r = self.client_cas_login(self.client, provider_id="clipper", + username='test') + user1 = r.context["user"] + nsa1 = SocialAccount.objects.count() + conn = user1.socialaccount_set.get(provider='clipper') + self.assertEqual(user1.id, user0.id) + self.assertEqual(nsa1, nsa0) + self.assertEqual(user1.username, "test@12") + self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12') + + def test_longterm_installer_from_allauth_command_socialaccounts(self): + self._setup_ldap(12) + with self.settings( + SOCIALACCOUNT_ADAPTER='allauth.socialaccount.' + 'adapter.DefaultSocialAccountAdapter'): + r = self.client_cas_login(self.client, provider_id="clipper", + username='test') + user0 = r.context["user"] + self.assertEqual(user0.username, "test") + self.client.logout() + + user1 = User.objects.create_user('bidule', 'bidule@clipper.ens.fr', + 'bidule') + nsa0 = SocialAccount.objects.count() + + with captured_stdout() as stdout: + command = InstallLongterm() + command.handle(use_socialaccounts=True) + + output = stdout.getvalue() + self.assertIn('test -> test@12', output) + self.assertNotIn('bidule ->', output) + + r = self.client_cas_login(self.client, provider_id="clipper", + username='test') + user1 = r.context["user"] + nsa1 = SocialAccount.objects.count() + conn = user1.socialaccount_set.get(provider='clipper') + self.assertEqual(user1.id, user0.id) + self.assertEqual(nsa1, nsa0) + self.assertEqual(nsa1, nsa0) + self.assertEqual(user1.username, "test@12") + self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12') + def test_longterm_installer_from_djangocas(self): with self.settings( SOCIALACCOUNT_ADAPTER='allauth.socialaccount.' From 126f367e7665781eb15544331739b06a48740e76 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 21 Oct 2018 22:35:44 +0200 Subject: [PATCH 39/47] flake8 --- allauth_ens/management/commands/install_longterm.py | 6 +++--- allauth_ens/tests.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/allauth_ens/management/commands/install_longterm.py b/allauth_ens/management/commands/install_longterm.py index 40cc988..fdff5f1 100644 --- a/allauth_ens/management/commands/install_longterm.py +++ b/allauth_ens/management/commands/install_longterm.py @@ -43,18 +43,18 @@ class Command(BaseCommand): elif options.get('clipper_field', None): fields = options['clipper_field'].split('.') User = get_user_model() - + def get_subattr(obj, fields): # Allows to follow OneToOne relationships if len(fields) == 1: return getattr(obj, fields[0]) return get_subattr(getattr(obj, fields[0]), fields[1:]) - + accounts = {get_subattr(account, fields): account for account in User.objects.all()} else: accounts = None - + logs = install_longterm_adapter(fake, accounts) self.stdout.write("Social accounts created : %d" diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index a7afe33..1824b60 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -361,7 +361,7 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(nsa1, nsa0) self.assertEqual(user1.username, "test@12") self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12') - + def test_longterm_installer_from_allauth_command_socialaccounts(self): self._setup_ldap(12) with self.settings( @@ -384,7 +384,7 @@ class LongTermClipperTests(CASTestCase): output = stdout.getvalue() self.assertIn('test -> test@12', output) self.assertNotIn('bidule ->', output) - + r = self.client_cas_login(self.client, provider_id="clipper", username='test') user1 = r.context["user"] @@ -395,7 +395,7 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(nsa1, nsa0) self.assertEqual(user1.username, "test@12") self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12') - + def test_longterm_installer_from_djangocas(self): with self.settings( SOCIALACCOUNT_ADAPTER='allauth.socialaccount.' From 1a91ca80904c4d896d97b656389051ceafdbc9d3 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 21 Oct 2018 23:21:47 +0200 Subject: [PATCH 40/47] Flag to keep usernames in install_longterm --- allauth_ens/adapter.py | 7 +++-- .../management/commands/install_longterm.py | 11 ++++++- allauth_ens/tests.py | 31 ++++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/allauth_ens/adapter.py b/allauth_ens/adapter.py index c68a5bf..3c02f2a 100644 --- a/allauth_ens/adapter.py +++ b/allauth_ens/adapter.py @@ -147,7 +147,7 @@ def deprecate_clippers(): clippers.update(provider='clipper_inactive') -def install_longterm_adapter(fake=False, accounts=None): +def install_longterm_adapter(fake=False, accounts=None, keep_usernames=False): """ Manages the transition from an older django_cas or an allauth_ens installation without LongTermClipperAccountAdapter @@ -184,7 +184,10 @@ def install_longterm_adapter(fake=False, accounts=None): user = accounts.get(clipper_uid, None) if user is None: continue - user.username = ltc_adapter.get_username(clipper_uid, data) + + if not keep_usernames: + user.username = ltc_adapter.get_username(clipper_uid, data) + if fake: cases.append(clipper_uid) else: diff --git a/allauth_ens/management/commands/install_longterm.py b/allauth_ens/management/commands/install_longterm.py index fdff5f1..a839555 100644 --- a/allauth_ens/management/commands/install_longterm.py +++ b/allauth_ens/management/commands/install_longterm.py @@ -26,6 +26,14 @@ class Command(BaseCommand): default=False, help=('Use the existing SocialAccounts rather than all the Users'), ) + parser.add_argument( + '--keep-usernames', + action='store_true', + default=False, + help=('Do not apply the username template (e.g. clipper@promo) to' + 'the existing account, only populate the SocialAccounts with' + 'ldap informations'), + ) parser.add_argument( '--clipper-field', default=None, @@ -35,6 +43,7 @@ class Command(BaseCommand): def handle(self, *args, **options): fake = options.get("fake", False) + keep_usernames = options.get("keep_usernames", False) if options.get('use_socialaccounts', False): accounts = {account.uid: account.user for account in @@ -55,7 +64,7 @@ class Command(BaseCommand): else: accounts = None - logs = install_longterm_adapter(fake, accounts) + logs = install_longterm_adapter(fake, accounts, keep_usernames) self.stdout.write("Social accounts created : %d" % len(logs["created"])) diff --git a/allauth_ens/tests.py b/allauth_ens/tests.py index 1824b60..61bb3a4 100644 --- a/allauth_ens/tests.py +++ b/allauth_ens/tests.py @@ -333,7 +333,7 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(user1.username, "test@12") self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12') - def test_longterm_installer_from_allauth_command_username(self): + def test_longterm_installer_from_allauth_command_using_username(self): self._setup_ldap(12) with self.settings( SOCIALACCOUNT_ADAPTER='allauth.socialaccount.' @@ -362,6 +362,35 @@ class LongTermClipperTests(CASTestCase): self.assertEqual(user1.username, "test@12") self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12') + def test_longterm_installer_from_allauth_command_keeping_username(self): + self._setup_ldap(12) + with self.settings( + SOCIALACCOUNT_ADAPTER='allauth.socialaccount.' + 'adapter.DefaultSocialAccountAdapter'): + r = self.client_cas_login(self.client, provider_id="clipper", + username='test') + user0 = r.context["user"] + nsa0 = SocialAccount.objects.count() + self.assertEqual(user0.username, "test") + self.client.logout() + + with captured_stdout() as stdout: + command = InstallLongterm() + command.handle(keep_usernames=True) + + output = stdout.getvalue() + self.assertIn('test -> test', output) + + r = self.client_cas_login(self.client, provider_id="clipper", + username='test') + user1 = r.context["user"] + nsa1 = SocialAccount.objects.count() + conn = user1.socialaccount_set.get(provider='clipper') + self.assertEqual(user1.id, user0.id) + self.assertEqual(nsa1, nsa0) + self.assertEqual(user1.username, "test") + self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12') + def test_longterm_installer_from_allauth_command_socialaccounts(self): self._setup_ldap(12) with self.settings( From a30d3866c55f28bfb5575d5548a57aa64a96b5e0 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 6 Jan 2019 11:47:16 +0100 Subject: [PATCH 41/47] Install options explained in readme --- README.rst | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 33ec1c7..c169e73 100644 --- a/README.rst +++ b/README.rst @@ -216,11 +216,23 @@ Customize behaviour, and for instance attribute a default entrance year. Initial migration - If you used allauth without LongTermClipperAccountAdapter, or another CAS - interface to log in, you need to update the Users to the new username policy, - and (in the second case) to create the SocialAccount instances to link CAS and - Users. This can be done easily with ``$ python manage.py install_longterm``. - + Description + If you used allauth without LongTermClipperAccountAdapter, or another CAS + interface to log in, you need to update the Users to the new username policy, + and (in the second case) to create the SocialAccount instances to link CAS and + Users. This can be done easily with ``$ python manage.py install_longterm``. + + Install_longterm options + - ``--use-socialaccounts``: Use the existing SocialAccounts rather than all the Users. Useful if you are already using Allauth and don't want ``install_longterm`` to mess with the non-clipper authentications. + - ``--keep-usernames``: Do not apply the username template (e.g. ``clipper@promo``) to the existing accounts, only populate the SocialAccounts with LDAP informations. Useful if you don't want to change the usernames of previous users, but do want such a template for future accounts. + - ``--clipper-field ``: Use a special field rather than the username to get the clipper username (for LDAP lookup and SocialAccount creation/update). This parameter is compatible with ForeignKeys (e.g. ``profile.clipper``). Note: ``--use-socialaccounts`` will ignore the ``--clipper-field`` parameter. + - ``--fake``: Do not modify the database. Use it to test there is no conflict, and be sure the changes are the ones expected. This command does not check for uniqueness errors, so there it may succeed and the actual command fail eventually. + + Typical use cases + - *Django-cas-ng -> Longterm*: Use ``install_longterm`` without parameters, or maybe ``--keep-usernames``. If you had a custom username handling, ``--clipper_field`` may be useful. + - *Allauth -> Longterm*: Use ``install_longterm`` with ``--use-socialaccounts``, and maybe ``--keep-usernames``. + + ********* Demo Site ********* From 884020cc20311e79de964fef7e5aa1472e202af2 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 6 Jan 2019 12:08:07 +0100 Subject: [PATCH 42/47] Make flake8 happy --- allauth_ens/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/allauth_ens/utils.py b/allauth_ens/utils.py index 02d4c10..7da3c6c 100644 --- a/allauth_ens/utils.py +++ b/allauth_ens/utils.py @@ -116,8 +116,7 @@ def remove_email(user, email): # Prefer a verified mail. new_primary = ( - others.filter(verified=True).last() or - others.last() + others.filter(verified=True).last() or others.last() ) if new_primary: From 572d58722d037354e79513757720b6d9c5970f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 6 Jan 2019 14:08:44 +0100 Subject: [PATCH 43/47] Bump version: 1.1.2 --- CHANGELOG.rst | 7 +++++++ allauth_ens/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d0f3f9d..7f3e26d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,10 @@ +***** +1.1.2 +***** + +- Options added to ``install_longterm`` command to preserve usernames and source + clipper identifiers from various sources. !18 + ***** 1.1.1 ***** diff --git a/allauth_ens/__init__.py b/allauth_ens/__init__.py index 28fe87d..d805e10 100644 --- a/allauth_ens/__init__.py +++ b/allauth_ens/__init__.py @@ -1,3 +1,3 @@ -__version__ = '1.1.1' +__version__ = '1.1.2' default_app_config = 'allauth_ens.apps.ENSAllauthConfig' From 239247efc90133b096926f815a431f7508e1ead2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Tue, 25 Jun 2019 10:36:08 +0200 Subject: [PATCH 44/47] Deploy translations with pip package --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 906277e..314f576 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include LICENSE README.rst recursive-include allauth_ens/static * recursive-include allauth_ens/templates * +recursive-include allauth_ens/locale * From 4c8c66b00522e67c739f53ad50867673dab73fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Tue, 25 Jun 2019 10:37:15 +0200 Subject: [PATCH 45/47] Add missing translations --- allauth_ens/locale/fr/LC_MESSAGES/django.mo | Bin 6615 -> 7277 bytes allauth_ens/locale/fr/LC_MESSAGES/django.po | 14 ++++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/allauth_ens/locale/fr/LC_MESSAGES/django.mo b/allauth_ens/locale/fr/LC_MESSAGES/django.mo index bc56d21e9d798158368217bb86c9703a30e247d8..9f7f6f2f601683f930e073772c11a7eaf8014833 100644 GIT binary patch delta 1756 zcmca^{MMrWo)F7a1_lNO69xtb83qQ1J&X(t?hFhJG0Y%Q28Ie228Kum28K!&28JaJ z3=Ef97#LPFFfdfGGBCt3FfgdFF)#!(FfcT;F))NOFfd$aV_?u>U|`T?XJDvfU|@)1 zXJANTU|_h(&cKksz`$U`!NB0fz`!t@Z8wLi3`J4<4 zX$%YucQ_ds8WOG1KtmL$Z38zmVS1Q{3@4oEW8gA?5)Nd^XS z1_p-rk_-%-3=9nHQVa}IARkFFFo=RIl7d8qlN2b<7#M=2AO@yOL87b<%AY6&F?fX( zB+7P4L89!06ePs1N-;1fGB7ZFmV%@mQR#Y!g|^ZR3~~$%4DQm9M3W{B5wDkq7|Nt*!@+)yDD z3Y42csUMU>Kn|5;fP_fBA(Aps834*kn$WU*%st4r=Wd=y} zfD$Jtiy1@ZK-o^8fq_8-%GYIJU|@o>Ss5Tv2g(@?3=EsiSxzx)1mvV97N;r{XQt;V zWacT9XO?6rx?{UKAmLP~0KW=d*`LP@?ta$;U`YK}rqetLRlUb;eNUP-?))MAB_%+#V{NGK)dl@#kJWIKOoBXQ2tq^$-)Q z6jBbaOwLR>yiy@WJuxq@G$*x4AtzBG8RX82%=|os!wVGh@=J<9@l%qjP?njffawBo z41w&{QwYvfNKVa3R5-k{Br&r@p)@a5A+@L|wX{e_0ql!{{L-@2Dup7O)UwpP%p!&2 v(jo;|@S%rZPNG6VVtOjbzi{_Jg0fm{ftc^%l_eSZDXGOM@|&f^_c8$hjY2tt delta 1142 zcmaEBaoxE7o)F7a1_lNOLk0#083qQ1C5#LV&I}9;I?NzZ1_lcj28Kum1_nzO28JaJ z3=ET57#LPFFfdrKGBCt3Ffcq}Wnc(qU|?`&V_*nnU|^We#=xM%z`*d9je((#fq_Aa zoq-{Vfq`KrI|G9k0|Ub!b_NDz1_lOq4hDuv3=H)Q*&GZEHVh05|2P;J(ij*RtT`DN z8W`_}28JUH3=H@A85lA_Q76E_;KRVcz$eJS;LE_k5G}~S zV8Ot^uu_nLVHpDh!y`ckhC&7ghE5>{22TbChTlRA4D|{O3=FQq3=C@+7#Pxo85m+1 z7#M^_7#J!T7#IpfAW`sAgn{8X0|UbpQHTR-#26U#7#JAdh(SU^TAYDF5ab|nNE)&c zXJ8O#U|@(8XJFt2g_Jl0gA@Y;L$x>qgD3+7!!&V7RIL$bs0YW*9&t!qT@;7L4V3>I zYOs(5Bx+P7AW>r^0SOs<2?hp51_p*$2}s(gmw;HfT!Mi?j)8$;qXZ;vT!4zdgzEn+ z0dY8kWIY3eGy?+zza&IJTatl6i-Cc`N|J#=g@J(~RT5%w7nHwAl7WE{np;4L5tOh% z>Oct!lwUw88$^TR8$|0eFfeF?7z_*ypbP~{Kl%&|3``6R3>qMDQ2c{@z{&s#0Z<`wW_*}O-jnRzmk 1);\n" -"X-Generator: Poedit 1.8.7.1\n" +"X-Generator: Poedit 2.2.1\n" #: apps.py:7 msgid "ENS Authentication" @@ -177,7 +177,6 @@ msgid "Other ways to sign in/sign up" msgstr "Autres méthodes de connexion" #: templates/account/login.html:65 -#, fuzzy msgid "" "\n" " Please sign in with one of your existing third party accounts, or with " @@ -185,8 +184,8 @@ msgid "" " " msgstr "" "\n" -" Merci de vous connecter avec un de vos comptes tiers existants, ou avec " -"le formulaire ci-dessous.\n" +" Connectez-vous avec l'un de vos comptes tiers existants, ou avec le " +"formulaire ci-dessous.\n" " " #: templates/account/login.html:70 @@ -358,7 +357,6 @@ msgstr "" #: templates/socialaccount/connections.html:5 #: templates/socialaccount/connections.html:6 -#, fuzzy msgid "Account Connections" msgstr "Méthodes de connexion" @@ -390,7 +388,7 @@ msgid "Login Cancelled" msgstr "Connexion annulée" #: templates/socialaccount/login_cancelled.html:13 -#, fuzzy, python-format +#, python-format msgid "" "\n" " You decided to cancel logging into our site using one of your existing " @@ -399,7 +397,7 @@ msgid "" msgstr "" "\n" " Vous avez décidé d'annuler la connexion à notre site via un de vos " -"comptes existants. Si cela était une erreur, merci de retourner à la page de connexion." #: templates/socialaccount/signup.html:11 From ad4ebfe7efb7ebf59b3f544c868e5685b847d94e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Wed, 25 Dec 2019 11:55:07 +0100 Subject: [PATCH 46/47] Bump version: 1.1.3 --- CHANGELOG.rst | 7 +++++++ allauth_ens/__init__.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7f3e26d..f7c8f28 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,10 @@ +***** +1.1.3 +***** + +- Fix: translation was not included in the package + + ***** 1.1.2 ***** diff --git a/allauth_ens/__init__.py b/allauth_ens/__init__.py index d805e10..8aced79 100644 --- a/allauth_ens/__init__.py +++ b/allauth_ens/__init__.py @@ -1,3 +1,3 @@ -__version__ = '1.1.2' +__version__ = "1.1.3" -default_app_config = 'allauth_ens.apps.ENSAllauthConfig' +default_app_config = "allauth_ens.apps.ENSAllauthConfig" From 122d99e515c512167d36b394a89cda51e0c4ab47 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Tue, 22 Oct 2024 10:53:31 +0200 Subject: [PATCH 47/47] fix: ugettext does not exist anymore --- allauth_ens/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allauth_ens/apps.py b/allauth_ens/apps.py index 987188b..a08f2b8 100644 --- a/allauth_ens/apps.py +++ b/allauth_ens/apps.py @@ -1,5 +1,5 @@ from django.apps import AppConfig -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class ENSAllauthConfig(AppConfig):