# -*- coding: utf-8 -*- from django.contrib.auth import get_user_model 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 ldap from .utils import extract_infos_from_ldap, get_ldap_infos, get_clipper_email, remove_email, init_ldap User = get_user_model() 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): """ 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_uid = sociallogin.account.uid try: 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_uid) sociallogin._ldap_data = ldap_data if old_conn.user.username != self.get_username(clipper_uid, ldap_data): # The admission year is different # 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 old_conn.provider = 'clipper' old_conn.save() # Redo the thing that had failed just before sociallogin.lookup() 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 'entrance_year' not in data: raise ValueError("No entrance year in LDAP data") return "{}@{}".format(clipper_uid, data['entrance_year']) 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() clipper_uid = sociallogin.account.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)) 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) # 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(): """ 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() # 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()))), ['uid', 'cn', 'mailRoutingAddress', 'homeDirectory']) logs = {"created": [], "updated": []} cases = [] for userinfo in info: infos = userinfo[1] 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_uid, data) if fake: cases.append(clipper_uid) 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)) logs["unmodified"] = User.objects.exclude(username__in=cases)\ .values_list("username", flat=True) return logs