Quelques améliorations, mise en place de fonctions pour tester parenté,

tester si c'est soi-même qu'on modifie, pour tester si on peut bien
altérer l'objet concerné.
Création des objets en deux temps (on crée l'objet Crans, puis
on l'enregistre dans ldap après test des droits.
Changement de méthode de binding : le binding nominatif va être bien
trop complexe à implémenter, on va donc faire autrement...
This commit is contained in:
Pierre-Elliott Bécue 2013-01-28 00:45:01 +01:00
parent fcafdbff28
commit 42c48f77e8
3 changed files with 190 additions and 61 deletions

View file

@ -425,6 +425,8 @@ class hostAlias(host):
optional = True
legend = u'Alias de nom de machine'
can_modify = [nounou, cableur]
class macAddress(Attr):
singlevalue = True
optional = False
@ -447,6 +449,7 @@ class ipHostNumber(Attr):
legend = u"Adresse IPv4 de la machine"
hname = "IPv4"
category = 'base_tech'
can_modify = [nounou]
def parse_value(self, ip, ldif):
if ip == '<automatique>':
@ -476,6 +479,7 @@ class rid(Attr):
unique = True
legend = "Identifiant réseau de machine"
category = 'id'
can_modify = [nounou]
def parse_value(self, rid, ldif):
self.value = Rid(rid = int(rid))
@ -494,18 +498,21 @@ class puissance(Attr):
optional = True
legend = u"puissance d'émission pour les bornes wifi"
category = 'wifi'
can_modify = [nounou]
class canal(intAttr):
singlevalue = True
optional = True
legend = u'Canal d\'émission de la borne'
category = 'wifi'
can_modify = [nounou]
class portAttr(Attr):
singlevalue = False
optional = True
legend = u'Ouverture de port'
category = 'firewall'
can_modify = [nounou]
def parse_value(self, port, ldif):
if ":" in port:
@ -553,6 +560,7 @@ class prise(Attr):
optional = True
legend = u"Prise sur laquelle est branchée la machine"
category = 'base_tech'
can_modify = [nounou]
def parse_value(self, prise, ldif):
### Tu es une Nounou, je te fais confiance
@ -570,6 +578,7 @@ class responsable(Attr):
optional = True
legend = u"Responsable du club"
category = 'perso'
can_modify = [nounou]
def get_respo(self):
if self.value == None:
@ -583,12 +592,12 @@ class responsable(Attr):
def __unicode__(self):
return self.__resp
class blacklist(Attr):
singlevalue = False
optional = True
legend = u"Blackliste"
category = 'info'
can_modify = [nounou]
def parse_value(self, bl, ldif):
bl_debut, bl_fin, bl_type, bl_comm = bl.split('$')
@ -712,9 +721,7 @@ class gecos(Attr):
category = 'id'
def parse_value(self, gecos, ldif):
a, b, c, d = gecos.split(',')
self.value = gecos
class sshFingerprint(Attr):
singlevalue = False

View file

@ -77,8 +77,6 @@ def ip6_of_mac(mac, rid):
else:
raise ValueError("Rid dans aucune plage: %d" % rid)
print net
# En théorie, format_mac est inutile, car on ne devrait avoir
# que des mac formatées.
mac = format_mac(mac).replace(':', '')

View file

@ -36,17 +36,29 @@
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import with_statement
import os, sys, ldap, re, netaddr, datetime, copy, time, random
import os
import sys
import ldap
import ldap.filter
from ldap.modlist import addModlist, modifyModlist
import re
import netaddr
import datetime
import copy
import time
import random
try:
from Levenshtein import jaro
except ImportError:
def jaro(a, b): return 0
sys.path.append('/usr/scripts/gestion')
import config, crans_utils
from attributs import attrify, blacklist
import config
import crans_utils
from attributs import *
from ldap_locks import CransLock
import ridtools
@ -54,6 +66,11 @@ uri = 'ldap://ldap.adm.crans.org/'
base_dn = 'ou=data,dc=crans,dc=org'
log_dn = "cn=log"
# Protection contre les typos
created = 'created'
modified = 'modified'
deleted = 'deleted'
# 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)
@ -75,6 +92,10 @@ def escape(chaine):
return ldap.filter.escape_filter_chars(chaine)
def ldif_to_uldif(ldif):
"""
Transforme un dictionnaire ldif en un dictionnaire
ldif unicode.
"""
uldif = {}
for attr, vals in ldif.items():
uldif[attr] = [ unicode(val, 'utf-8') for val in vals ]
@ -92,6 +113,10 @@ def ldif_to_cldif(ldif, conn, check_ctxt = True):
return cldif
def cldif_to_ldif(cldif):
"""
Transforme un ldif crans en ldif valide au sens openldap.
Ce ldif est celui qui sera transmis à la base.
"""
ldif = {}
for attr, vals in cldif.items():
ldif[attr] = [ str(val) for val in vals ]
@ -108,6 +133,7 @@ def lc_ldap_admin():
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
"""
@ -147,7 +173,7 @@ class lc_ldap(ldap.ldapobject.LDAPObject):
# Si on a un dn, on se connecte avec à la base ldap sinon on s'y
# connecte en anonyme
if dn:
self.conn = self.bind_s(dn, cred)
self.conn = self.bind_s(secrets.ldap_auth_dn, secrets.ldap_password)
self.dn = dn
self.droits = self.search_s(dn, ldap.SCOPE_BASE, attrlist=['droits'])[0][1].get('droits', [])
else:
@ -155,13 +181,13 @@ class lc_ldap(ldap.ldapobject.LDAPObject):
self.dn = None
self.droits = []
def search(self, filterstr='(objectClass=*)', mode='ro', dn= base_dn, scope= 2, sizelimit=400):
def search(self, filterstr='(objectClass=*)', mode='ro', dn= base_dn, scope=ldap.SCOPE_SUBTREE, sizelimit=1000):
"""La fonction de recherche dans la base ldap qui renvoie un liste de
CransLdapObjects. On utilise la feature de sizelimit de python ldap"""
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))
ret.append(new_cransldapobject(self, dn, mode, ldif))
return ret
def allMachinesAdherents(self):
@ -213,20 +239,17 @@ class lc_ldap(ldap.ldapobject.LDAPObject):
#adm, serveurs, bornes, wifi, adherents, gratuit ou personnel-ens"""
owner = self.search('objectClass=*', dn=parent, scope=0)[0]
if realm in ["adm", "serveurs"]:
if realm in ["adm", "serveurs", "serveurs-v6", "adm-v6"]:
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", "personnel-ens"]:
uldif['objectClass'] = [u'machineFixe']
@ -234,26 +257,43 @@ class lc_ldap(ldap.ldapobject.LDAPObject):
else: raise ValueError("Realm inconnu: %r" % realm)
# On récupère la plage des mids
plage = xrange( *(config.mid[realm]))
# On récupère le premier id libre dans la plages s'il n'est pas
# déjà précisé dans le ldiff
mid = uldif.setdefault('mid', [ unicode(self._find_id('mid', plage)) ])
rid = uldif.setdefault('rid', [ unicode(self._find_id('rid', plage)) ])
# La machine peut-elle avoir une ipv4 ?
if 'v6' not in realm:
uldif['ipHostNumber'] = [ unicode(crans_utils.ip4_of_mid(int (mid[0]))) ]
return self._create_entity('mid=%s,%s' % (mid[0], parent), uldif)
uldif['ipHostNumber'] = [ unicode(crans_utils.ip4_of_rid(rid[0])) ]
uldif['ip6HostNumber'] = [ unicode(crans_utils.ip6_of_mac(uldif['macAddress'][0], rid[0])) ]
# On récupère la plage des mids
plage = xrange( *(config.rid[realm]))
# Tout doit disparaître !!
machine = self._create_entity('mid=%s,%s' % (mid[0], parent), uldif)
if machine.may_be(created, self.droits + self._is_parent(machine)):
machine.create()
else:
raise EnvironmentError("Vous n'avez pas le droit de créer cette machine.")
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)
adherent = self._create_entity('aid=%s,%s' % (aid[0], base_dn), uldif)
if adherent.may_be(created, self):
adherent.create()
else:
raise EnvironmentError("Vous n'avez pas le droit de créer cet adhérent.")
def newClub(self, uldif):
"""Crée un nouveau club"""
cid = uldif.setdefault('cid', [ unicode(self._find_id('cid')) ])
uldif['objectClass'] = [u'club']
return self._create_entity('cid=%s,%s' % (cid[0], base_dn), uldif)
club = self._create_entity('cid=%s,%s' % (cid[0], base_dn), uldif)
if club.may_be(created, self):
club.create()
else:
raise EnvironmentError("Vous n'avez pas le droit de créer cet adhérent.")
def newFacture(self, uldif):
"""Crée une nouvelle facture
@ -267,31 +307,53 @@ class lc_ldap(ldap.ldapobject.LDAPObject):
cldif = ldif_to_cldif(uldif, self)
# Conversion en ascii
ldif = cldif_to_ldif(cldif)
# Création de la requête LDAP
modlist = addModlist(ldif)
# Requête LDAP de création de l'objet
self.add_s(dn, modlist)
# Renvoi du CransLdapObject
return new_cransldapobject(self, dn, mode='w')
return new_cransldapobject(self, dn, 'rw', ldif)
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])
def _find_id(self, attr, plage=None):
'''Trouve un id libre. Si une plage est fournie, cherche
l'id dans celle-ci, sinon, prend le plus élevé possible.'''
res = self.search_s(base_dn, ldap.SCOPE_SUBTREE, '%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:]
if plage != None:
for i in plage:
if i in nonfree:
continue
else:
break
else:
break
raise EnvironmentError('Aucun %s libre dans la plage [%d, %d]' %
(attr, plage[0], i))
else:
raise EnvironmentError('Aucun %s libre dans la plage [%d, %d]' %
(attr, plage[0], i))
i = nonfree[-1]+1
return i
def _is_parent(self, obj):
"""
Teste le lien de parenté de l'objet 1 sur l'objet 2.
Retourne une liste qui s'ajoutera à la liste des droits
"""
if obj.dn.endswith(self.dn) and obj.dn != self.dn:
return [parent]
else:
return []
def _is_self(obj1, obj2):
"""
Teste si l'objet qui se binde est celui qui est en cours de modif.
Retourne une liste qui s'ajoutera à la liste des droits
"""
if obj.dn == self.dn:
return [soi]
else:
return []
def new_cransldapobject(conn, dn, mode='ro', ldif = None):
"""Crée un objet CransLdap en utilisant la classe correspondant à
@ -317,6 +379,11 @@ class CransLdapObject(object):
"""Classe de base des objets CransLdap.
Cette classe ne devrait pas être utilisée directement."""
can_be_by = { created: [nounou],
modified: [nounou],
deleted: [nounou],
}
""" Champs uniques et nécessaires """
ufields = []
@ -348,36 +415,48 @@ class CransLdapObject(object):
self.conn = conn
self.dn = dn
orig = {}
if ldif:
# Vous précisez un ldif, l'objet est 'ro'
self.mode = 'ro'
self.attrs = ldif
if dn != base_dn: # new_cransldapobject ne donne pas de ldif formaté et utilise un ldif non formaté, donc on formate
self.attrs = ldif_to_uldif(self.attrs)
self.attrs = ldif_to_cldif(self.attrs, conn, check_ctxt = False) # on est en read only, donc pas la peine de vérifier la validité
self.attrs = ldif_to_cldif(self.attrs, conn, check_ctxt = True)
self._modifs = ldif_to_uldif(ldif)
self._modifs = ldif_to_cldif(self._modifs, conn, check_ctxt = False)
orig = ldif
elif dn != base_dn:
res = conn.search_s(dn, 0)
if not res:
raise ValueError ('objet inexistant: %s' % dn)
self.dn, self.attrs = res[0]
# Pour test en cas de mode w ou rw
orig = res[0][1]
self.attrs = ldif_to_uldif(self.attrs)
# L'objet sortant de la base ldap, on ne fait pas de vérifications sur
# l'état des données.
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)
self._modifs = ldif_to_cldif(ldif_to_uldif(res[0][1]), conn, check_ctxt = False)
for attr, vals in oldif.items():
if nldif[attr] != vals:
for v in nldif[attr]:
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 (ie non idempotente):", attr, nvals, vals)
# Je m'interroge sur la pertinence de cette partie, je pense qu'elle n'est
# pas utile. -- PEB 27/01/2013
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 = orig
nldif = cldif_to_ldif(self.attrs)
self._modifs = ldif_to_cldif(ldif_to_uldif(res[0][1]), conn, check_ctxt = False)
for attr, vals in oldif.items():
if nldif[attr] != vals:
for v in nldif[attr]:
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 (ie non idempotente):", attr, nvals, vals)
def _get_fields(self):
"""Renvoie la liste des champs LDAP de l'objet"""
@ -387,7 +466,7 @@ class CransLdapObject(object):
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"""
### mais on devrait tout recoder pour utiliser l'historique LDAP"""
assert isinstance(login, str) or isinstance(login, unicode)
assert isinstance(chain, unicode)
@ -395,6 +474,16 @@ class CransLdapObject(object):
# Attention, le __setitem__ est surchargé, mais pas .append sur l'historique
self["historique"] = self["historique"] + [new_line]
def create(self):
"""Crée l'objet dans la base ldap, cette méthode vise à faire en sorte que
l'objet se crée lui-même, si celui qui essaye de le modifier a les droits
de le faire."""
# Création de la requête LDAP
modlist = addModlist(cldif_to_ldif(self.attrs))
# Requête LDAP de création de l'objet
self.add_s(self.dn, modlist)
def save(self):
"""Sauvegarde dans la base les modifications apportées à l'objet.
@ -405,7 +494,10 @@ class CransLdapObject(object):
# On récupère la liste des modifications
modlist = self.get_modlist()
self.conn.modify_s(self.dn, modlist)
try:
self.conn.modify_s(self.dn, modlist)
except:
raise EnvironmentError("Impossible de modifier l'objet, peut-être n'existe-t-il pas ?")
# Vérification des modifications
self.attrs = ldif_to_uldif(self.conn.search_s(self.dn, 0)[0][1])
@ -420,6 +512,16 @@ class CransLdapObject(object):
if differences:
raise EnvironmentError("Les modifications apportées à l'objet %s n'ont pas été correctement sauvegardées\n%s" % (self.dn, differences))
def may_be(self, what, obj):
"""Teste si celui qui est bindé peut effectuer ce qui est dans what, pour
what élément de {create, delete, modify}.
Retourne un booléen
"""
if set(obj.droits).intersection(self.can_be_by[what]) != set([]):
return True
else:
return False
def get_modlist(self):
"""Renvoie un dictionnaire des modifications apportées à l'objet"""
# unicode -> utf-8
@ -475,9 +577,9 @@ class CransLdapObject(object):
if res != []:
author = res[0].compte()
if attrs['reqType'][0] == 'delete':
if attrs['reqType'][0] == deleted:
out.append(u"%s : [%s] Suppression" % (date, author))
elif attrs['reqType'][0] == 'modify':
elif attrs['reqType'][0] == modified:
fields = {}
for mod in attrs['reqMod']:
mod = mod.decode('utf-8')
@ -534,6 +636,11 @@ class CransLdapObject(object):
class proprio(CransLdapObject):
u""" Un propriétaire de machine (adhérent, club…) """
can_be_by = { created: [nounou, bureau, cableur],
modified: [nounou, bureau, soi, cableur],
deleted: [nounou, bureau],
}
ufields = [ 'nom', 'chbre' ]
mfields = [ 'paiement', 'info', 'blacklist', 'controle']
ofields = []
@ -623,11 +730,16 @@ class proprio(CransLdapObject):
class machine(CransLdapObject):
u""" Une machine """
can_be_by = { created: [nounou, bureau, cableur, parent],
modified: [nounou, bureau, cableur, parent],
deleted: [nounou, bureau, cableur, parent],
}
ufields = ['mid', 'macAddress', 'host', 'midType']
ofields = []
ofields = ['rid']
mfields = ['info', 'blacklist', 'hostAlias', 'exempt',
'portTCPout', 'portTCPin', 'portUDPout', 'portUDPin','sshFingerprint']
xfields = ['ipHostNumber']
'portTCPout', 'portTCPin', 'portUDPout', 'portUDPin','sshFingerprint', 'ipHostNumber', 'ip6HostNumber']
xfields = []
def __init__(self, conn, dn, mode='ro', ldif = None):
super(machine, self).__init__(conn, dn, mode, ldif)
@ -746,14 +858,26 @@ class machineWifi(machine):
ufields = machine.ufields + ['ipsec']
class machineCrans(machine):
can_be_by = { created: [nounou],
modified: [nounou],
deleted: [nounou],
}
ufields = machine.ufields + ['prise']
ofields = machine.ofields + ['nombrePrises']
class borneWifi(machine):
can_be_by = { created: [nounou],
modified: [nounou],
deleted: [nounou],
}
ufields = machine.ufields + ['canal', 'puissance', 'hotspot',
'prise', 'positionBorne', 'nvram']
class facture(CransLdapObject):
can_be_by = { created: [nounou, bureau, cableur],
modified: [nounou, bureau, cableur],
deleted: [nounou, bureau, cableur],
}
ufields = ['fid', 'modePaiement', 'recuPaiement']
class service(CransLdapObject): pass