LongTermClipper preserving LDAP data #14
6 changed files with 72 additions and 20 deletions
|
@ -175,6 +175,10 @@ usernames won't be reused later.
|
||||||
This adapter also handles getting basic information about the user from SPI's
|
This adapter also handles getting basic information about the user from SPI's
|
||||||
LDAP.
|
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
|
Configuration
|
||||||
Set ``SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'``
|
Set ``SOCIALACCOUNT_ADAPTER='allauth_ens.adapter.LongTermClipperAccountAdapter'``
|
||||||
in `settings.py`
|
in `settings.py`
|
||||||
|
@ -199,7 +203,6 @@ Customize
|
||||||
You can customize the SocialAccountAdapter by inheriting
|
You can customize the SocialAccountAdapter by inheriting
|
||||||
``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to
|
``allauth_ens.adapter.LongTermClipperAccountAdapter``. You might want to
|
||||||
modify ``get_username(clipper, data)`` to change the default username format.
|
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
|
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
|
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
|
``get_username`` (as done in the example website) allows to get rid of that
|
||||||
|
|
|
@ -25,8 +25,8 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter):
|
||||||
If a clipper connection has already existed with the uid, it checks
|
If a clipper connection has already existed with the uid, it checks
|
||||||
that this connection still belongs to the user it was associated with.
|
that this connection still belongs to the user it was associated with.
|
||||||
|
|
||||||
This check is performed by comparing the generated username corresponding
|
This check is performed by comparing the entrance years provided by the
|
||||||
to this connection with the old one.
|
LDAP.
|
||||||
|
|
||||||
If the check succeeds, it simply reactivates the clipper connection as
|
If the check succeeds, it simply reactivates the clipper connection as
|
||||||
belonging to the associated user.
|
belonging to the associated user.
|
||||||
|
@ -52,8 +52,13 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter):
|
||||||
ldap_data = get_ldap_infos(clipper_uid)
|
ldap_data = get_ldap_infos(clipper_uid)
|
||||||
sociallogin._ldap_data = ldap_data
|
sociallogin._ldap_data = ldap_data
|
||||||
|
|
||||||
if old_conn.user.username != self.get_username(clipper_uid, ldap_data):
|
if ldap_data is None or 'entrance_year' not in ldap_data:
|
||||||
# The admission year is different
|
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
|
# We cannot reuse this SocialAccount, so we need to invalidate
|
||||||
# the email address of the previous user to prevent conflicts
|
# the email address of the previous user to prevent conflicts
|
||||||
# if a new SocialAccount is created
|
# if a new SocialAccount is created
|
||||||
|
@ -72,8 +77,7 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter):
|
||||||
|
|
||||||
def get_username(self, clipper_uid, data):
|
def get_username(self, clipper_uid, data):
|
||||||
"""
|
"""
|
||||||
Util function to generate a unique username, by default 'clipper@promo'
|
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 'entrance_year' not in data:
|
if data is None or 'entrance_year' not in data:
|
||||||
raise ValueError("No entrance year in LDAP data")
|
raise ValueError("No entrance year in LDAP data")
|
||||||
|
@ -114,7 +118,7 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter):
|
||||||
get_account_adapter().populate_username(request, user)
|
get_account_adapter().populate_username(request, user)
|
||||||
|
|
||||||
# Save extra data (only once)
|
# 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.save(request)
|
||||||
sociallogin.account.save()
|
sociallogin.account.save()
|
||||||
|
|
||||||
|
@ -175,15 +179,22 @@ def install_longterm_adapter(fake=False):
|
||||||
else:
|
else:
|
||||||
user.save()
|
user.save()
|
||||||
cases.append(user.username)
|
cases.append(user.username)
|
||||||
if SocialAccount.objects.filter(provider='clipper',
|
|
||||||
uid=clipper_uid).exists():
|
try:
|
||||||
logs["updated"].append((clipper_uid, user.username))
|
sa = SocialAccount.objects.get(provider='clipper', uid=clipper_uid)
|
||||||
continue
|
if not sa.extra_data.get('ldap'):
|
||||||
sa = SocialAccount(user=user, provider='clipper',
|
sa.extra_data['ldap'] = data
|
||||||
uid=clipper_uid, extra_data=data)
|
if not fake:
|
||||||
if not fake:
|
sa.save(update_fields=['extra_data'])
|
||||||
sa.save()
|
logs["updated"].append((clipper_uid, user.username))
|
||||||
logs["created"].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)\
|
logs["unmodified"] = User.objects.exclude(username__in=cases)\
|
||||||
.values_list("username", flat=True)
|
.values_list("username", flat=True)
|
||||||
|
|
|
@ -37,8 +37,23 @@ class ClipperProvider(CASProvider):
|
||||||
]
|
]
|
||||||
|
|
||||||
def extract_extra_data(self, data):
|
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 = super(ClipperProvider, self).extract_extra_data(data)
|
||||||
extra_data['email'] = self.extract_email(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
|
return extra_data
|
||||||
|
|
||||||
def message_suggest_caslogout_on_logout(self, request):
|
def message_suggest_caslogout_on_logout(self, request):
|
||||||
|
|
|
@ -17,6 +17,18 @@ class ClipperProviderTests(CASTestCase):
|
||||||
u = User.objects.get(username='clipper_uid')
|
u = User.objects.get(username='clipper_uid')
|
||||||
self.assertEqual(u.email, 'clipper_uid@clipper.ens.fr')
|
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):
|
class ClipperViewsTests(CASViewTestCase):
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import django
|
import django
|
||||||
|
@ -8,6 +10,7 @@ from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from allauth.socialaccount.models import SocialAccount
|
from allauth.socialaccount.models import SocialAccount
|
||||||
|
|
||||||
|
import six
|
||||||
from allauth_cas.test.testcases import CASTestCase
|
from allauth_cas.test.testcases import CASTestCase
|
||||||
from fakeldap import MockLDAP
|
from fakeldap import MockLDAP
|
||||||
from mock import patch
|
from mock import patch
|
||||||
|
@ -161,8 +164,12 @@ class LongTermClipperTests(CASTestCase):
|
||||||
_mock_ldap.reset()
|
_mock_ldap.reset()
|
||||||
|
|
||||||
def _setup_ldap(self, promo=12, username="test"):
|
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'] = {
|
_mock_ldap.directory['dc=spi,dc=ens,dc=fr'] = {
|
||||||
'uid': [username],
|
'uid': [buid],
|
||||||
'cn': [b'John Smith'],
|
'cn': [b'John Smith'],
|
||||||
'mailRoutingAddress': [b'test@clipper.ens.fr'],
|
'mailRoutingAddress': [b'test@clipper.ens.fr'],
|
||||||
'homeDirectory': [b'/users/%d/phy/test/' % promo],
|
'homeDirectory': [b'/users/%d/phy/test/' % promo],
|
||||||
|
@ -188,6 +195,7 @@ class LongTermClipperTests(CASTestCase):
|
||||||
|
|
||||||
sa = list(SocialAccount.objects.all())[-1]
|
sa = list(SocialAccount.objects.all())[-1]
|
||||||
self.assertEqual(sa.user.id, u.id)
|
self.assertEqual(sa.user.id, u.id)
|
||||||
|
self.assertEqual(sa.extra_data['ldap']['entrance_year'], '12')
|
||||||
|
|
||||||
def test_connect_disconnect(self):
|
def test_connect_disconnect(self):
|
||||||
self._setup_ldap()
|
self._setup_ldap()
|
||||||
|
@ -312,9 +320,11 @@ class LongTermClipperTests(CASTestCase):
|
||||||
username='test')
|
username='test')
|
||||||
user1 = r.context["user"]
|
user1 = r.context["user"]
|
||||||
nsa1 = SocialAccount.objects.count()
|
nsa1 = SocialAccount.objects.count()
|
||||||
|
conn = user1.socialaccount_set.get(provider='clipper')
|
||||||
self.assertEqual(user1.id, user0.id)
|
self.assertEqual(user1.id, user0.id)
|
||||||
self.assertEqual(nsa1, nsa0)
|
self.assertEqual(nsa1, nsa0)
|
||||||
self.assertEqual(user1.username, "test@12")
|
self.assertEqual(user1.username, "test@12")
|
||||||
|
self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12')
|
||||||
|
|
||||||
def test_longterm_installer_from_djangocas(self):
|
def test_longterm_installer_from_djangocas(self):
|
||||||
with self.settings(SOCIALACCOUNT_ADAPTER=
|
with self.settings(SOCIALACCOUNT_ADAPTER=
|
||||||
|
@ -332,9 +342,11 @@ class LongTermClipperTests(CASTestCase):
|
||||||
username='test')
|
username='test')
|
||||||
user1 = r.context["user"]
|
user1 = r.context["user"]
|
||||||
nsa1 = SocialAccount.objects.count()
|
nsa1 = SocialAccount.objects.count()
|
||||||
|
conn = user1.socialaccount_set.get(provider='clipper')
|
||||||
self.assertEqual(user1.id, user0.id)
|
self.assertEqual(user1.id, user0.id)
|
||||||
self.assertEqual(nsa1, nsa0 + 1)
|
self.assertEqual(nsa1, nsa0 + 1)
|
||||||
self.assertEqual(user1.username, "test@12")
|
self.assertEqual(user1.username, "test@12")
|
||||||
|
self.assertEqual(conn.extra_data['ldap']['entrance_year'], '12')
|
||||||
|
|
||||||
def test_disconnect_ldap(self):
|
def test_disconnect_ldap(self):
|
||||||
nu0 = User.objects.count()
|
nu0 = User.objects.count()
|
||||||
|
|
|
@ -7,7 +7,6 @@ combine_as_imports = True
|
||||||
default_section = THIRDPARTY
|
default_section = THIRDPARTY
|
||||||
include_trailing_comma = True
|
include_trailing_comma = True
|
||||||
known_allauth = allauth
|
known_allauth = allauth
|
||||||
known_future_library = future,six
|
|
||||||
known_django = django
|
known_django = django
|
||||||
known_first_party = allauth_ens
|
known_first_party = allauth_ens
|
||||||
multi_line_output = 5
|
multi_line_output = 5
|
||||||
|
|
Loading…
Reference in a new issue