annuaire-eleves/fiches/management/commands/_ldap.py
2020-11-21 17:45:55 +01:00

146 lines
4.7 KiB
Python

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/<promo>/<dept>/<login>`.
"""
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