lc_ldap/lc_ldap.py
2010-10-16 15:23:25 +02:00

556 lines
21 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# LC_LDAP.PY-- LightWeight CransLdap
#
# Copyright (C) 2010 Cr@ns <roots@crans.org>
# Author: Antoine Durand-Gasselin <adg@crans.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Cr@ns nor the names of its contributors may
# be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT
# HOLDER> BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import with_statement
import os, sys, ldap, re, netaddr, datetime, copy, time
from ldap.modlist import addModlist, modifyModlist
sys.path.append('/usr/scripts/gestion')
import config, crans_utils
from attributs import attrify
from ldap_locks import CransLock
uri = 'ldapi:///' #'ldap://ldap.adm.crans.org/'
base_dn = 'ou=data,dc=crans,dc=org'
def is_actif(sanction):
"""Retourne True ou False suivant si la sanction fournie (chaîne
venant de blacklist) est active ou non
"""
bl = sanction.split('$')
now = time.time()
debut = int(bl[0])
if bl[1] == '-':
fin = now + 1
else:
fin = int(bl[1])
return debut < now and fin > now
def uldif_to_ldif(uldif):
## XXX - Kill
"""Prend en argument un dico ldif, et vérifie que toutes les
valeurs sont bien des unicodes, les converti alors en chaînes
utf-8, renvoie un ldif"""
ldif = {}
for attr, vals in uldif.items():
ldif[attr] = [ unicode.encode(val, 'utf-8') for val in vals ]
return ldif
def ldif_to_uldif(ldif):
## XXX - Kill
'Prend en argument un dico ldif, et décode toutes les chaînes en utf-8'
uldif = {}
for attr, vals in ldif.items():
uldif[attr] = [ unicode(val, 'utf-8') for val in vals ]
return uldif
def ldif_to_cldif(ldif):
cldif = {}
for attr, vals in ldif.items():
cldif[attr] = [ attrify(val, attr, ldif) for val in vals]
return cldif
def cldif_to_ldif(cldif):
ldif = {}
for attr, vals in cldif.items():
ldif[attr] = [ str(attr) for attr in vals ]
return ldif
def lc_ldap_test():
return lc_ldap(dn='cn=admin,dc=crans,dc=org', cred='75bdb64f32')
class lc_ldap(ldap.ldapobject.LDAPObject):
def __init__(self, dn=None, user=None, cred=None, uri=uri):
"""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
"""
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)
if user and not dn:
self.simple_bind_s(base_dn)
res = self.search_s('uid=%s' % user)
if len(res) < 1:
raise ldap.INVALID_CREDENTIALS({'desc': 'No such user: %s' % user })
elif len(res) > 1:
raise ldap.INVALID_CREDENTIALS({'desc': 'Too many matches: uid=%s' % user })
else:
dn = res[0][0]
if dn:
self.conn = self.bind_s(dn, cred)
else:
self.conn = self.simple_bind_s()
def search(self, filterstr, mode='ro', dn= base_dn, scope= 2, sizelimit=400):
res = self.search_ext_s(dn, scope, filterstr, sizelimit=sizelimit)
return [ CransLdapObject(self, r[0], mode=mode) for r in res ]
def allMachines(self):
"""Renvoie la liste de toutes les machines,
Conçue pour s'éxécuter le plus rapidement possible"""
res = {}
machines = []
for dn, attrs in self.search_s(base_dn, scope=2):
res[dn] = attrs
for dn, attrs in res.items():
if dn.startswith('mid='):
m = CransLdapObject(self, dn, ldif = attrs)
parent_dn = dn.split(',', 1)[1]
m._proprio = CransLdapObject(self, parent_dn, res[parent_dn])
machines.append(m)
return machines
def newMachine(self, parent, realm, uldif):
"""Crée une nouvelle machine: realm peut être:
fil, fil-v6, wifi, wifi-v6, adm, gratuit, personnel-ens, special"""
#adm, serveurs, bornes, wifi, adherents, gratuit ou personnel-ens"""
owner = self.search('objectClass=*', dn=parent, scope=0)[0]
if realm in ["adm", "serveurs"]:
uldif['objectClass'] = [u'machineCrans']
assert isinstance(owner, AssociationCrans)
# XXX - Vérifier les droits
elif realm == "bornes":
uldif['objectClass'] = [u'borneWifi']
assert isinstance(owner, AssociationCrans)
# XXX - Vérifier les droits
elif realm in ["wifi", "wifi-v6"]:
uldif['objectClass'] = [u'machineWifi']
assert isinstance(owner, adherent) or isinstance(owner, club)
# XXX - Vérifier les droits (owner.type_connexion)
elif realm in ["fil", "fil-v6", "gratuit", "personnel-ens"]:
uldif['objectClass'] = [u'machineFixe']
assert isinstance(owner, adherent) or isinstance(owner, club)
# XXX - Vérifier les droits
else: raise ValueError("Realm inconnu: %s" % realm)
plage = xrange( *(config.mid[realm]))
mid = uldif.setdefault('mid', [ unicode(self._find_id('mid', plage)) ])
uldif['ipHostNumber'] = [ unicode(crans_utils.ip_of_mid(int (mid[0]))) ]
return self._create_entity('mid=%s,%s' % (mid[0], parent), uldif)
def newAdherent(self, uldif):
"""Crée un nouvel adhérent"""
aid = uldif.setdefault('aid', [ unicode(self._find_id('aid')) ])
uldif['objectClass'] = [u'adherent']
return self._create_entity('aid=%s,%s' % (aid[0], base_dn), uldif)
def newClub(self, uldif):
"""Crée un nouveau club"""
raise NotImplementedError()
def newFacture(self, uldif):
"""Crée une nouvelle facture"""
raise NotImplementedError()
def _create_entity(self, dn, ldif):
'''Crée une nouvelle entité ldap en dn, avec attributs ldif:
uniquement en unicode'''
cldif = ldif_to_cldif(ldif)
#lock = CransLock(self)
for item in ['aid', 'uid', 'chbre', 'mailAlias', 'canonicalAlias',
'fid', 'cid', 'mid', 'macAddress', 'host', 'hostAlias' ]:
for val in cldif.get(item, []):
pass #lock.add(item, val)
#uldif['historique'] = [ self._hist('Création')]
ldif = cldif_to_ldif(cldif)
modlist = addModlist(ldif)
#with lock:
# print dn, modlist
#
self.add_s(dn, modlist)
return CransLdapObject(self, dn, mode='w')
def _find_id(self, attr, plage = xrange(1, 32000)):
'''Trouve un <attr>id libre dans plage'''
res = self.search_s(base_dn, 2, '%s=*' % attr, attrlist = [attr])
nonfree = [ int(r[1].get(attr)[0]) for r in res if r[1].get(attr) ]
nonfree.sort()
for i in plage:
if nonfree and nonfree[0] <= i:
while nonfree and nonfree[0] <= i:
nonfree = nonfree[1:]
else:
break
else:
raise EnvironmentError(u'Aucun %s libre dans la plage [%d, %d]' %
(attr, plage[0], i))
return i
def _hist(self, msg):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M : ')
return unicode(now) + msg
# ? def reconnect(self, conn=None):
class CransLdapObject:
mode = 'ro'
attrs = None # Contient un dico uldif qui doit représenter ce qui
# est dans la base
_modifs = None # C'est là qu'on met les modifications
def __init__(self, conn, dn, mode='ro', ldif = None):
'''Créé une instance d'un objet Crans (machine, adhérent,
etc...) à ce dn, si ldif est précisé, n'effectue pas de
recherche dans la base ldap.
'''
if not isinstance(conn, lc_ldap):
raise TypeError(u"conn doit être une instance de lc_ldap")
self.conn = conn
if ldif:
self.dn = dn
# /!\ attention, on a pas un uldif (rapidité...)
self.attrs = ldif
self.__class__ = eval(self.attrs['objectClass'][0])
elif dn == base_dn:
self.__class__ = AssociationCrans
else:
self.mode = mode
res = conn.search_s(dn, 0)
if not res:
raise ValueError ('objet inexistant: %s' % dn)
self.dn, self.attrs = res[0]
if mode in ['w', 'rw']:
self.attrs = ldif_to_cldif(self.attrs)
else:
self.attrs = ldif_to_uldif(self.attrs)
self.__class__ = eval(self.attrs['objectClass'][0])
# self._modifs = copy.deepcopy(self.attrs)
def save(self):
"Vérifie que self._modifs contient des valeurs correctes et enregistre les modifications"
if self.mode not in ['w', 'rw']:
raise EnvironmentError(u"Objet en lecture seule, réessayer en lecture/écriture")
# Vérifications et Historique
#histo = self._gen_hist(self._modifs)
#self._modifs['historique'] += histo
# On récupère la liste des modifications
modlist = self.get_modlist()
self.conn.modify_s(self.dn, modlist)
# Vérification des modifications
self.attrs = ldif_to_uldif(self.conn.search_s(self.dn, 0)[0][1])
if self.attrs != self._modifs:
raise EnvironmentError(u"Les modifications apportées à l'objet %s n'ont pas été correctement sauvegardées\nexpected = %s, found = %s" % (self.dn, self._modifs, self.attrs))
def get_modlist(self):
"""Renvoie le dico des modifs"""
# unicode -> utf-8
ldif = cldif_to_ldif(self._modifs)
orig_ldif = cldif_to_ldif(self.attrs)
return modifyModlist(orig_ldif, ldif)
def get_values(self, attr):
"""Renvoie les valeurs d'un attribut ldap de l'objet self"""
attrs = self.attrs.get(attr, [])
return attrs
def get_value(self, attr):
"""Renvoie la première valeur d'un attribut ldap de l'objet self"""
return self.get_values(attr)[0]
def set_ldapattr(self, attr, new_vals):
"""Définit les nouvelles valeurs d'un attribut"""
if self.mode not in ['w', 'rw']:
raise EnvironmentError(u"Objet en lecture seule, réessayer en lecture/écriture")
if not isinstance(new_vals, list):
new_vals = [new_vals]
# On attrify
new_vals = [ attrify(val, attr, self._modifs) for val in new_vals ]
# XXX - Manque un sanity check
# Si ça passe, on effectue les modifications
old_vals = [ str(val) for val in self.attrs.get(attr, []) ]
new_vals = [ str(val) for val in new_vals ]
modlist = modifyModlist({attr : old_vals}, {attr : new_vals})
self.conn.modify_s(self.dn, modlist)
def mod_ldapattr(self, attr, new_val, old_val = None):
"""Modifie l'attribut attr ayant la valeur oldVal en newVal. Si
l'attribut attr n'a qu'une seule valeur, il n'est pas nécessaire
de préciser oldVal."""
new_val = attrify(new_val, attr, self._modifs)
new_vals = self.attrs.get(attr, [])[:]
if old_val: # and oldVal in attrs:
new_vals = [ val for val in new_vals if str(val) != str(old_val) ]
new_vals.append(new_val)
elif len(new_vals) == 1:
new_vals = [ new_val ]
else:
raise ValueError(u"%s has multiple values, must specify old_val")
return self.set_ldapattr(attr, new_vals)
def del_ldapattr(self, attr, val):
"""Supprime la valeur val de l'attribut attr"""
new_vals = self.attrs.get(attr, [])[:]
new_vals = [ v for v in new_vals if str(v) != str(val) ]
return self.set_ldapattr(attr, new_vals)
def add_ldapattr(self, attr, new_val):
"""Rajoute la valeur val à l'attribut attr"""
new_vals = self.attrs.get(attr, [])[:]
new_vals.append(new_val)
return self.set_ldapattr(attr, new_vals)
def _gen_hist(self, modifs):
# XXX - Kill it! l'historique devrait être généré par ldap
"""Genère l'historique des modifications apportées. Cette
fonction n'est là que pour de la rétro-compatibilité,
normalement les modifications sont automatiquement loggées."""
histo = []
for field in self.ofields:
if modifs.get(field, []) != self.attrs.get(field, []):
if modifs.get(field, []) == []:
msg = u"[%s] %s -> RESET" % (field, self.attrs[field][0])
elif self.attrs.get(field, []) == []:
msg = u"[%s] := %s" % (field, modifs[field][0])
else:
msg = u"[%s] %s -> %s" % (field, self.attrs[field][0], modifs[field][0])
histo.append(self.conn._hist(msg))
for field in self.xfields + self.ufields:
if modifs.get(field, []) != self.attrs.get(field, []):
msg = u"[%s] %s -> %s" % (field, u'; '.join(self.attrs[field]), u'; '.join(modifs[field]))
histo.append(self.conn._hist(msg))
for field in self.mfields:
oldvals = self.attrs.get(field, [])
newvals = modifs.get(field, [])
olds = set(oldvals)
news = set(newvals)
if oldvals == newvals:
continue
elif olds != news:
adds = ''.join([ '+' + val for val in news - olds])
diff = ''.join([ '-' + val for val in olds - news])
msg = u'[%s]%s%s' % ( field, adds, diff)
elif olds == news and len(oldvals) == len(newvals) == len(olds):
msg = u"[%s].shuffle()" % field
else:
raise ValueError(u"Les valeurs pour %s : %s -> %s ne semblent pas différentes" % (field, oldvals, newvals))
histo.append(self.conn._hist(msg))
return histo
def blacklist_actif(self):
u"""Vérifie si l'instance courante est blacklistée.
Retourne les sanctions en cours (liste).
Retourne une liste vide si aucune sanction en cours.
"""
return self.blacklist_all()[0].keys()
def blacklist_all(self):
u"""Vérifie si l'instance courante est blacklistée ou a été
blacklistée. Retourne les sanctions en cours sous la forme
d'un couple de deux dictionnaires (l'un pour les sanctions
actives, l'autre pour les inactive), chacun ayant comme
clef la sanction et comme valeur une liste de couple de
dates (en secondes depuis epoch) correspondant aux
différentes périodes de sanctions.
ex: {'upload': [(1143336210, 1143509010), ...]}
"""
bl_liste = self.attrs.get('blacklist', [])
if isinstance(self, machine): # Blist du propriétaire
bl_liste += proprio().blacklist()
actifs = {}; inactifs = {}
for sanction in bl_liste:
f = sanction.split('$')
if is_actif(sanction):
actifs.setdefault(f[2], []).append((f[0], f[1]))
else:
inactifs.setdefault(f[2], []).append((f[0], f[1]))
return (actifs, inactifs)
def blacklist(self, new=None):
u"""
Blacklistage de la ou de toutes la machines du propriétaire
* new est une liste de 4 termes :
[debut_sanction, fin_sanction, sanction, commentaire]
* début et fin sont le nombre de secondes depuis epoch
* pour un début ou fin immédiate mettre now
* pour une fin indéterminée mettre '-'
Les données sont stockées dans la base sous la forme :
debut$fin$sanction$commentaire
Pour modifier une entrée donner un tuple de deux termes :
(index dans blacklist à modifier, nouvelle liste),
l'index étant celui dans la liste retournée par blacklist().
"""
Blist = self.attrs.setdefault('blacklist', [])[:]
if new == None:
return Blist
if type(new) == tuple:
# Modification
index = new[0]
new = new[1]
if new == '':
Blist.pop(index)
return Blist
else:
index = -1
if type(new) != list or len(new) != 4:
raise TypeError
# Verification que les dates sont OK
if new[0] == 'now':
debut = new[0] = int(time.time())
else:
try: debut = new[0] = int(new[0])
except: raise ValueError(u'Date de début blacklist invalide')
if new[1] == 'now':
fin = new[1] = int(time.time())
elif new[1] == '-':
fin = -1
else:
try: fin = new[1] = int(new[1])
except: raise ValueError(u'Date de fin blacklist invalide')
if debut == fin:
raise ValueError(u'Dates de début et de fin identiques')
elif fin != -1 and debut > fin:
raise ValueError(u'Date de fin avant date de début')
# On dépasse la fin de sanction d'1min pour être sûr qu'elle est périmée.
fin = fin + 60
new_c = u'$'.join(map(str, new))
if index != -1:
Blist[index] = new_c
else:
Blist.append(new_c)
if Blist != self.attrs.get('blacklist'):
self.set_ldapattr('blacklist', Blist)
if not hasattr(self, "_blacklist_restart"):
self._blacklist_restart = {}
restart = self._blacklist_restart.setdefault(new[2], [])
if debut not in restart:
restart.append(debut)
if fin != -1 and fin not in restart:
restart.append(fin)
return Blist
class proprio(CransLdapObject):
ufields = [ 'nom', 'chbre' ]
mfields = [ 'paiement', 'info', 'blacklist', 'controle']
ofields = []; xfields = []
_machines = None
def machines(self):
if self._machines == None:
self._machines = self.conn.search_s('mid=*', dn = self.dn, scope = 1)
for m in self._machines:
m._proprio = self
return self._machines
class machine(CransLdapObject):
_proprio = None
ufields = ['mid', 'macAddress', 'host', 'midType']
ofields = []
mfields = ['info', 'blacklist', 'hostAlias', 'exempt',
'portTCPout', 'portTCPin', 'portUDPout', 'portUDPin']
xfields = ['ipHostNumber']
def proprio(self):
parent_dn = self.dn.split(',', 1)[1]
self._proprio = CransLdapObject(self.conn, parent_dn, self.mode)
return self._proprio
class AssociationCrans(proprio): pass
class adherent(proprio):
ufields = proprio.ufields + ['aid', 'prenom', 'tel', 'mail', 'mailInvalide']
ofields = proprio.ofields + ['charteMA', 'adherentPayant', 'typeAdhesion',
'canonicalAlias', 'solde', 'contourneGreylist',
'rewriteMailHeaders', 'derniereConnexion',
'homepageAlias']
mfields = proprio.mfields + ['carteEtudiant', 'mailAlias', 'droits' ]
xfields = ['etudes', 'postalAddress']
class club(proprio):
ufields = ['cid', 'responsable']
mfields = ['imprimeurClub']
class machineFixe(machine): pass
class machineWifi(machine):
ufields = machine.ufields + ['ipsec']
class machineCrans(machine):
ufields = machine.ufields + ['prise']
ofields = machine.ofields + ['nombrePrises']
class borneWifi(machine):
ufields = machine.ufields + ['canal', 'puissane', 'hotspot',
'prise', 'positionBorne', 'nvram']
class facture(CransLdapObject):
ufields = ['fid', 'modePaiement', 'recuPaiement']
class service(CransLdapObject): pass
class lock(CransLdapObject): pass
MODIFIABLE_ATTRS = [ 'tel', 'chbre', 'mailAlias', 'loginShell' ]