#!/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 sys import re import ldap ## import locaux import crans_utils import attributs 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(): uldif[attr] = [ unicode(val, 'utf-8') 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 ressuscite(self, ldif_file, login=None): if login is None: login = self.current_login 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()] dn = ldif['dn'][0] del(ldif['dn']) try: if self.search(dn=dn): raise ValueError ('objet existant: %s' % dn) except ldap.NO_SUCH_OBJECT: pass obj = objets.new_cransldapobject(self, dn, mode='rw', uldif=ldif_to_uldif(ldif)) # On vérifie que les attibuts uniques que l'on veut réssuciter # ne sont pas déjà dans ldap. ### TODO ### S'il existent déjà, traiter au cas par cas, ### par exemple, remettre l'ip/rid en automatique for attr in obj.attrs.keys(): for attribut in obj[attr]: attribut.check_uniqueness([]) 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 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 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é""" # 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) # 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))]) # La machine peut-elle avoir une ipv4 ? if 'v6' not in realm: uldif['ipHostNumber'] = [ unicode(crans_utils.ip4_of_rid(int(rid[0]))) ] uldif['ip6HostNumber'] = [ unicode(crans_utils.ip6_of_mac(uldif['macAddress'][0], int(rid[0]))) ] # Mid uldif['mid'] = [ unicode(self._find_id('mid')) ] # Tout doit disparaître !! machine = self._create_entity('mid=%s,%s' % (uldif['mid'][0], parent), uldif) if machine.may_be(variables.created, self.droits + self._check_parent(machine.dn)): return machine else: raise EnvironmentError("Vous n'avez pas le droit de créer cette machine.") def newAdherent(self, aldif): """Crée un nouvel adhérent""" uldif = copy.deepcopy(aldif) aid = uldif.setdefault('aid', [ unicode(self._find_id('aid')) ]) uldif['objectClass'] = [u'adherent'] adherent = self._create_entity('aid=%s,%s' % (aid[0], variables.base_dn), uldif) if adherent.may_be(variables.created, self.droits): return adherent else: raise EnvironmentError("Vous n'avez pas le droit de créer cet adhérent.") def newClub(self, cldif): """Crée un nouveau club""" uldif = copy.deepcopy(cldif) cid = uldif.setdefault('cid', [ unicode(self._find_id('cid')) ]) uldif['objectClass'] = [u'club'] club = self._create_entity('cid=%s,%s' % (cid[0], variables.base_dn), uldif) if club.may_be(variables.created, self.droits): return club else: raise EnvironmentError("Vous n'avez pas le droit de créer cet adhérent.") def newFacture(self, parent, fldif): """Crée une nouvelle facture --Non implémenté !""" uldif = copy.deepcopy(fldif) # fid uldif['fid'] = [ unicode(self._find_id('fid')) ] uldif['objectClass'] = [u'facture'] facture = self._create_entity('fid=%s,%s' % (uldif['fid'][0], parent), uldif) if facture.may_be(variables.created, self.droits + self._check_parent(facture.dn)): return facture else: raise EnvironmentError("Vous n'avez pas le droit de créer cette facture.") def _create_entity(self, dn, uldif): '''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. for key, values in uldif.iteritems(): attribs = [attributs.attrify(val, key, self) for val in values] for attribut in attribs: if attribut.unique: self.lockholder.addlock(key, str(attribut)) try: return objets.new_cransldapobject(self, dn, 'rw', uldif) except ldap_locks.LockError: for key, values in uldif.iteritems(): attribs = [attributs.attrify(val, key, self) for val in values] for attribut in attribs: self.lockholder.removelock(key, str(attribut)) raise def _find_id(self, attr, realm=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(variables.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() plage = None # On récupère la plage des mids if realm != None: plage = itertools.chain(*[xrange(a,b+1) for (a,b) in config.rid_primaires[realm]]) if plage != None: 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)) self.lockholder.removelock(attr, str(i)) break except: continue else: raise EnvironmentError('Aucun %s libre dans la plage [%d, %d]' % (attr, plage[0], i)) else: i = nonfree[-1] + 1 while True: try: self.lockholder.addlock(attr, str(i)) self.lockholder.removelock(attr, str(i)) break except ldap_locks.LockError: i += 1 except Exception: raise 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 []