Util management command to install longtermadapter + fixes

This commit is contained in:
Evarin 2018-04-24 00:19:42 +02:00
parent b6f5acaa46
commit f0a73f6ef6
4 changed files with 163 additions and 30 deletions

View file

@ -178,9 +178,9 @@ Configuration
Auto-signup Auto-signup
Populated data Populated data
- username: ``<clipper>@<entrance year>`` - username: ``<clipper>@<entrance year>``
- email: from LDAP's `mailRoutingAddress` field, or ``<clipper>@clipper.ens.fr`` - email: from LDAP's *mailRoutingAddress* field, or ``<clipper>@clipper.ens.fr``
- first_name, last_name from LDAP's `cn` field - 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) - 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 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 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.
@ -188,6 +188,9 @@ Account deprecation
Customize 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. 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 Demo Site
********* *********

View file

@ -3,9 +3,12 @@ import ldap
from allauth.account.utils import user_email, user_field, user_username from allauth.account.utils import user_email, user_field, user_username
from allauth.account.models import EmailAddress 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 allauth.socialaccount.models import SocialAccount
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model
User = get_user_model()
import six import six
@ -36,6 +39,30 @@ def _init_ldap():
l.set_option(ldap.OPT_TIMEOUT, 10) l.set_option(ldap.OPT_TIMEOUT, 10)
return l 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): def get_ldap_infos(clipper):
assert clipper.isalnum() assert clipper.isalnum()
data = {'email':'{}@clipper.ens.fr'.format(clipper.strip().lower())} data = {'email':'{}@clipper.ens.fr'.format(clipper.strip().lower())}
@ -50,29 +77,7 @@ def get_ldap_infos(clipper):
str("homeDirectory") ]) str("homeDirectory") ])
if len(info) > 0: if len(info) > 0:
infos = info[0][1] data = _extract_infos_from_ldap(info[0][1], 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]
except ldap.LDAPError: except ldap.LDAPError:
pass pass
@ -87,6 +92,9 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter):
""" """
def pre_social_login(self, request, sociallogin): 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 clipper = sociallogin.account.uid
try: try:
a = SocialAccount.objects.get(provider='clipper_inactive', a = SocialAccount.objects.get(provider='clipper_inactive',
@ -137,6 +145,8 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter):
return "{}@{}".format(clipper, data.get('annee', '00')) return "{}@{}".format(clipper, data.get('annee', '00'))
def save_user(self, request, sociallogin, form=None): 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 = sociallogin.user
user.set_unusable_password() user.set_unusable_password()
@ -164,6 +174,11 @@ class LongTermClipperAccountAdapter(DefaultSocialAccountAdapter):
return user return user
def deprecate_clippers(): def deprecate_clippers():
"""
Marks all the SocialAccount with clipper as deprecated, by setting their
provider to 'clipper_inactive'
"""
clippers = SocialAccount.objects.filter(provider='clipper') clippers = SocialAccount.objects.filter(provider='clipper')
c_uids = clippers.values_list('uid', flat=True) c_uids = clippers.values_list('uid', flat=True)
@ -172,3 +187,49 @@ def deprecate_clippers():
# Deprecate accounts # Deprecate accounts
clippers.update(provider='clipper_inactive') 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

View file

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

View file

@ -10,7 +10,7 @@ from mock import patch
from fakeldap import MockLDAP from fakeldap import MockLDAP
from allauth_cas.test.testcases import CASTestCase, CASViewTestCase 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 from allauth.socialaccount.models import SocialAccount
_mock_ldap = MockLDAP() _mock_ldap = MockLDAP()
@ -250,10 +250,11 @@ class LongTermClipperTests(CASTestCase):
def test_multiple_deprecation(self): def test_multiple_deprecation(self):
self._setup_ldap(12) self._setup_ldap(12)
self._setup_ldap(15, "truc")
r = self.client_cas_login(self.client, provider_id="clipper", r = self.client_cas_login(self.client, provider_id="clipper",
username="test") username="test")
self.client.logout() self.client.logout()
self._setup_ldap(15, "truc")
r = self.client_cas_login(self.client, provider_id="clipper", r = self.client_cas_login(self.client, provider_id="clipper",
username="truc") username="truc")
self.client.logout() self.client.logout()
@ -275,3 +276,44 @@ class LongTermClipperTests(CASTestCase):
# while "truc" remains # while "truc" remains
self.assertEqual(sa0, sa2) 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'):
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")