lc_ldap/lc_ldap.py

444 lines
19 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 sys
import re
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():
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 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
try:
if self.search(dn=dn):
for id in ["aid", "mid", "fid", "cid"]:
if dn.startswith("%s=" % id):
ldif[id]=[str(self._find_id(id))]
dn="%s=%s,%s" % (id, ldif['aid'][0], dn.split(',',1)[1])
except ldap.NO_SUCH_OBJECT:
pass
obj = objets.new_cransldapobject(self, dn, mode='rw', uldif=ldif_to_uldif(ldif))
# La vérification des attibuts uniques et de l'existance du dn est faite en appelant create()
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, "", ldif=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
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"""
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 newCertificat(self, parent, xldif):
"""Crée un nouveau certificat x509"""
uldif = copy.deepcopy(xldif)
# xid
uldif['xid'] = [ unicode(self._find_id('xid')) ]
uldif['objectClass'] = [u'baseCert']
baseCert = self._create_entity('xid=%s,%s' % (uldif['xid'][0], parent), uldif)
if baseCert.may_be(variables.created, self.droits + self._check_parent(baseCert.dn)):
return baseCert
else:
raise EnvironmentError("Vous n'avez pas le droit de créer ce certiticat.")
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:
try:
i = nonfree[-1] + 1
except IndexError:
i = 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 []