diff --git a/__init__.py b/__init__.py index 05c1aaf..37840b5 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,17 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +""" +Binding Cr@ns-made pour s'interfacer avec la base LDAP + +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 +""" + from lc_ldap import * diff --git a/attributs.py b/attributs.py index ff785e6..04f07e7 100644 --- a/attributs.py +++ b/attributs.py @@ -1,7 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# ATTRIBUTS.PY-- Description des attributs ldap + +""" Définition des classes permettant d'instancier les attributs LDAP. """ + # # Copyright (C) 2010-2013 Cr@ns # Authors: Antoine Durand-Gasselin @@ -160,6 +162,16 @@ class AttrsDict(dict): def items(self): return [(key, self[key]) for key in self] + + def to_ldif(self): + """ + Transforme le dico en ldif valide au sens openldap. + Ce ldif est celui qui sera transmis à la base. + """ + ldif = {} + for attr, vals in self.items(): + ldif[attr] = [ str(val) for val in vals ] + return ldif class Attr(object): """Objet représentant un attribut. diff --git a/lc_ldap.py b/lc_ldap.py index 11762d8..e5c2fb5 100644 --- a/lc_ldap.py +++ b/lc_ldap.py @@ -35,53 +35,24 @@ # 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 de la lib standard import sys +import re import ldap -import ldap.filter -from ldap.modlist import addModlist, modifyModlist -import re -import netaddr -import datetime -import copy -import time +## import de /usr/scripts/ +if not "/usr/scripts/" in sys.path: + sys.path.append('/usr/scripts/') -try: - from Levenshtein import jaro -except ImportError: - def jaro(a, b): return 0 - -sys.path.append('/usr/scripts/gestion') -import config +import gestion.config as config +## import locaux import crans_utils import attributs +import objets 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' - -# 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"] +import variables # A priori, ldif_to_uldif et ldif_to_cldif sont obsolètes, # du fait de l'apparition de AttrsDict dans attributs.py @@ -95,31 +66,11 @@ def ldif_to_uldif(ldif): 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): + 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 @@ -130,10 +81,7 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): 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). - 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): @@ -141,30 +89,39 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): # Si un username, on récupère le dn associé… if user and not dn: - dn = self.user_to_dn(user) + 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: - #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: + if dn == variables.admin_dn: self.droits += [attributs.nounou] - current_user = self.search("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_user = current_user[0] + # On autorise root à se binder en cn=readonly ou cn=admin, les autres doivent exister dans la base. + if user != 'root' and dn in [variables.admin_dn, variables.readonly_dn]: + current_user = self.search("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_user = current_user[0] else: self.conn = self.simple_bind_s() self.dn = None self.droits = [] + self._username_given = user + + def _get_login(self): + if self._username_given == 'root': + return 'root' + else: + return self.current_user["uid"][0].value + current_login = property(_get_login) def ressuscite(self, ldif_file, login=None): if login is None: - login = self.conn.current_user["uid"][0] + login = self.current_login ldif={} for line in open(ldif_file).readlines(): line = line.split(':',1) @@ -178,7 +135,7 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): raise ValueError ('objet existant: %s' % dn) except ldap.NO_SUCH_OBJECT: pass - obj = new_cransldapobject(self, dn, mode='rw', ldif=ldif) + obj = objets.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 @@ -191,10 +148,12 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): 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) + 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: @@ -203,19 +162,14 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): 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): + def search(self, filterstr='(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``""" 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)) + ret.append(objets.new_cransldapobject(self, dn, mode, ldif)) return ret def allMachinesAdherents(self, mode='ro'): @@ -226,16 +180,16 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): parent = {} machines = {} # (proxying de la base ldap) - for dn, attrs in self.search_s(base_dn, scope=2): + 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 = new_cransldapobject(self, dn, mode, ldif = attrs) + m = objets.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) + 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, ldif = attrs) allmachines = [] for dn,mlist in machines.iteritems(): # on associe propriétaires et machines parent[dn]._machines = mlist @@ -265,25 +219,25 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): fil, fil-v6, wifi, wifi-v6, adm, gratuit, personnel-ens, special --Partiellement implémenté""" if login is None: - login = self.current_user["uid"][0].value + login = self.current_login #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) + assert isinstance(owner, objets.AssociationCrans) elif realm == "bornes": ldif['objectClass'] = ['borneWifi'] - assert isinstance(owner, AssociationCrans) + assert isinstance(owner, objets.AssociationCrans) elif realm in ["wifi", "wifi-v6"]: ldif['objectClass'] = ['machineWifi'] - assert isinstance(owner, adherent) or isinstance(owner, club) + assert isinstance(owner, objets.adherent) or isinstance(owner, objets.club) elif realm in ["adherents", "fil-v6", "personnel-ens"]: ldif['objectClass'] = ['machineFixe'] - assert isinstance(owner, adherent) or isinstance(owner, club) + assert isinstance(owner, objets.adherent) or isinstance(owner, objets.club) else: raise ValueError("Realm inconnu: %r" % realm) @@ -304,7 +258,7 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): # Tout doit disparaître !! machine = self._create_entity('mid=%s,%s' % (ldif['mid'][0], parent), ldif) machine.history_add(login, "inscription") - if machine.may_be(created, self.droits + self._check_parent(machine.dn)): + 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.") @@ -313,8 +267,8 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): """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): + adherent = self._create_entity('aid=%s,%s' % (aid[0], variables.base_dn), ldif) + 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.") @@ -323,8 +277,8 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): """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): + club = self._create_entity('cid=%s,%s' % (cid[0], variables.base_dn), ldif) + 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.") @@ -338,19 +292,12 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): '''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) + return objets.new_cransldapobject(self, dn, 'rw', ldif) 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]) + 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() @@ -395,751 +342,3 @@ class lc_ldap(ldap.ldapobject.LDAPObject, object): 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:`CransLdapObject` en utilisant la classe correspondant à - l'``objectClass`` du ``ldif`` - --pour usage interne à la librairie uniquement !""" - - classe = None - - if dn == base_dn: - classe = AssociationCrans - elif dn == invite_dn: - classe = BaseInvites - elif ldif: - classe = ObjectFactory.get(ldif['objectClass'][0]) - else: - res = conn.search_s(dn, 0) - if not res: - raise ValueError ('objet inexistant: %s' % dn) - _, attrs = res[0] - classe = ObjectFactory.get(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): - """Sauvegarde l'objet dans un fichier dans le cimetière.""" - 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=None): - """Supprime l'objet de la base LDAP. Appelle :py:meth:`CransLdapObject.bury`.""" - if login is None: - login = self.conn.current_user["uid"][0].value - 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 ObjectFactory(object): - """Utilisée pour enregistrer toutes les classes servant à instancier un objet LDAP. - Elle sert à les récupérer à partir de leur nom LDAP. - - Cette classe n'est jamais instanciée. - - """ - _classes = {} - - @classmethod - def register(cls, name, classe): - """Enregistre l'association ``name`` -> ``classe``""" - cls._classes[name] = classe - - @classmethod - def get(cls, name): - """Retourne la classe qui a ``name`` pour ``ldap_name``. - - Pas de fallback, on ne veut pas instancier des objets de manière hasardeuse. - """ - return cls._classes.get(name) - -def crans_object(classe): - """Pour décorer les classes permettant d'instancier des attributs LDAP, - afin de les enregistrer dans :py:class:`ObjectFactory`. - - """ - ObjectFactory.register(classe.ldap_name, classe) - return classe - - -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.""" - if login is None: - login = self.conn.current_user["uid"][0].value - 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=None): - """Supprimme l'objet de la base LDAP. En supprimant ses enfants d'abord.""" - if login is None: - login = self.conn.current_user["uid"][0].value - 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 save(self): - raise EnvironmentError("AssociationCrans.save(): done.") - - def ressuscite(self, comm, login): - raise EnvironmentError("Ressusciter le Crans ? Hum…") - - 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 - - -@crans_object -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] - ldap_name = "adherent" - - 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") - - -@crans_object -class club(proprio): - u"""Club crans""" - attribs = proprio.attribs + [attributs.cid, attributs.responsable, attributs.imprimeurClub] - ldap_name = "club" - -@crans_object -class machineFixe(machine): - u"""Machine fixe""" - ldap_name = "machineFixe" - -@crans_object -class machineWifi(machine): - u"""Machine wifi""" - attribs = machine.attribs + [attributs.ipsec] - ldap_name = "machineWifi" - - def set_ipv4(self, login=None): - u"""Définie une ipv4 à la machine si elle n'est possède pas déjà une.""" - if login is None: - login = self.conn.current_user["uid"][0].value - 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]))) ] - 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])) - -@crans_object -class machineCrans(machine): - can_be_by = { created: [attributs.nounou], - modified: [attributs.nounou], - deleted: [attributs.nounou], - } - attribs = machine.attribs + [attributs.prise, attributs.nombrePrises] - ldap_name = "machineCrans" - -@crans_object -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] - ldap_name = "borneWifi" - -@crans_object -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] - ldap_name = "facture" - -@crans_object -class service(CransLdapObject): - ldap_name = "service" - -@crans_object -class lock(CransLdapObject): - ldap_name = "lock" diff --git a/objets.py b/objets.py new file mode 100644 index 0000000..fcb5fb3 --- /dev/null +++ b/objets.py @@ -0,0 +1,744 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +""" Définition des classes permettant d'instancier les objets LDAP. """ + +# +# 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 +import datetime +import time + +import ldap +from ldap.modlist import addModlist, modifyModlist + +## import de /usr/scripts/ +if not "/usr/scripts/" in sys.path: + sys.path.append('/usr/scripts/') + +import gestion.config as config + +## import locaux +import lc_ldap +import crans_utils +import attributs +import ldap_locks +import services +import variables + +#: Champs à ignorer dans l'historique +HIST_IGNORE_FIELDS = ["modifiersName", "entryCSN", "modifyTimestamp", "historique"] + +def new_cransldapobject(conn, dn, mode='ro', ldif = None): + """Crée un objet :py:class:`CransLdapObject` en utilisant la classe correspondant à + l'``objectClass`` du ``ldif`` + --pour usage interne à la librairie uniquement !""" + + classe = None + + if dn == variables.base_dn: + classe = AssociationCrans + elif dn == variables.invite_dn: + classe = BaseInvites + elif ldif: + classe = ObjectFactory.get(ldif['objectClass'][0]) + else: + res = conn.search_s(dn, 0) + if not res: + raise ValueError ('objet inexistant: %s' % dn) + _, attrs = res[0] + classe = ObjectFactory.get(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 = { variables.created: [attributs.nounou], + variables.modified: [attributs.nounou], + variables.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.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: + self.attrs = attributs.AttrsDict(self.conn, ldif, Parent=self) + self._modifs = attributs.AttrsDict(self.conn, ldif, Parent=self) + orig = ldif + + elif dn != variables.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(variables.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 = self.attrs.to_ldif() + + 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(self._modifs.to_ldif()) + # 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): + """Sauvegarde l'objet dans un fichier dans le cimetière.""" + 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=None): + """Supprime l'objet de la base LDAP. Appelle :py:meth:`CransLdapObject.bury`.""" + if login is None: + login = self.conn.current_login + if self.mode not in ['w', 'rw']: + raise EnvironmentError("Objet en lecture seule, réessayer en lecture/écriture") + if not self.may_be(variables.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 = self._modifs.to_ldif() + orig_ldif = self.attrs.to_ldif() + + 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(variables.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] == variables.deleted: + out.append(u"%s : [%s] Suppression" % (date, author)) + elif attrs['reqType'][0] == variables.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 ObjectFactory(object): + """Utilisée pour enregistrer toutes les classes servant à instancier un objet LDAP. + Elle sert à les récupérer à partir de leur nom LDAP. + + Cette classe n'est jamais instanciée. + + """ + _classes = {} + + @classmethod + def register(cls, name, classe): + """Enregistre l'association ``name`` -> ``classe``""" + cls._classes[name] = classe + + @classmethod + def get(cls, name): + """Retourne la classe qui a ``name`` pour ``ldap_name``. + + Pas de fallback, on ne veut pas instancier des objets de manière hasardeuse. + """ + return cls._classes.get(name) + +def crans_object(classe): + """Pour décorer les classes permettant d'instancier des attributs LDAP, + afin de les enregistrer dans :py:class:`ObjectFactory`. + + """ + ObjectFactory.register(classe.ldap_name, classe) + return classe + + +class proprio(CransLdapObject): + u""" Un propriétaire de machine (adhérent, club…) """ + can_be_by = { variables.created: [attributs.nounou, attributs.bureau, attributs.cableur], + variables.modified: [attributs.nounou, attributs.bureau, attributs.soi, attributs.cableur], + variables.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 == variables.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 == variables.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.""" + if login is None: + login = self.conn.current_login + 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=None): + """Supprimme l'objet de la base LDAP. En supprimant ses enfants d'abord.""" + if login is None: + login = self.conn.current_login + if self.mode not in ['w', 'rw']: + raise EnvironmentError("Objet en lecture seule, réessayer en lecture/écriture") + if not self.may_be(variables.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 = { variables.created: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent], + variables.modified: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent], + variables.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 save(self): + raise EnvironmentError("AssociationCrans.save(): done.") + + def ressuscite(self, comm, login): + raise EnvironmentError("Ressusciter le Crans ? Hum…") + + 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 + + +@crans_object +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] + ldap_name = "adherent" + + 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 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") + + +@crans_object +class club(proprio): + u"""Club crans""" + attribs = proprio.attribs + [attributs.cid, attributs.responsable, attributs.imprimeurClub] + ldap_name = "club" + +@crans_object +class machineFixe(machine): + u"""Machine fixe""" + ldap_name = "machineFixe" + +@crans_object +class machineWifi(machine): + u"""Machine wifi""" + attribs = machine.attribs + [attributs.ipsec] + ldap_name = "machineWifi" + + def set_ipv4(self, login=None): + u"""Définie une ipv4 à la machine si elle n'est possède pas déjà une.""" + if login is None: + login = self.conn.current_login + 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]))) ] + 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])) + +@crans_object +class machineCrans(machine): + can_be_by = { variables.created: [attributs.nounou], + variables.modified: [attributs.nounou], + variables.deleted: [attributs.nounou], + } + attribs = machine.attribs + [attributs.prise, attributs.nombrePrises] + ldap_name = "machineCrans" + +@crans_object +class borneWifi(machine): + can_be_by = { variables.created: [attributs.nounou], + variables.modified: [attributs.nounou], + variables.deleted: [attributs.nounou], + } + attribs = machine.attribs + [attributs.canal, attributs.puissance, attributs.hotspot, + attributs.prise, attributs.positionBorne, attributs.nvram] + ldap_name = "borneWifi" + +@crans_object +class facture(CransLdapObject): + can_be_by = { variables.created: [attributs.nounou, attributs.bureau, attributs.cableur], + variables.modified: [attributs.nounou, attributs.bureau, attributs.cableur], + variables.deleted: [attributs.nounou, attributs.bureau, attributs.cableur], + } + attribs = [attributs.fid, attributs.modePaiement, attributs.recuPaiement] + ldap_name = "facture" + +@crans_object +class service(CransLdapObject): + ldap_name = "service" diff --git a/services.py b/services.py index c96ccaa..ee9e1c8 100644 --- a/services.py +++ b/services.py @@ -4,6 +4,7 @@ import ldap import lc_ldap import attributs +import objets from gen_confs.dhcpd_new import dydhcp services_dn = 'ou=services,dc=crans,dc=org' @@ -26,7 +27,7 @@ services_to_attrs['mail_modif'] = [] # génération des arguments du service à redémarrer (par defaut []) services_to_args={} -services_to_args['macip']=lambda x: issubclass(type(x.parent), lc_ldap.machine) and ([str(x)] if isinstance(x, attributs.ipHostNumber) else [ str(ip) for ip in x.parent.get('ipHostNumber',[]) ]) or ([ str(ip) for m in x.parent.machines() for ip in m.get('ipHostNumber',[])] if issubclass(type(x.parent), lc_ldap.proprio) else []) +services_to_args['macip']=lambda x: issubclass(type(x.parent), objets.machine) and ([str(x)] if isinstance(x, attributs.ipHostNumber) else [ str(ip) for ip in x.parent.get('ipHostNumber',[]) ]) or ([ str(ip) for m in x.parent.machines() for ip in m.get('ipHostNumber',[])] if issubclass(type(x.parent), lc_ldap.proprio) else []) ## Inutile pour blackliste pour le moment #services_to_args['blacklist']=lambda x: issubclass(type(x.parent), lc_ldap.machine) and [ str(ip) for m in x.parent.proprio().machines() for ip in m['ipHostNumber'] ] or [ str(ip) for m in x.parent.machines() for ip in m['ipHostNumber'] ] services_to_args['port']=lambda x: [str(x)] if isinstance(x, attributs.ipHostNumber) or isinstance(x, attributs.ip6HostNumber) else [ str(ip) for ip in x.parent.get('ipHostNumber',[]) ] diff --git a/shortcuts.py b/shortcuts.py new file mode 100644 index 0000000..5d8c306 --- /dev/null +++ b/shortcuts.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" Raccourcis pour se connecter facilement à la base LDAP avec le binding lc_ldap. """ + +import sys +import os.path + +import lc_ldap as module_qui_a_le_meme_nom_que_sa_classe_principale +import variables + +#: 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 + +def lc_ldap(*args, **kwargs): + """Renvoie une connexion à la base LDAP.""" + return module_qui_a_le_meme_nom_que_sa_classe_principale.lc_ldap(*args, **kwargs) + +def lc_ldap_test(*args, **kwargs): + """Renvoie une connexion LDAP à la base de tests.""" + # 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", variables.ldap_test_password) + # On en a aussi besoin pour le lookup en readonly + kwargs.setdefault("readonly_dn", variables.readonly_dn) + kwargs.setdefault("readonly_password", variables.ldap_test_password) + kwargs.setdefault("user", current_user) + return module_qui_a_le_meme_nom_que_sa_classe_principale.lc_ldap(*args, **kwargs) + +def lc_ldap_admin(*args, **kwargs): + """Renvoie une connexion LDAP à la vraie base, en admin. + Possible seulement si on peut lire secrets.py + + """ + secrets = import_secrets() + kwargs.setdefault("user", current_user) + return module_qui_a_le_meme_nom_que_sa_classe_principale.lc_ldap(uri='ldap://ldap.adm.crans.org/', dn=secrets.ldap_auth_dn, + cred=secrets.ldap_password, user=current_user) + +def lc_ldap_readonly(*args, **kwargs): + """Connexion LDAP à la vraie base, en readonly. + Possible seulement si on peut lire secrets.py + + """ + secrets = import_secrets() + kwargs["uri"] = 'ldap://ldap.adm.crans.org/' + kwargs["dn"] = secrets.ldap_readonly_auth_dn + kwargs["cred"] = secrets.ldap_readonly_password + kwargs.setdefault("user", current_user) + return module_qui_a_le_meme_nom_que_sa_classe_principale.lc_ldap(*args, **kwargs) + +def lc_ldap_local(*args, **kwargs): + """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). + + """ + 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 + kwargs["uri"] = ro_uri + kwargs["dn"] = auth_dn + kwargs["cred"] = auth_pw + kwargs.setdefault("user", current_user) + return module_qui_a_le_meme_nom_que_sa_classe_principale.lc_ldap(*args, **kwargs) diff --git a/test.py b/test.py index dc29057..d661a7c 100755 --- a/test.py +++ b/test.py @@ -1,15 +1,23 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- + +# import lib standard import psycopg2 import traceback import random import string import os import sys + +## import dans /usr/scripts/ sys.path.append("/usr/scripts/") -import lc_ldap from gestion.affich_tools import anim, OK, cprint, ERREUR +## import locaux +import lc_ldap +import shortcuts +import variables + show_traceback = False if "--traceback" in sys.argv: show_traceback = True @@ -124,8 +132,7 @@ def tests_machines(parent_dn, realm_list, ipsec=False): print "Test de la librairie lc_ldap" print "Connection" -current_user = os.getenv("SUDO_USER") or os.getenv("USER") -conn= lc_ldap.lc_ldap_test(user=current_user) +conn = shortcuts.lc_ldap_test() print u"Tests effectués avec les droits %s " % ', '.join(conn.droits) @@ -174,7 +181,7 @@ anim("Creation d'un adherent") try: adherent = conn.newAdherent(adherent_ldif) adherent.create() -except: +except Exception: print ERREUR print traceback.format_exc() adherent = None @@ -191,8 +198,8 @@ else: # Création et suppression de machines Crans # ############################################# -tests_machines(lc_ldap.base_dn, ["adm", "serveurs", "serveurs-v6", "adm-v6"]) -tests_machines(lc_ldap.base_dn, ["bornes"]) +tests_machines(variables.base_dn, ["adm", "serveurs", "serveurs-v6", "adm-v6"]) +tests_machines(variables.base_dn, ["bornes"]) ###################### @@ -204,7 +211,7 @@ try: club = conn.newClub(club_ldif) club['responsable'] = str(adherent['aid'][0]) club.create() -except: +except Exception: print ERREUR print traceback.format_exc() else: @@ -216,7 +223,7 @@ else: try: club = conn.search('cid=%s' % club['cid'][0], mode='rw')[0] club.delete() - except: + except Exception: print ERREUR print traceback.format_exc() else: print OK @@ -234,7 +241,7 @@ if adherent: try: adherent = conn.search('aid=%s' % adherent['aid'][0], mode='rw')[0] adherent.delete() - except: + except Exception: print ERREUR print traceback.format_exc() else: print OK @@ -256,7 +263,7 @@ else: try: facture = conn.search('fid=%s' % facture['fid'][0], mode='rw')[0] facture.delete() - except: + except Exception: print ERREUR print traceback.format_exc() else: print OK diff --git a/variables.py b/variables.py new file mode 100644 index 0000000..ffd1bfb --- /dev/null +++ b/variables.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" Définitions de variables utiles pour lc_ldap. """ + +#: uri par défaut de la base LDAP +uri = 'ldap://ldap.adm.crans.org/' +#: dn racine de l'endroit où sont stockées les données +base_dn = 'ou=data,dc=crans,dc=org' +#: dn racine de l'endroit où sont stockés les logs +log_dn = "cn=log" +#: dn pour se binder en root +admin_dn = "cn=admin,dc=crans,dc=org" +#: dn pour se binder en readonly +readonly_dn = "cn=readonly,dc=crans,dc=org" +#: dn racine de l'endroit où sont stockés les invités (artefact garbage ?) +invite_dn = 'ou=invites,ou=data,dc=crans,dc=org' + +# Protection contre les typos +#: Droit de créer +created = 'created' +#: Droit de modifier +modified = 'modified' +#: Droit de supprimer +deleted = 'deleted' + +#: Mot de passe de la base de tests +ldap_test_password = '75bdb64f32'