from collections import namedtuple import ldap import ldap.filter from django.conf import settings ClipperAccount = namedtuple("ClipperAccount", ["uid", "name", "email", "year", "dept"]) class LDAP: """Classe pour se connecter à un LDAP ENS (CRI ou SPI). Inspirée de `merle_scripts`. """ # À renseigner LDAP_SERVER = { "PROTOCOL": "ldaps", "URL": "ldap.example.com", "PORT": 636, } search_base = "" attr_list = [] def __init__(self): """ Initialize the LDAP object """ self.ldap_obj = ldap.initialize( "{PROTOCOL}://{URL}:{PORT}".format(**self.LDAP_SERVER) ) def search(self, filters="(objectClass=*)"): """Do a ldap.search_s with the given filters, as specified for ldap.search_s""" return self.ldap_obj.search_s( self.search_base, ldap.SCOPE_SUBTREE, filters, self.attr_list ) @staticmethod def extract_ldap_info(entry, field): """ Extract the given field from an LDAP entry as an UTF-8 string """ return entry[1].get(field, [b""])[0].decode("utf-8") class ClipperLDAP(LDAP): LDAP_SERVER = settings.LDAP["SPI"] search_base = "dc=spi,dc=ens,dc=fr" attr_list = ["cn", "uid", "mail", "homeDirectory"] verbose_depts = { "bio": "Biologie", "chimie": "Chimie", "dec": "Études Cognitives", "geol": "Géosciences", "guests": "Invité·e", "info": "Informatique", "litt": "Lettres", "maths": "Mathématiques", "pei": "PEI", "phy": "Physique", } def parse_dept(self, home_dir): """Extrait le département d'entrée d'un·e élève à partir de son dossier. Le dossier a le format `/users///`. """ users, promo, dept, login = home_dir.split("/")[1:] if users != "users": raise ValueError("Invalid home directory") # Ça casse en 2100, mais le système de naming de sas aussi... promo = 2000 + int(promo) return promo, self.verbose_depts.get(dept, None) def get_clipper_list(self, promo_filter=None, verbosity=1, stdout=None): """Extrait la liste des comptes clipper présents dans le LDAP. Renvoie une liste de `namedTuple` contenant leur nom, prénom, adresse mail et département/année d'entrée. Si `promo_filter != None`, ne renvoie que les clippers d'une promotion donnée. """ search_res = self.search() clipper_list = [] for entry in search_res: uid = self.extract_ldap_info(entry, "uid") # Il y a des comptes "bizarre" (e.g. `root`) avec uid vide if len(uid) > 0: try: name = self.extract_ldap_info(entry, "cn") email = self.extract_ldap_info(entry, "mail") promo, dept = self.parse_dept( self.extract_ldap_info(entry, "homeDirectory") ) if promo_filter is None or promo == promo_filter: clipper_list.append( ClipperAccount(uid, name, email, promo, dept) ) if verbosity == 3: stdout.write("Compte clipper trouvé : {}".format(uid)) except ValueError: if verbosity >= 2: stdout.write("Entrée malformée trouvée : {}".format(entry)) pass return clipper_list class AnnuaireLDAP(LDAP): LDAP_SERVER = settings.LDAP["CRI"] search_base = "ou=people,dc=ens,dc=fr" attr_list = ["uid", "sn", "givenName", "ou"] def try_match(self, profile, verbosity=1, stderr=None): """Essaie de trouver une entrée correspondant au profile donné dans l'annuaire de l'ENS. L'heuristique est la suivante : il est très probable que le prénom de la personne commence par le premier mot de `profile.full_name`, et que son nom de famille finisse par le dernier mot. """ given_name = profile.full_name.split(" ")[0] last_name = profile.full_name.split(" ")[-1] search_name = self.search( "(&(givenName={}*)(sn=*{}))".format(given_name, last_name) ) if len(search_name) > 0: if len(search_name) > 2: if verbosity >= 2: stderr.write( "Erreur : deux logins CRI trouvés pour {} ({})".format( profile.user.username, profile.promotion ) ) return None return self.extract_ldap_info(search_name[0], "uid") return None