lc_ldap/lc_ldap.py

1082 lines
45 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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.
from __future__ import with_statement
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
try:
from Levenshtein import jaro
except ImportError:
def jaro(a, b): return 0
sys.path.append('/usr/scripts/gestion')
import config
import crans_utils
import attributs
import ldap_locks
import services
uri = 'ldap://ldap.adm.crans.org/'
base_dn = 'ou=data,dc=crans,dc=org'
log_dn = "cn=log"
admin_dn = "cn=admin,dc=crans,dc=org"
invite_dn = 'ou=invites,ou=data,dc=crans,dc=org'
# 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)
current_user = os.getenv("SUDO_USER") or os.getenv("USER")
# Quand on a besoin du fichier de secrets
def import_secrets():
"""Importe le fichier de secrets"""
if not "/etc/crans/secrets/" in sys.path:
sys.path.append("/etc/crans/secrets/")
import secrets
return secrets
# Champs à ignorer dans l'historique
HIST_IGNORE_FIELDS = ["modifiersName", "entryCSN", "modifyTimestamp", "historique"]
def escape(chaine):
"""Renvoie une chaîne échapée pour pouvoir la mettre en toute sécurité
dans une requête ldap."""
return ldap.filter.escape_filter_chars(chaine)
# 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
#def ldif_to_cldif(ldif, conn):
# """Transforme un dictionnaire renvoyé par python-ldap, en
# un dictionnaire dont les valeurs sont des instances de Attr
# Lorsqu'on récupère le ldif de la base ldap, on n'a pas besoin
# de faire de tests...
# """
# cldif = {}
# for attr, vals in ldif.items():
# cldif[attr] = [ attributs.attrify(val, attr, conn, ldif) for val in vals]
# 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 ]
return ldif
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=uri, test=False):
"""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 ``test`` est à ``True``, on se connecte à la base de test sur ``vo``.
"""
if test:
uri = "ldapi:///"
ldap.ldapobject.LDAPObject.__init__(self, uri)
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)
# Si on a un dn, on se connecte avec à la base ldap sinon on s'y
# connecte en anonyme
if dn:
#secrets = import_secrets()
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 == admin_dn:
self.droits += [attributs.nounou]
else:
self.conn = self.simple_bind_s()
self.dn = None
self.droits = []
def ressuscite(self, ldif_file, login=current_user):
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 = new_cransldapobject(self, dn, mode='rw', ldif=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):
"""Cherche le dn à partir de l'username."""
self._first_bind()
res = self.search_s(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 _first_bind(self):
"""Premier bind, avant de connaître le vrai dn."""
secrets = import_secrets()
self.simple_bind_s(who=secrets.ldap_readonly_auth_dn, cred=secrets.ldap_readonly_password)
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
:py:class:`CransLdapObject`. 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, ldif))
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(base_dn, scope=2):
# On crée les listes des machines et propriétaires
if dn.startswith('mid='): # les machines
m = new_cransldapobject(self, dn, mode, ldif = 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 == base_dn) and not parent.has_key(dn):
parent[dn] = new_cransldapobject(self, dn, mode, ldif = 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, ldif, login=None):
"""Crée une nouvelle machine: ``realm`` peut être:
fil, fil-v6, wifi, wifi-v6, adm, gratuit, personnel-ens, special
--Partiellement implémenté"""
#adm, serveurs, bornes, wifi, adherents, gratuit ou personnel-ens"""
owner = self.search('objectClass=*', dn=parent, scope=0)[0]
if realm in ["adm", "serveurs", "serveurs-v6", "adm-v6"]:
ldif['objectClass'] = ['machineCrans']
assert isinstance(owner, AssociationCrans)
elif realm == "bornes":
ldif['objectClass'] = ['borneWifi']
assert isinstance(owner, AssociationCrans)
elif realm in ["wifi", "wifi-v6"]:
ldif['objectClass'] = ['machineWifi']
assert isinstance(owner, adherent) or isinstance(owner, club)
elif realm in ["adherents", "fil-adherents", "fil-v6", "personnel-ens"]:
ldif['objectClass'] = ['machineFixe']
assert isinstance(owner, adherent) or isinstance(owner, club)
else: raise ValueError("Realm inconnu: %r" % realm)
# On récupère la plage des mids
plage = xrange( *(config.rid[realm]))
# On récupère le premier id libre dans la plages s'il n'est pas
# déjà précisé dans le ldiff
rid = ldif.setdefault('rid', [ str(self._find_id('rid', plage)) ])
# La machine peut-elle avoir une ipv4 ?
if 'v6' not in realm:
ldif['ipHostNumber'] = [ str(crans_utils.ip4_of_rid(int(rid[0]))) ]
ldif['ip6HostNumber'] = [ str(crans_utils.ip6_of_mac(ldif['macAddress'][0], int(rid[0]))) ]
# Mid
ldif['mid'] = [ str(self._find_id('mid')) ]
# Tout doit disparaître !!
machine = self._create_entity('mid=%s,%s' % (ldif['mid'][0], parent), ldif)
login = login or current_user
machine.history_add(login, "inscription")
if machine.may_be(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, ldif):
"""Crée un nouvel adhérent"""
aid = ldif.setdefault('aid', [ str(self._find_id('aid')) ])
ldif['objectClass'] = ['adherent']
adherent = self._create_entity('aid=%s,%s' % (aid[0], base_dn), ldif)
if adherent.may_be(created, self.droits):
return adherent
else:
raise EnvironmentError("Vous n'avez pas le droit de créer cet adhérent.")
def newClub(self, ldif):
"""Crée un nouveau club"""
cid = ldif.setdefault('cid', [ str(self._find_id('cid')) ])
ldif['objectClass'] = ['club']
club = self._create_entity('cid=%s,%s' % (cid[0], base_dn), ldif)
if club.may_be(created, self.droits):
return club
else:
raise EnvironmentError("Vous n'avez pas le droit de créer cet adhérent.")
def newFacture(self, ldif):
"""Crée une nouvelle facture
--Non implémenté !"""
raise NotImplementedError()
def _create_entity(self, dn, ldif):
'''Crée une nouvelle entité ldap avec le dn ``dn`` et les
attributs de ``ldif``. Attention, ldif doit contenir des
données encodées.'''
# Conversion en cldif pour vérification des valeurs
cldif = attributs.AttrsDict(self, ldif, Parent=None)
# Conversion en ascii
### On a besoin du parent pour instancier les attributs
# data = cldif_to_ldif(cldif)
# Renvoi du CransLdapObject
data = ldif
return new_cransldapobject(self, dn, 'rw', data)
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()
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:
break
else:
raise EnvironmentError('Aucun %s libre dans la plage [%d, %d]' %
(attr, plage[0], i))
else:
i = nonfree[-1]+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 []
class lc_ldap_test(lc_ldap):
"""Connexion LDAP à la base de tests"""
def __init__(self, *args, **kwargs):
# On impose le serveur
kwargs["uri"] = 'ldap://vo.adm.crans.org'
# On pense à laisser la possibilité de se connecter par username ou dn…
if not kwargs.has_key("user"):
# … mais si rien n'est spécifié, on fournit le dn par défaut
kwargs.setdefault("dn", 'cn=admin,dc=crans,dc=org')
# Le mot de passe de la base de test
kwargs.setdefault("cred", '75bdb64f32')
super(lc_ldap_test, self).__init__(*args, **kwargs)
def _first_bind(self):
"""Sur la base de test, on peut lookup en anonyme"""
self.simple_bind_s()
class lc_ldap_admin(lc_ldap):
"""Connexion LDAP à la vraie base, en admin.
Possible seulement si on peut lire secrets.py"""
def __init__(self):
secrets = import_secrets()
super(lc_ldap_admin, self).__init__(uri='ldap://ldap.adm.crans.org/', dn=secrets.ldap_auth_dn, cred=secrets.ldap_password)
class lc_ldap_readonly(lc_ldap):
"""Connexion LDAP à la vraie base, en readonly.
Possible seulement si on peut lire secrets.py"""
def __init__(self):
secrets = import_secrets()
super(lc_ldap_readonly, self).__init__(uri='ldap://ldap.adm.crans.org/', dn=secrets.ldap_readonly_auth_dn, cred=secrets.ldap_readonly_password)
class lc_ldap_local(lc_ldap):
"""Connexion LDAP en lecture seule sur la base locale.
L'idée est que les machines avec un réplica bossent
avec elles-mêmes pour la lecture, pas avec vert.
Attention, les accès internes en lecture seule
ou avec une socket ldapi semblent moins prioritaires
qu'avec cn=admin. Ne pas utiliser cette fonction
si vous souhaitez faire beaucoup de recherches
indépendantes (c'est le temps d'accès à la socket
qui est problématique)"""
def __init__(self):
if os.path.exists('/var/run/slapd/ldapi'):
ro_uri = 'ldapi://%2fvar%2frun%2fslapd%2fldapi/'
auth_dn = auth_pw = ""
elif os.path.exists('/var/run/ldapi'):
ro_uri = 'ldapi://%2fvar%2frun%2fldapi/'
auth_dn = auth_pw = ""
else:
secrets = import_secrets()
ro_uri = 'ldap://127.0.0.1'
auth_dn = secrets.ldap_readonly_auth_dn
auth_pw = secrets.ldap_readonly_password
super(lc_ldap_local, self).__init__(uri=ro_uri, dn=auth_dn, cred=auth_pw)
def new_cransldapobject(conn, dn, mode='ro', ldif = None):
"""Crée un objet :py:class:`CransLdap` en utilisant la classe correspondant à
l'``objectClass`` du ``ldif``
--pour usage interne à la libraire uniquement !"""
classe = None
if dn == base_dn:
classe = AssociationCrans
elif dn == invite_dn:
classe = BaseInvites
elif ldif:
classe = globals()[ldif['objectClass'][0]]
else:
res = conn.search_s(dn, 0)
if not res:
raise ValueError ('objet inexistant: %s' % dn)
_, attrs = res[0]
classe = globals()[attrs['objectClass'][0]]
return classe(conn, dn, mode, ldif)
class CransLdapObject(object):
"""Classe de base des objets :py:class:`CransLdap`.
Cette classe ne devrait pas être utilisée directement."""
""" Qui peut faire quoi ? """
can_be_by = { created: [attributs.nounou],
modified: [attributs.nounou],
deleted: [attributs.nounou],
}
attribs = []
def __init__(self, conn, dn, mode='ro', ldif = None):
'''
Créée une instance d'un objet Crans (machine, adhérent,
etc...) à ce ``dn``, si ``ldif`` est précisé, n'effectue pas de
recherche dans la base ldap.
'''
if not isinstance(conn, lc_ldap):
raise TypeError("conn doit être une instance de lc_ldap")
self.conn = conn
self.attrs = attributs.AttrsDict(conn, Parent=self) # Contient un dico ldif qui doit représenter ce qui
# est dans la base. On attrify paresseusement au moment où on utilise un attribut
self._modifs = None # C'est là qu'on met les modifications
self.dn = dn
orig = {}
if ldif:
if dn != base_dn: # new_cransldapobject ne donne pas de ldif formaté et utilise un ldif non formaté, donc on formate
self.attrs = attributs.AttrsDict(self.conn, ldif, Parent=self)
else:
self.attrs = ldif
self._modifs = attributs.AttrsDict(self.conn, ldif, Parent=self)
orig = ldif
elif dn != base_dn:
res = self.conn.search_s(dn, 0)
if not res:
raise ValueError ('objet inexistant: %s' % dn)
self.dn, res_attrs = res[0]
# L'objet sortant de la base ldap, on ne fait pas de vérifications sur
# l'état des données.
self.attrs = attributs.AttrsDict(self.conn, res_attrs, Parent=self)
# Pour test en cas de mode w ou rw
orig = res[0][1]
self._modifs = attributs.AttrsDict(self.conn, res[0][1], Parent=self)
if mode in ['w', 'rw']:
if not self.may_be(modified, self.conn.droits + self.conn._check_parent(dn) + self.conn._check_self(dn)):
raise EnvironmentError("Vous n'avez pas le droit de modifier cet objet.")
self.mode = mode
# 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)
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"""
# return self.attribs
# attribs = property(_get_fields)
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"""
assert isinstance(login, str) or isinstance(login, unicode)
assert isinstance(chain, str) or isinstance(chain, unicode)
new_line = "%s, %s : %s" % (time.strftime("%d/%m/%Y %H:%M"), login, chain)
# Attention, le __setitem__ est surchargé, mais pas .append sur l'historique
self["historique"] = self.get("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."""
objet = self.__class__.__name__
for attribut in self.attribs:
if not attribut.optional:
nom_attr = attribut.__name__
if len(self._modifs.get(nom_attr, [])) <= 0:
raise attributs.OptionalError("L'objet %s que vous créez doit posséder au moins un attribut %s" % (objet, nom_attr))
# Création de la requête LDAP
modlist = addModlist(cldif_to_ldif(self._modifs))
# Requête LDAP de création de l'objet
self.conn.add_s(self.dn, modlist)
services.services_to_restart(self.conn, {}, self._modifs)
def bury(self, comm, login):
self.history_add(login, u"destruction (%s)" % comm)
self.save()
#On produit un ldif
ldif=u"dn: %s\n" % self.dn
for key in self.attrs.keys():
for value in self.attrs[key]:
ldif+=u"%s: %s\n" % (key, value)
import datetime
file="%s %s" % (datetime.datetime.now(), self.dn)
f = open('/home/cimetiere_lc/%s/%s' % (self['objectClass'][0],file.replace(' ','_')), 'w')
f.write(ldif.encode("UTF-8"))
f.close()
def delete(self, comm="", login=current_user):
if self.mode not in ['w', 'rw']:
raise EnvironmentError("Objet en lecture seule, réessayer en lecture/écriture")
if not self.may_be(deleted, self.conn.droits):
raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn)
self.bury(comm, login)
self.conn.delete_s(self.dn)
services.services_to_restart(self.conn, self.attrs, {})
def save(self):
"""Sauvegarde dans la base les modifications apportées à l'objet.
Interne: Vérifie que ``self._modifs`` contient des valeurs correctes et
enregistre les modifications."""
if self.mode not in ['w', 'rw']:
raise EnvironmentError("Objet en lecture seule, réessayer en lecture/écriture")
objet = self.__class__.__name__
for attribut in self.attribs:
if not attribut.optional:
nom_attr = attribut.__name__
if len(self._modifs.get(nom_attr, [])) <= 0:
raise attributs.OptionalError("L'objet %s que vous créez doit posséder au moins un attribut %s" % (objet, nom_attr))
# On récupère la liste des modifications
modlist = self.get_modlist()
try:
self.conn.modify_s(self.dn, modlist)
except:
raise EnvironmentError("Impossible de modifier l'objet, peut-être n'existe-t-il pas ?")
# On programme le redémarrage des services
services.services_to_restart(self.conn, self.attrs, self._modifs)
# Vérification des modifications
self.attrs = attributs.AttrsDict(self.conn, self.conn.search_s(self.dn, 0)[0][1], Parent=self)
differences = []
# On fait les différences entre les deux dicos
for attr in set(self.attrs.keys()).union(set(self._modifs.keys())):
exp_vals = set([unicode(i) for i in self.attrs.get(attr,[])])
new_vals = set([unicode(i) for i in self._modifs.get(attr,[])])
if exp_vals != new_vals:
differences.append({"missing": exp_vals - new_vals, "having": new_vals - exp_vals})
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, liste):
"""Teste si liste peut faire ce qui est dans what, pour
what élément de {create, delete, modify}.
On passe une liste de droits plutôt que l'objet car il faut ajouter
les droits soi et parent.
Retourne un booléen
"""
if set(liste).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
ldif = cldif_to_ldif(self._modifs)
orig_ldif = cldif_to_ldif(self.attrs)
return modifyModlist(orig_ldif, ldif)
def get(self, attr, default):
"""Renvoie l'attribut demandé ou default si introuvable"""
try:
return self[attr]
except KeyError:
return default
def __getitem__(self, attr):
if self.mode in [ 'w', 'rw' ]:
return [ v for v in self._modifs[attr] ]
elif self.attrs.has_key(attr):
return [ v for v in self.attrs[attr] ]
elif self.has_key(attr):
return []
raise KeyError(attr)
def has_key(self, attr):
"""Est-ce que notre objet a l'attribut en question ?"""
return attr in [attrib.__name__ for attrib in self.attribs]
def __setitem__(self, attr, values):
"""Permet d'affecter des valeurs à l'objet comme
s'il était un dictionnaire."""
# Quand on est pas en mode d'écriture, ça plante.
if self.mode not in ['w', 'rw']:
raise ValueError("Objet en lecture seule")
if not self.has_key(attr):
raise ValueError("L'objet que vous modifiez n'a pas d'attribut %s" % (attr))
# Les valeurs sont nécessairement stockées en liste
if not isinstance(values, list):
values = [ values ]
# On génére une liste des attributs, le dictionnaire ldif
# sert à permettre les vérifications de cardinalité
# (on peut pas utiliser self._modifs, car il ne faut
# faire le changement que si on peut)
attrs_before_verif = [ attributs.attrify(val, attr, self.conn, Parent=self) for val in values ]
if attr in self.attrs.keys():
for attribut in attrs_before_verif:
attribut.check_uniqueness([str(content) for content in self.attrs[attr]])
# On groupe les attributs précédents, et les nouveaux
mixed_attrs = attrs_before_verif + self.attrs[attr]
else:
mixed_attrs = attrs_before_verif
# Si c'est vide, on fait pas de vérifs, on avait une liste
# vide avant, puis on en a une nouvelle après.
if mixed_attrs:
# Tests de droits.
if not mixed_attrs[0].is_modifiable(self.conn.droits + self.conn._check_parent(self.dn) + self.conn._check_self(self.dn)):
raise EnvironmentError("Vous ne pouvez pas toucher aux attributs de type %r." % (attr))
self._modifs[attr] = attrs_before_verif
def search_historique(self, ign_fields=HIST_IGNORE_FIELDS):
u"""Récupère l'historique
l'argument optionnel ign_fields contient la liste des champs
à ignorer, HIST_IGNORE_FIELDS par défaut
Renvoie une liste de lignes de texte."""
res = self.conn.search_s(log_dn, ldap.SCOPE_SUBTREE, 'reqDN=%s' % self.dn)
res.sort(key=(lambda a: a[1]['reqEnd'][0]))
out = []
for cn, attrs in res:
date = crans_utils.format_ldap_time(attrs['reqEnd'][0])
author = attrs['reqAuthzID'][0]
if author == "cn=admin,dc=crans,dc=org":
author = u"respbats"
else:
author = author.split(",", 1)[0]
res = self.conn.search(author, scope=ldap.SCOPE_ONELEVEL)
if res != []:
author = res[0].compte()
if attrs['reqType'][0] == deleted:
out.append(u"%s : [%s] Suppression" % (date, author))
elif attrs['reqType'][0] == modified:
fields = {}
for mod in attrs['reqMod']:
mod = mod.decode('utf-8')
field, change = mod.split(':', 1)
if field not in ign_fields:
if field in fields:
fields[field].append(change)
else:
fields[field] = [change]
mod_list = []
for field in fields:
mods = fields[field]
mod_list.append(u"%s %s" %(field, ", ".join(mods)))
if mod_list != []:
out.append(u"%s : [%s] %s" % (date, author, u" ; ".join(mod_list)))
return out
def blacklist_actif(self):
"""Renvoie la liste des blacklistes actives sur l'entité
Améliorations possibles:
- Proposer de filtrer les blacklistes avec un arg supplémentaire ?
- Vérifier les blacklistes des machines pour les adhérents ?
"""
blacklist_liste=[]
# blacklistes virtuelle si on est un adhérent pour carte étudiant et chambre invalides
if self.__class__.__name__ == "adherent" and self.paiement_ok():
if not config.periode_transitoire and config.bl_carte_et_actif and not self.carte_ok() and not self.sursis_carte():
bl = attributs.blacklist(u'%s$%s$%s$%s' % ('-', '-', 'carte_etudiant', ''), {}, self.conn)
blacklist_liste.append(bl)
if self['chbre'][0].value == '????':
bl = attributs.blacklist(u'%s$%s$%s$%s' % ('-', '-', 'chambre_invalide', ''), {}, self.conn)
blacklist_liste.append(bl)
attrs = (self.attrs if self.mode not in ["w", "rw"] else self._modifs)
blacklist_liste.extend(filter((lambda bl: bl.is_actif()), attrs.get("blacklist",[])))
return blacklist_liste
def blacklist(self, sanction, commentaire, debut="now", fin = '-'):
u"""
Blacklistage de la ou de toutes la machines du propriétaire
* debut et fin sont le nombre de secondes depuis epoch
* pour un début ou fin immédiate mettre now
* pour une fin indéterminée mettre '-'
Les données sont stockées dans la base sous la forme :
debut$fin$sanction$commentaire
"""
if debut == 'now':
debut = int(time.time())
if fin == 'now':
fin = int(time.time())
bl = attributs.blacklist(u'%s$%s$%s$%s' % (debut, fin, sanction, commentaire), {}, self.conn)
self._modifs.setdefault('blacklist', []).append(bl)
class proprio(CransLdapObject):
u""" Un propriétaire de machine (adhérent, club…) """
can_be_by = { created: [attributs.nounou, attributs.bureau, attributs.cableur],
modified: [attributs.nounou, attributs.bureau, attributs.soi, attributs.cableur],
deleted: [attributs.nounou, attributs.bureau],
}
attribs = [attributs.nom, attributs.chbre, attributs.paiement, attributs.info, attributs.blacklist, attributs.controle, attributs.historique]
def __init__(self, conn, dn, mode='ro', ldif = None, machines=[]):
super(proprio, self).__init__(conn, dn, mode, ldif)
self._machines = machines
def sursis_carte(self):
for h in self['historique'][::-1]:
x=re.match("(.*),.* : .*(paiement\+%s|inscription).*" % config.ann_scol,h.value)
if x != None:
return ((time.time()-time.mktime(time.strptime(x.group(1),'%d/%m/%Y %H:%M')))<=config.sursis_carte)
return False
def paiement_ok(self):
u"""Renvoie si le propriétaire a payé pour l'année en cours"""
if self.dn == base_dn:
return True
bool_paiement = False
try:
for paiement in self['paiement']:
if paiement.value == config.ann_scol:
bool_paiement = True
break
# Pour la période transitoire année précédente ok
if config.periode_transitoire and paiement.value == config.ann_scol -1:
bool_paiement = True
break
except KeyError:
pass
# Doit-on bloquer en cas de manque de la carte d'etudiant ?
# (si période transitoire on ne bloque dans aucun cas)
if config.bl_carte_et_definitif and not 'club' in map(lambda x:x.value,self["objectClass"]):
bool_carte = False
try:
for carte in self['carteEtudiant']:
if carte.value == config.ann_scol:
bool_carte = True
except KeyError:
pass
# Si inscrit depuis moins de config.sursis_carte, on laisse un sursis
if not bool_carte and self.sursis_carte():
bool_carte = True
return bool_carte and bool_paiement
return bool_paiement
def carte_ok(self):
u"""Renvoie si le propriétaire a donné sa carte pour l'année en cours"""
if not self.dn == base_dn and config.bl_carte_et_actif and not 'club' in map(lambda x:x.value,self["objectClass"]):
bool_carte = False
try:
for carte in self['carteEtudiant']:
if carte.value == config.ann_scol:
bool_carte = True
except KeyError:
pass
return bool_carte
return True
def update_solde(self, diff, comment=u"", login=None):
"""Modifie le solde du proprio. diff peut être négatif ou positif."""
login = login or current_user
assert isinstance(diff, int) or isinstance(diff, float)
assert isinstance(comment, unicode)
solde = float(self["solde"][0].value)
new_solde = solde + diff
# On vérifie qu'on ne dépasse par le découvert autorisé
if new_solde < config.impression.decouvert:
raise ValueError(u"Solde minimal atteint, opération non effectuée.")
transaction = u"credit" if diff >=0 else u"debit"
new_solde = u"%.2f" % new_solde
self.history_add(login, u"%s %.2f Euros [%s]" % (transaction, abs(diff), comment))
self["solde"] = new_solde
def machines(self):
"""Renvoie la liste des machines"""
if not self._machines:
self._machines = self.conn.search('mid=*', dn = self.dn, scope = 1, mode=self.mode)
for m in self._machines:
m._proprio = self
return self._machines
def delete(self, comm="", login=current_user):
if self.mode not in ['w', 'rw']:
raise EnvironmentError("Objet en lecture seule, réessayer en lecture/écriture")
if not self.may_be(deleted, self.conn.droits):
raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn)
for machine in self.machines():
machine.delete(comm, login)
self.bury(comm, login)
self.conn.delete_s(self.dn)
services.services_to_restart(self.conn, self.attrs, {})
class machine(CransLdapObject):
u""" Une machine """
can_be_by = { created: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent],
modified: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent],
deleted: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent],
}
attribs = [attributs.mid, attributs.macAddress, attributs.host,
attributs.rid, attributs.info, attributs.blacklist, attributs.hostAlias,
attributs.exempt, attributs.portTCPout, attributs.portTCPin,
attributs.portUDPout, attributs.portUDPin, attributs.sshFingerprint,
attributs.ipHostNumber, attributs.ip6HostNumber, attributs.historique,
attributs.dnsIpv6, attributs.machineAlias]
def __init__(self, conn, dn, mode='ro', ldif = None):
super(machine, self).__init__(conn, dn, mode, ldif)
self._proprio = None
def proprio(self):
u"""Renvoie le propriétaire de la machine"""
parent_dn = self.dn.split(',', 1)[1]
if not self._proprio:
self._proprio = new_cransldapobject(self.conn, parent_dn, self.mode)
return self._proprio
def blacklist_actif(self):
u"""Renvoie la liste des blacklistes actives sur la machine et le proprio
Améliorations possibles:
- Proposer de filtrer les blacklistes avec un arg supplémentaire ?
- Vérifier les blacklistes des machines pour les adhérents ?"""
black=self.proprio().blacklist_actif()
attrs = (self.attrs if self.mode not in ["w", "rw"] else self._modifs)
black.extend(filter((lambda bl: bl.is_actif()), attrs.get("blacklist",[])))
return black
class AssociationCrans(proprio):
u""" Association crans (propriétaire particulier)."""
def delete(self, comm, login):
raise EnvironmentError("Casser le Crans ? Hum…")
pass
class BaseInvites(proprio):
u"""Un artefact de la base ldap"""
def delete(self, comm, login):
raise EnvironmentError("Les pauvres invites")
pass
class adherent(proprio):
u"""Adhérent crans."""
attribs = proprio.attribs + [attributs.aid, attributs.prenom, attributs.tel,
attributs.mail, attributs.mailInvalide, attributs.charteMA,
attributs.derniereConnexion, attributs.gpgFingerprint,
attributs.carteEtudiant, attributs.droits, attributs.etudes,
attributs.postalAddress, attributs.mailExt, attributs.compteWiki]
def __init__(self, conn, dn, mode='ro', ldif = None):
super(adherent, self).__init__(conn, dn, mode, ldif)
if u'cransAccount' in [ str(o) for o in self['objectClass']]:
self.attribs = self.attribs + [attributs.uid, attributs.canonicalAlias, attributs.solde,
attributs.contourneGreylist, attributs.derniereConnexion,
attributs.homepageAlias, attributs.mailAlias, attributs.loginShell ]
def compte(self, login = None, uidNumber=0, hash_pass = '', shell=config.login_shell):
u"""Renvoie le nom du compte crans. S'il n'existe pas, et que uid
est précisé, le crée."""
if u'posixAccount' in [str(o) for o in self.attrs['objectClass']]:
return self.attrs['uid'][0]
elif login:
fn = crans_utils.strip_accents(unicode(self.attrs['prenom'][0]).capitalize())
ln = crans_utils.strip_accents(unicode(self.attrs['nom'][0]).capitalize())
login = crans_utils.strip_accents(login).lower()
if jaro(ln.lower(), login) < 0.75 and jaro(fn.lower() + ' ' + ln.lower(), login) < 0.75:
raise ValueError("Le login est trop différent du nom",
login, self.attrs['nom'][0])
if not re.match('^[a-z][-a-z]{1,15}$', login):
raise ValueError("Le login a entre 2 et 16 lettres, il peut contenir (pas au début) des - ")
if crans_utils.mailexist(login):
raise ValueError("Login existant ou correspondant à un alias mail.")
home = u'/home/' + login
if os.path.exists(home):
raise ValueError('Création du compte impossible : home existant')
if os.path.exists("/var/mail/" + login):
raise ValueError('Création du compte impossible : /var/mail/%s existant' % login)
self._modifs['homeDirectory'] = [home]
self._modifs['mail'] = [login]
self._modifs['uid' ] = [login]
calias = crans_utils.strip_spaces(fn) + u'.' + crans_utils.strip_spaces(ln)
if crans_utils.mailexist(calias):
calias = login
self._modifs['canonicalAlias'] = [calias]
self._modifs['objectClass'] = [u'adherent', u'cransAccount', u'posixAccount', u'shadowAccount']
self._modifs['cn'] = [ fn + u' ' + ln ]
self._modifs['loginShell'] = [unicode(shell)]
self._modifs['userPassword'] = [unicode(hash_pass)]
if uidNumber:
if self.conn.search('(uidNumber=%s)' % uidNumber):
raise ValueError(u'uidNumber pris')
else:
pool_uid = range(1001, 9999)
random.shuffle(pool_uid)
while len(pool_uid) > 0:
uidNumber = pool_uid.pop() # On choisit un uid
if not self.conn.search('(uidNumber=%s)' % uidNumber):
break
if not len(pool_uid):
raise ValueError("Plus d'uid disponibles !")
## try:
## self.lock('uidNumber', str(uidNumber))
## except:
## # Quelqu'un nous a piqué l'uid que l'on venait de choisir !
## return self.compte(login, uidNumber, hash_pass, shell)
self._modifs['uidNumber'] = [unicode(uidNumber)]
self._modifs['gidNumber'] = [unicode(config.gid)]
self._modifs['gecos'] = [self._modifs['cn'][0] + u',,,']
self.save()
else:
raise EnvironmentError("L'adhérent n'a pas de compte crans")
class club(proprio):
u"""Club crans"""
attribs = proprio.attribs + [attributs.cid, attributs.responsable, attributs.imprimeurClub]
class machineFixe(machine):
u"""Machine fixe"""
pass
class machineWifi(machine):
u"""Machine wifi"""
attribs = machine.attribs + [attributs.ipsec]
def set_ipv4(self, login=None):
u"""Définie une ipv4 à la machine si elle n'est possède pas déjà une."""
if not 'ipHostNumber' in self.attrs.keys() or not self['ipHostNumber']:
rid = self['rid']=[ unicode(self.conn._find_id('rid', range(config.rid['wifi'][0], config.rid['wifi'][1]))) ]
ip = self['ipHostNumber'] = [ unicode(crans_utils.ip4_of_rid(int(rid[0]))) ]
login = login or current_user
self.history_add(login, u"rid")
self.history_add(login, u"ipHostNumber (N/A -> %s)" % ip[0])
self.save()
from gen_confs.dhcpd_new import dydhcp
dhcp=dydhcp()
dhcp.add_host(str(self['ipHostNumber'][0]), str(self['macAddress'][0]), str(self['host'][0]))
class machineCrans(machine):
can_be_by = { created: [attributs.nounou],
modified: [attributs.nounou],
deleted: [attributs.nounou],
}
attribs = machine.attribs + [attributs.prise, attributs.nombrePrises]
class borneWifi(machine):
can_be_by = { created: [attributs.nounou],
modified: [attributs.nounou],
deleted: [attributs.nounou],
}
attribs = machine.attribs + [attributs.canal, attributs.puissance, attributs.hotspot,
attributs.prise, attributs.positionBorne, attributs.nvram]
class facture(CransLdapObject):
can_be_by = { created: [attributs.nounou, attributs.bureau, attributs.cableur],
modified: [attributs.nounou, attributs.bureau, attributs.cableur],
deleted: [attributs.nounou, attributs.bureau, attributs.cableur],
}
attribs = [attributs.fid, attributs.modePaiement, attributs.recuPaiement]
class service(CransLdapObject):
pass
class lock(CransLdapObject):
pass