diff --git a/attributs.py b/attributs.py index a5eb676..92a2ea4 100644 --- a/attributs.py +++ b/attributs.py @@ -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 == '': @@ -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 diff --git a/crans_utils.py b/crans_utils.py index 4593a67..cb93d8a 100644 --- a/crans_utils.py +++ b/crans_utils.py @@ -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(':', '') diff --git a/lc_ldap.py b/lc_ldap.py index 56e4088..7ec1aec 100644 --- a/lc_ldap.py +++ b/lc_ldap.py @@ -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 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