#!/usr/bin/env python # -*- coding: utf-8 -*- # # LC_LDAP.PY-- LightWeight CransLdap # # Copyright (C) 2010-2013 Cr@ns # Authors: Antoine Durand-Gasselin # Nicolas Dandrimont # Olivier Iffrig # Valentin Samir # Daniel Stan # Vincent Le Gallic # Pierre-Elliott Bécue # # 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 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. ## import de la lib standard import os import sys import re from contextlib import contextmanager import ldap ## import locaux import crans_utils import attributs import cimetiere import objets import ldap_locks import variables import copy import itertools ## import de /usr/scripts/ if not "/usr/scripts/" in sys.path: sys.path.append('/usr/scripts/') import gestion.config as config import cranslib.deprecated # A priori, ldif_to_uldif et ldif_to_cldif sont obsolètes, # du fait de l'apparition de AttrsDict dans attributs.py def ldif_to_uldif(ldif): """ Transforme un dictionnaire ldif en un dictionnaire ldif unicode. """ uldif = {} for attr, vals in ldif.items(): if attr.endswith(';binary'): binary = True attr=attr[:-7] else: binary = False attr_class = attributs.AttributeFactory.get(attr, fallback=attributs.Attr) binary = binary or attr_class.binary uldif[attr] = [ unicode(val, 'utf-8') if not binary else val for val in vals ] return uldif class lc_ldap(ldap.ldapobject.LDAPObject, object): """Connexion à la base ldap crans, chaque instance représente une connexion """ def __init__(self, dn=None, user=None, cred=None, uri=variables.uri, readonly_dn=None, readonly_password=None): """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 on ne se binde pas en anonyme, il faut de toutes façons fournir un ``user``. Il sert à savoir qui fait les modifications (utilisé dans les champs historique). """ ldap.ldapobject.LDAPObject.__init__(self, uri) self.lockholder = ldap_locks.LdapLockHolder(self) if user and not re.match('[a-z_][a-z0-9_-]*', user): raise ValueError('Invalid user name: %r' % user) # Si un username, on récupère le dn associé… if user and not dn: dn = self.user_to_dn(user, readonly_dn, readonly_password) # 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.dn = dn self.droits = self.search_s(dn, ldap.SCOPE_BASE, attrlist=['droits'])[0][1].get('droits', []) if dn == variables.admin_dn: self.droits += [attributs.nounou] # Il faut peupler current_login, qui sera utilisé pour écrire dans l'historique qui fait des modifications if dn in [variables.admin_dn, variables.readonly_dn]: # À ce stade, l'utilsateur qui appelle le script a réussi à se binder en cn=admin ou cn=readonly, # c'est donc qu'il a pu lire les secrets, (directement ou par un sudo idoine) # on lui fait donc confiance sur l'username qu'il fournit à condition qu'il en ait fournit un, quand même if user is None: raise ValueError("Même root doit préciser qui il est pour se connecter à la base LDAP.") self.current_login = user else: current_user = self.search(u'uid=%s' % user) if len(current_user) != 1: raise ValueError("L'utilisateur %s n'est pas présent dans la base en *1* exemplaire." % user) else: self.current_login = current_user[0]["uid"][0].value else: self.conn = self.simple_bind_s() self.dn = None self.droits = [] self._username_given = user def __repr__(self): if self.dn: return str(self.__class__) + " : " + self.dn else: return super(lc_ldap, self).__repr__() def gravedig(self, type, filter=None, date=None): """Cherche dans le cimetière un objet de type ``type``, correspondant au filtre ``filter`` entre les dates ``date[0]`` et ``date[1]`` où la date est de la forme YYYY-MM-JJ ou - pour l'infini""" valid=cimetiere.find(type, filter, date) return [self.ressuscite(item) for item in valid] @staticmethod def ressuscite_build_ldif(ldif_file): ldif={} for line in open(ldif_file).readlines(): line = line.split(':',1) if len(line)==2: (key, value) = line ldif[key]=ldif.get(key, []) + [value.strip()] try: dn = ldif['dn'][0] del(ldif['dn']) return (dn,ldif) except KeyError as error: raise KeyError("%s in %s" % (error, ldif_file)) def ressuscite(self, ldif_file, login=None): if login is None: login = self.current_login (dn, ldif)= self.ressuscite_build_ldif(ldif_file) # On définit de nouveaux dn si ceux-ci sont déjà pris lockId = self.lockholder.newid() try: if self.search(dn=dn): for id in ["aid", "mid", "fid", "cid", "xid"]: if dn.startswith("%s=" % id): ldif[id]=[str(self._find_id(id, lockId=lockId))] dn="%s=%s,%s" % (id, ldif[id][0], dn.split(',',1)[1]) except ldap.NO_SUCH_OBJECT: pass obj = self._create_entity(dn, ldif_to_uldif(ldif), lockId) obj.history_add(login, u"resurrection") return obj def user_to_dn(self, user, readonly_dn, readonly_password): """Cherche le dn à partir de l'username. Cette méthode est utilisée par l'intranet2 (en mode sans CAS) car il donne l'username.""" # On commence par se binder en readonly self.simple_bind_s(who=readonly_dn, cred=readonly_password) res = self.search_s(variables.base_dn, 1, '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] return dn def search(self, filterstr=u'(objectClass=*)', mode='ro', dn=variables.base_dn, scope=ldap.SCOPE_SUBTREE, sizelimit=1000): """La fonction de recherche dans la base LDAP qui renvoie un liste de :py:class:`CransLdapObject`. On utilise la feature de ``sizelimit`` de ``python-ldap``""" if not isinstance(filterstr, unicode): cranslib.deprecated.usage("search ne devrait utiliser que des unicode comme filtre(%r)" % filterstr, level=2) filterstr = filterstr.decode('utf-8') ldap_res = self.search_ext_s(dn, scope, filterstr.encode('utf-8'), sizelimit=sizelimit) ret = [] for dn, ldif in ldap_res: uldif = ldif_to_uldif(ldif) ret.append(objets.new_cransldapobject(self, dn, mode, uldif)) return ret def machinesMulticast(self): import cPickle import tv.dns import config.dns machines = [] sap=cPickle.load(open('/usr/scripts/var/tv/sap.pickel')) for name_ip in sap.values(): for (nom, ip) in name_ip.items(): nom=unicode(nom, 'utf-8') nom_ascii=tv.dns.ascii(nom) nom_punycode=tv.dns.punycode(nom) ldif = { 'ipHostNumber' : [unicode(ip)], 'objectClass': [u'machineFixe'], 'host': [u"%s.%s" % (nom_ascii, config.dns.zone_tv)] } if nom_punycode: ldif['hostAlias']=[u"%s.%s" % (nom_punycode, config.dns.zone_tv)] machines.append(objets.machineMulticast(self, "", uldif=ldif)) return machines def allMachinesAdherents(self, mode='ro'): """Renvoie la liste de toutes les machines et de tous les adherents (club et Association Crans compris). Conçue pour s'éxécuter le plus rapidement possible. On dumpe malgré tout toute la base.""" res = {} parent = {} machines = {} # (proxying de la base ldap) for dn, attrs in self.search_s(variables.base_dn, scope=2): # On crée les listes des machines et propriétaires if dn.startswith('mid='): # les machines m = objets.new_cransldapobject(self, dn, mode, uldif=ldif_to_uldif(attrs)) parent_dn = dn.split(',', 1)[1] if not machines.has_key(parent_dn): machines[parent_dn] = [] machines[parent_dn].append(m) elif (dn.startswith('aid=') or dn.startswith('cid=') or dn == variables.base_dn) and not parent.has_key(dn): parent[dn] = objets.new_cransldapobject(self, dn, mode, uldif=ldif_to_uldif(attrs)) allmachines = [] for dn, mlist in machines.iteritems(): # on associe propriétaires et machines parent[dn]._machines = mlist for m in mlist: m._proprio = parent[dn] allmachines.append(m) return allmachines, parent.values() # on renvoie la liste des machines et des adherents (dont club et crans) def allMachines(self, mode='ro'): """Renvoie la liste de toutes les machines, Conçue pour s'éxécuter le plus rapidement possible. On dumpe malgré tout toute la base, c'est pour pouvoir aussi rajouter à moindre coût les propriétaires.""" machines, _ = self.allMachinesAdherents(mode) return machines def allAdherents(self, mode='ro'): """Renvoie la liste de toutes les adherents, Conçue pour s'éxécuter le plus rapidement possible. On dumpe malgré tout toute la base, c'est pour pouvoir aussi rajouter à moindre coût les machines.""" _, adherents = self.allMachinesAdherents(mode) return adherents @contextmanager def newMachine(self, parent, realm, mldif, login=None): """ Crée une nouvelle machine: ``realm`` peut être: fil, adherents-v6, wifi, wifi-adh-v6, adm, gratuit, personnel-ens, special mldif est un uldif pour la machine --Partiellement implémenté Doit être utilisé avec un context manager, c'est à dire comme ci-dessous : 1: with newMachine(parent, realm, mldif) as machine: 2: machine.create() 3: print machine La fonction est executé jusqu'au yield à la ligne 1, puis son exécution reprend au niveau du yield jusqu'à la fin de la fonction à la ligne 3, en sortant du contexte Une fois sorti du contexte, il n'est plus possible d'effectuer des actions d'écriture sur l'objet. """ lockId = self.lockholder.newid() # On ne veut pas modifier mldif directement uldif = copy.deepcopy(mldif) if login is None: login = self.current_login #adm, serveurs, bornes, wifi, adherents, gratuit ou personnel-ens""" owner = self.search(u'objectClass=*', dn=parent, scope=0)[0] if realm in ["adm", "serveurs", "serveurs-v6", "adm-v6"]: uldif['objectClass'] = [u'machineCrans'] assert isinstance(owner, objets.AssociationCrans) elif realm == "bornes": uldif['objectClass'] = [u'borneWifi'] assert isinstance(owner, objets.AssociationCrans) elif realm in ["wifi-adh", "wifi-adh-v6"]: uldif['objectClass'] = [u'machineWifi'] assert isinstance(owner, objets.adherent) or isinstance(owner, objets.club) elif realm in ["adherents", "adherents-v6", "personnel-ens"]: uldif['objectClass'] = [u'machineFixe'] assert isinstance(owner, objets.adherent) or isinstance(owner, objets.club) else: raise ValueError("Realm inconnu: %r" % realm) try: # On récupère le premier id libre dans la plages s'il n'est pas # déjà précisé dans le ldif rid = uldif.setdefault('rid', [unicode(self._find_id('rid', realm, lockId=lockId))]) # La machine peut-elle avoir une ipv4 ? if 'v6' not in realm: uldif['ipHostNumber'] = [ unicode(crans_utils.ip4_of_rid(int(rid[0]))) ] ip6 = unicode(crans_utils.ip6_of_mac(uldif['macAddress'][0], int(rid[0]))) uldif['ip6HostNumber'] = [ ip6 ] if ip6 else [] # Mid uldif['mid'] = [ unicode(self._find_id('mid', lockId=lockId)) ] # Tout doit disparaître !! machine = self._create_entity('mid=%s,%s' % (uldif['mid'][0], parent), uldif, lockId) machine.__enter__() if machine.may_be(variables.created, self.droits + self._check_parent(machine.dn)): yield machine else: raise EnvironmentError("Vous n'avez pas le droit de créer cette machine.") finally: try: machine.__exit__(None, None, None) except NameError: self.lockholder.purge(lockId) @contextmanager def newAdherent(self, aldif): """Crée un nouvel adhérent Doit être utilisé avec un context manager, voir newMachine pour plus de détails""" lockId = self.lockholder.newid() uldif = copy.deepcopy(aldif) try: aid = uldif.setdefault('aid', [ unicode(self._find_id('aid', lockId=lockId)) ]) uldif['objectClass'] = [u'adherent'] adherent = self._create_entity('aid=%s,%s' % (aid[0], variables.base_dn), uldif, lockId) adherent.__enter__() if adherent.may_be(variables.created, self.droits): yield adherent else: raise EnvironmentError("Vous n'avez pas le droit de créer cet adhérent.") finally: try: adherent.__exit__(None, None, None) except NameError: self.lockholder.purge(lockId) @contextmanager def newClub(self, cldif): """Crée un nouveau club Doit être utilisé avec un context manager, voir newMachine pour plus de détails""" lockId = self.lockholder.newid() uldif = copy.deepcopy(cldif) try: cid = uldif.setdefault('cid', [ unicode(self._find_id('cid', lockId=lockId)) ]) uldif['objectClass'] = [u'club'] club = self._create_entity('cid=%s,%s' % (cid[0], variables.base_dn), uldif, lockId) club.__enter__() if club.may_be(variables.created, self.droits): yield club else: raise EnvironmentError("Vous n'avez pas le droit de créer cet adhérent.") finally: try: club.__exit__(None, None, None) except NameError: self.lockholder.purge(lockId) @contextmanager def newFacture(self, parent, fldif): """Crée une nouvelle facture Doit être utilisé avec un context manager, voir newMachine pour plus de détails""" lockId = self.lockholder.newid() uldif = copy.deepcopy(fldif) try: # fid uldif['fid'] = [ unicode(self._find_id('fid', lockId=lockId)) ] uldif['objectClass'] = [u'facture'] facture = self._create_entity('fid=%s,%s' % (uldif['fid'][0], parent), uldif, lockId) facture.__enter__() if facture.may_be(variables.created, self.droits + self._check_parent(facture.dn)): yield facture else: raise EnvironmentError("Vous n'avez pas le droit de créer cette facture.") finally: try: facture.__exit__(None, None, None) except NameError: self.lockholder.purge(lockId) @contextmanager def newCertificat(self, parent, xldif): """Crée un nouveau certificat x509 Doit être utilisé avec un context manager, voir newMachine pour plus de détails""" lockId = self.lockholder.newid() uldif = copy.deepcopy(xldif) try: # xid uldif['xid'] = [ unicode(self._find_id('xid', lockId=lockId)) ] uldif['objectClass'] = [u'baseCert'] baseCert = self._create_entity('xid=%s,%s' % (uldif['xid'][0], parent), uldif, lockId) baseCert.__enter__() if baseCert.may_be(variables.created, self.droits + self._check_parent(baseCert.dn)): yield baseCert else: raise EnvironmentError("Vous n'avez pas le droit de créer ce certiticat.") finally: try: baseCert.__exit__(None, None, None) except NameError: self.lockholder.purge(lockId) def _create_entity(self, dn, uldif, lockId): '''Crée une nouvelle entité ldap avec le dn ``dn`` et les attributs de ``ldif``. Attention, ldif doit contenir des données encodées.''' # Ajout des locks, on instancie les attributs qui ne sont pas # des id, ceux-ci étant déjà lockés. try: for key, values in uldif.iteritems(): attribs = [attributs.attrify(val, key, self) for val in values] for attribut in attribs: if attribut.unique: try: self.lockholder.addlock(key, str(attribut), Id=lockId) except ldap_locks.LdapLockedByMySelf: # On vient juste d'acquérir le lock dans _find_id, ça n'est pas grâve pass return objets.new_cransldapobject(self, dn, 'rw', uldif, lockId=lockId) except ldap_locks.LockError: # On supprime seulement les locks que l'on vient de poser self.lockholder.purge(lockId) raise def _find_id(self, attr, realm=None, lockId=None): '''Trouve un id libre. Si une plage est fournie, cherche l'id dans celle-ci, sinon, prend le plus élevé possible.''' if lockId is None: raise ValueError("lockId ne devrait pas être à None") plage = None # On récupère la plage des ids if realm != None: plage = itertools.chain(*[xrange(a,b+1) for (a,b) in config.rid_primaires[realm]]) # Si plage vaut None, on veux un id strictement croissant if plage is None: # On essaye de récupérer le dernier id si on l'a déjà vu passer try: last_id = open('/tmp/lc_ldap_lastid_%s_%s' % (attr, os.getuid())).read().strip() except IOError: last_id = 0 # On récupère tous les id plus grand que le dernier que l'on connait res = self.search_s(variables.base_dn, ldap.SCOPE_SUBTREE, '%s>=%s' % (attr, last_id), attrlist = [attr]) # Si jamais id n'a pas de methode ORDERING, on récupère une liste vide et on fallback en récupérant tous les id (c'est lent) if res == []: res = self.search_s(variables.base_dn, ldap.SCOPE_SUBTREE, '%s=*' % attr, attrlist = [attr]) else: # On récupère tous les id res = self.search_s(variables.base_dn, ldap.SCOPE_SUBTREE, '%s=*' % attr, attrlist = [attr]) if plage != None: # On extrait seulement les valeurs des id qui nous intêressent, dans un set # car on n'a pas besoin de trier et que et que i in set c'est O(1) contre O(n) pour les list nonfree = set(int(r[1].get(attr)[0]) for r in res if r[1].get(attr)) for i in plage: if i in nonfree: continue else: # On crée l'attribut associé, pour parser sa valeur. my_id = attributs.attrify(unicode(i), attr, self, None) if my_id.value != i: continue else: try: self.lockholder.addlock(attr, str(i), lockId) break except ldap_locks.LockError: continue else: raise EnvironmentError('Aucun %s libre dans la plage [%d, %d]' % (attr, plage[0], i)) else: # On extrait seulement les valeurs des id qui nous intêressent dans une liste nonfree = [ int(r[1].get(attr)[0]) for r in res if r[1].get(attr) ] # On trie pour récupérer le dernier nonfree.sort() try: last_id = nonfree[-1] except IndexError: last_id = 0 # On écrit le nouveau dernier id connu f=os.open('/tmp/lc_ldap_lastid_%s_%s' % (attr, os.getuid()), os.O_WRONLY | os.O_CREAT, 0600) os.write(f, str(last_id)) os.close(f) i = last_id + 1 while True: try: self.lockholder.addlock(attr, str(i), lockId) break except ldap_locks.LockError: i += 1 return i def _check_parent(self, objdn): """ Teste le rapport entre le dn fourni et self Retourne une liste qui s'ajoutera à la liste des droits """ if objdn.endswith(self.dn) and objdn != self.dn: return [attributs.parent] else: return [] def _check_self(self, objdn): """ Teste si le dn fourni est celui de self. Retourne une liste qui s'ajoutera à la liste des droits """ if objdn == self.dn: return [attributs.soi] else: return []