lc_ldap/lc_ldap.py

555 lines
24 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# LC_LDAP.PY-- LightWeight CransLdap
#
# Copyright (C) 2010-2013 Cr@ns <roots@crans.org>
# Authors: Antoine Durand-Gasselin <adg@crans.org>
# Nicolas Dandrimont <olasd@crans.org>
# Olivier Iffrig <iffrig@crans.org>
# Valentin Samir <samir@crans.org>
# Daniel Stan <dstan@crans.org>
# Vincent Le Gallic <legallic@crans.org>
# Pierre-Elliott Bécue <becue@crans.org>
#
# 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.
## 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
real_user = self.search(u'(&(uid=%s)(objectClass=cransAccount))' % user)
# Si l'utilisteur existe vraiement, on utilise les droits de cet utilisateur
if real_user:
self.droits = map(unicode, real_user[0]['droits'])
self.dn = real_user[0].dn
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 = unicode(current_user[0]["uid"][0])
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 []