Merge branch 'master' of ssh://git.crans.org/git/ldap into cerveaulent
This commit is contained in:
commit
2073f5055b
14 changed files with 887 additions and 42 deletions
147
lc_ldap.py
147
lc_ldap.py
|
@ -3,9 +3,13 @@
|
|||
#
|
||||
# LC_LDAP.PY-- LightWeight CransLdap
|
||||
#
|
||||
# Copyright (C) 2010 Cr@ns <roots@crans.org>
|
||||
# Author: Antoine Durand-Gasselin <adg@crans.org>
|
||||
# All rights reserved.
|
||||
# Copyright (C) 2010-2013 Cr@ns <roots@crans.org>
|
||||
# Authors: Antoine Durand-Gasselin <adg@crans.org>
|
||||
# Nicolas Dandrimont <olasd@crans.org>
|
||||
# Olivier Iffrig <iffrig@crans.org>
|
||||
# Valentin Samir <samir@crans.org>
|
||||
# Daniel Stan <dstan@crans.org>
|
||||
# Vincent Le Gallic <legallic@crans.org>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
|
@ -32,6 +36,7 @@
|
|||
|
||||
from __future__ import with_statement
|
||||
import os, sys, ldap, re, netaddr, datetime, copy, time, random
|
||||
import ldap.filter
|
||||
from ldap.modlist import addModlist, modifyModlist
|
||||
try:
|
||||
from Levenshtein import jaro
|
||||
|
@ -43,13 +48,30 @@ import config, crans_utils
|
|||
from attributs import attrify, blacklist
|
||||
from ldap_locks import CransLock
|
||||
|
||||
uri = 'ldapi:///' #'ldap://ldap.adm.crans.org/'
|
||||
uri = 'ldap://ldap.adm.crans.org/'
|
||||
base_dn = 'ou=data,dc=crans,dc=org'
|
||||
log_dn = "cn=log"
|
||||
|
||||
# Pour enregistrer dans l'historique, on a besoin de savoir qui exécute le script
|
||||
# Si le script a été exécuté via un sudo, la variable SUDO_USER (l'utilisateur qui a effectué le sudo)
|
||||
# est plus pertinente que USER (qui sera root)
|
||||
current_user = os.getenv("SUDO_USER") or os.getenv("USER")
|
||||
|
||||
# Quand on a besoin du fichier de secrets
|
||||
def import_secrets():
|
||||
if not "/etc/crans/secrets/" in sys.path:
|
||||
sys.path.append("/etc/crans/secrets/")
|
||||
import secrets
|
||||
return secrets
|
||||
|
||||
# Champs à ignorer dans l'historique
|
||||
HIST_IGNORE_FIELDS = ["modifiersName", "entryCSN", "modifyTimestamp", "historique"]
|
||||
|
||||
def escape(chaine):
|
||||
"""Renvoie une chaîne échapée pour pouvoir la mettre en toute sécurité
|
||||
dans une requête ldap."""
|
||||
return ldap.filter.escape_filter_chars(chaine)
|
||||
|
||||
def ldif_to_uldif(ldif):
|
||||
uldif = {}
|
||||
for attr, vals in ldif.items():
|
||||
|
@ -76,28 +98,42 @@ def cldif_to_ldif(cldif):
|
|||
|
||||
def lc_ldap_test():
|
||||
"""Binding LDAP à la base de tests"""
|
||||
return lc_ldap(dn='cn=admin,dc=crans,dc=org', cred='75bdb64f32')
|
||||
return lc_ldap(uri='ldap://vo.adm.crans.org',dn='cn=admin,dc=crans,dc=org', cred='75bdb64f32')
|
||||
|
||||
def lc_ldap_admin():
|
||||
"""Binding LDAP à la vraie base, en admin.
|
||||
Possible seulement si on peut lire secrets.py"""
|
||||
secrets = import_secrets()
|
||||
return lc_ldap(uri='ldap://ldap.adm.crans.org/', dn=secrets.ldap_auth_dn, cred=secrets.ldap_password)
|
||||
|
||||
class lc_ldap(ldap.ldapobject.LDAPObject):
|
||||
"""Connexion à la base ldap crans, chaque instance représente une connexion
|
||||
"""
|
||||
def __init__(self, dn=None, user=None, cred=None, uri=uri):
|
||||
def __init__(self, dn=None, user=None, cred=None, uri=uri, test=False):
|
||||
"""Initialise la connexion ldap,
|
||||
- En authentifiant avec dn et cred s'ils sont précisés
|
||||
- Si dn n'est pas précisé, mais que user est précisé, récupère
|
||||
le dn associé à l'uid user, et effectue l'authentification
|
||||
avec ce dn et cred
|
||||
- Sinon effectue une authentification anonyme
|
||||
Si test est à True, on se connecte à la base de test sur vo.
|
||||
"""
|
||||
|
||||
if test:
|
||||
uri = "ldapi:///"
|
||||
ldap.ldapobject.LDAPObject.__init__(self, uri)
|
||||
|
||||
if user and not re.match('[a-z_][a-z0-9_-]*', user):
|
||||
raise ValueError('Invalid user name: %s' % user)
|
||||
raise ValueError('Invalid user name: %r' % user)
|
||||
|
||||
# Si un username, on récupère le dn associé
|
||||
# Si un username, on récupère le dn associé…
|
||||
if user and not dn:
|
||||
self.simple_bind_s(base_dn)
|
||||
if test:
|
||||
# …en anonyme si on se connecte à la base de test
|
||||
self.simple_bind_s(base_dn)
|
||||
else:
|
||||
# …sinon, en se connectant en readonly (on récupère les identifiants dans secrets.py)
|
||||
secrets = import_secrets()
|
||||
self.simple_bind_s(who=secrets.ldap_readonly_auth_dn, cred=secrets.ldap_readonly_password)
|
||||
res = self.search_s(base_dn, 1, 'uid=%s' % user)
|
||||
if len(res) < 1:
|
||||
raise ldap.INVALID_CREDENTIALS({'desc': 'No such user: %s' % user })
|
||||
|
@ -120,8 +156,11 @@ class lc_ldap(ldap.ldapobject.LDAPObject):
|
|||
def search(self, filterstr='(objectClass=*)', mode='ro', dn= base_dn, scope= 2, sizelimit=400):
|
||||
"""La fonction de recherche dans la base ldap qui renvoie un liste de
|
||||
CransLdapObjects. On utilise la feature de sizelimit de python ldap"""
|
||||
res = self.search_ext_s(dn, scope, filterstr, sizelimit=sizelimit)
|
||||
return [ new_cransldapobject(self, r[0], mode=mode) for r in res ]
|
||||
ldap_res = self.search_ext_s(dn, scope, filterstr, sizelimit=sizelimit)
|
||||
ret = []
|
||||
for dn, ldif in ldap_res:
|
||||
ret.append(new_cransldapobject(self, dn, mode=mode))
|
||||
return ret
|
||||
|
||||
def allMachinesAdherents(self):
|
||||
"""Renvoie la liste de toutes les machines et de tous les adherents
|
||||
|
@ -192,7 +231,7 @@ class lc_ldap(ldap.ldapobject.LDAPObject):
|
|||
assert isinstance(owner, adherent) or isinstance(owner, club)
|
||||
# XXX - Vérifier les droits
|
||||
|
||||
else: raise ValueError("Realm inconnu: %s" % realm)
|
||||
else: raise ValueError("Realm inconnu: %r" % realm)
|
||||
|
||||
# On récupère la plage des mids
|
||||
if realm == 'fil':
|
||||
|
@ -327,6 +366,7 @@ class CransLdapObject(object):
|
|||
self.attrs = ldif_to_cldif(self.attrs, conn, check_ctxt = False)
|
||||
if mode in ['w', 'rw']:
|
||||
### Vérification que `λv. str(Attr(v))` est bien une projection
|
||||
### C'est-à-dire que si on str(Attr(str(Attr(v)))) on retombe sur str(Attr(v))
|
||||
oldif = res[0][1]
|
||||
nldif = cldif_to_ldif(self.attrs)
|
||||
|
||||
|
@ -336,10 +376,27 @@ class CransLdapObject(object):
|
|||
if v in vals:
|
||||
vals.remove(v)
|
||||
nvals = [nldif[attr][v.index(v)] for v in vals ]
|
||||
raise EnvironmentError("λv. str(Attr(v)) n'est peut-être pas une projection:", attr, nvals, vals)
|
||||
raise EnvironmentError("λv. str(Attr(v)) n'est peut-être pas une projection (ie non idempotente):", attr, nvals, vals)
|
||||
|
||||
self._modifs = ldif_to_cldif(ldif_to_uldif(res[0][1]), conn, check_ctxt = False)
|
||||
|
||||
def _get_fields(self):
|
||||
"""Renvoie la liste des champs LDAP de l'objet"""
|
||||
return self.ofields + self.xfields + self.ufields + self.mfields
|
||||
fields = property(_get_fields)
|
||||
|
||||
def history_add(self, login, chain):
|
||||
"""Ajoute une ligne à l'historique de l'objet.
|
||||
###ATTENTION : C'est un kludge pour pouvoir continuer à faire "comme avant",
|
||||
### mais on devrait tout recoder pour utiliser l'historique LDAP"""
|
||||
assert isinstance(login, str) or isinstance(login, unicode)
|
||||
assert isinstance(chain, unicode)
|
||||
|
||||
new_line = u"%s, %s : %s" % (time.strftime("%d/%m/%Y %H:%M"), login, chain)
|
||||
# Attention, le __setitem__ est surchargé, mais pas .append sur l'historique
|
||||
self["historique"] = self["historique"] + [new_line]
|
||||
|
||||
|
||||
def save(self):
|
||||
"""Sauvegarde dans la base les modifications apportées à l'objet.
|
||||
Interne: Vérifie que self._modifs contient des valeurs correctes et
|
||||
|
@ -445,8 +502,18 @@ class CransLdapObject(object):
|
|||
- Proposer de filtrer les blacklistes avec un arg supplémentaire ?
|
||||
- Vérifier les blacklistes des machines pour les adhérents ?
|
||||
"""
|
||||
blacklist_liste=[]
|
||||
# blacklistes virtuelle si on est un adhérent pour carte étudiant et chambre invalides
|
||||
if self.__class__.__name__ == "adherent" and self.paiement_ok():
|
||||
if not config.periode_transitoire and config.bl_carte_et_actif and not self.carte_ok() and not self.sursis_carte():
|
||||
bl = blacklist(u'%s$%s$%s$%s' % ('-', '-', 'carte_etudiant', ''), {}, self.conn, False)
|
||||
blacklist_liste.append(bl)
|
||||
if self['chbre'][0].value == '????':
|
||||
bl = blacklist(u'%s$%s$%s$%s' % ('-', '-', 'chambre_invalide', ''), {}, self.conn, False)
|
||||
blacklist_liste.append(bl)
|
||||
attrs = (self.attrs if self.mode not in ["w", "rw"] else self._modifs)
|
||||
return filter((lambda bl: bl.is_actif()), attrs.get("blacklist",[]))
|
||||
blacklist_liste.extend(filter((lambda bl: bl.is_actif()), attrs.get("blacklist",[])))
|
||||
return blacklist_liste
|
||||
|
||||
def blacklist(self, sanction, commentaire, debut="now", fin = '-'):
|
||||
u"""
|
||||
|
@ -477,6 +544,13 @@ class proprio(CransLdapObject):
|
|||
super(proprio, self).__init__(conn, dn, mode, ldif)
|
||||
self._machines = machines
|
||||
|
||||
def sursis_carte(self):
|
||||
for h in self['historique'][::-1]:
|
||||
x=re.match("(.*),.* : .*(paiement\+%s|inscription).*" % config.ann_scol,h.value)
|
||||
if x != None:
|
||||
return ((time.time()-time.mktime(time.strptime(x.group(1),'%d/%m/%Y %H:%M')))<=config.sursis_carte)
|
||||
return False
|
||||
|
||||
def paiement_ok(self):
|
||||
u"""Renvoie si le propriétaire a payé pour l'année en cours"""
|
||||
if self.dn == base_dn:
|
||||
|
@ -486,8 +560,15 @@ class proprio(CransLdapObject):
|
|||
for paiement in self['paiement']:
|
||||
if paiement.value == config.ann_scol:
|
||||
bool_paiement = True
|
||||
break
|
||||
# Pour la période transitoire année précédente ok
|
||||
if config.periode_transitoire and paiement.value == config.ann_scol -1:
|
||||
bool_paiement = True
|
||||
break
|
||||
except KeyError:
|
||||
pass
|
||||
# Doit-on bloquer en cas de manque de la carte d'etudiant ?
|
||||
# (si période transitoire on ne bloque dans aucun cas)
|
||||
if config.bl_carte_et_definitif and not 'club' in map(lambda x:x.value,self["objectClass"]):
|
||||
bool_carte = False
|
||||
try:
|
||||
|
@ -496,10 +577,42 @@ class proprio(CransLdapObject):
|
|||
bool_carte = True
|
||||
except KeyError:
|
||||
pass
|
||||
# Si inscrit depuis moins de config.sursis_carte, on laisse un sursis
|
||||
if not bool_carte and self.sursis_carte():
|
||||
bool_carte = True
|
||||
return bool_carte and bool_paiement
|
||||
return bool_paiement
|
||||
|
||||
|
||||
|
||||
def carte_ok(self):
|
||||
u"""Renvoie si le propriétaire a donné sa carte pour l'année en cours"""
|
||||
if not self.dn == base_dn and config.bl_carte_et_actif and not 'club' in map(lambda x:x.value,self["objectClass"]):
|
||||
bool_carte = False
|
||||
try:
|
||||
for carte in self['carteEtudiant']:
|
||||
if carte.value == config.ann_scol:
|
||||
bool_carte = True
|
||||
except KeyError:
|
||||
pass
|
||||
return bool_carte
|
||||
return True
|
||||
|
||||
def update_solde(self, diff, comment=u"", login=None):
|
||||
"""Modifie le solde du proprio. diff peut être négatif ou positif."""
|
||||
login = login or current_user
|
||||
assert isinstance(diff, int) or isinstance(diff, float)
|
||||
assert isinstance(comment, unicode)
|
||||
|
||||
solde = float(self["solde"][0].value)
|
||||
new_solde = solde + diff
|
||||
|
||||
# On vérifie qu'on ne dépasse par le découvert autorisé
|
||||
if new_solde < config.impression.decouvert:
|
||||
raise ValueError(u"Solde minimal atteint, opération non effectuée.")
|
||||
|
||||
transaction = u"credit" if diff >=0 else u"debit"
|
||||
new_solde = u"%.2f" % new_solde
|
||||
self.history_add(login, u"%s %.2f Euros [%s]" % (transaction, abs(diff), comment))
|
||||
self["solde"] = new_solde
|
||||
|
||||
def machines(self):
|
||||
"""Renvoie la liste des machines"""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue