Article publié dans Linux Magazine 68, janvier 2005.
Copyright © 2004 - Jérôme Fenal.
Net::LDAP
Net::LDAP
Après avoir vu un peu de théorie et de mise en pratique de LDAP avec Linux, et
puisque nous sommes dans la rubrique tenue par les Mongueurs de Perl,
attachons-nous au sujet qui nous intéresse : Net::LDAP
.
Perl est un langage qui sait à peu près tout faire, et LDAP, ainsi que LDIF sont tout à fait dans ses capacités.
Ses capacités de traitement de chaînes de caractères ainsi que les structures de données qu'il met à disposition du programmeur permettent, par exemple, de réécrire à peu de frais notre propre version des outils de migration de PADL.com ; le but étant d'intégrer facilement d'autres sources de données qu'un simple fichier /etc/passwd.
Historiquement, Perl a aussi été un des premiers langages utilisés avec LDAP,
Netscape ayant développé une interface Perl sur la base de ses API LDAP écrites
en langage C. Le module en question est perldap
, module toujours disponible
lorsque vous achetez Sun Java System Directory Server (le nouveau nom de
Netscape Directory Server, après son passage par la société iPlanet). Notez
qu'une distribution de Perl est intégrée au produit. À ce sujet, vous pourrez même
bientôt tester un petit frère de ce produit lorsque Red Hat aura ouvert le code
de Netscape Directory Server, racheté à AOL, ex-coactionnaire de iPlanet avec
Sun.
Citons enfin comme autre moyen d'accès à LDAP la suite de modules Net::LDAP
développée par Graham Barr. Cette suite a le gros avantage d'être écrite
nativement et totalement en Perl, et donc de ne pas employer de code XS
nécessitant un compilateur C et surtout des bibliothèques LDAP. C'est ce module
spécifique que nous allons voir plus en détail.
Plusieurs raisons à cela :
Perl est un langage de script, et en cela permet de faire beaucoup plus facilement qu'en C des requêtes puis des modifications / insertions dans l'annuaire. Un autre avantage de Perl quand on travaille sur des informations nommées telles que celles que l'on trouve dans LDAP réside dans l'utilisation des hachages. Ainsi, avec les seuls utilitaires présentés ici, on peut faire énormément de choses, mais en passant toujours par le format LDIF, qu'il faut générer.
Perl aide donc à cela, facilement et rapidement.
Enfin, avec le remplacement de chaîne (s///
ou substr()
), il est facile
de se faire une sorte de patron (%U est le nom d'utilisateur, %G son groupe
principal, etc) et de faire générer du LDIF ou de faire des modifications
paramétrées dans l'annuaire.
Net::LDAP
Le choix d'utiliser LDIF dans nos premiers scripts est celui de l'assurance. En effet, aucune modification n'est faite dans l'annuaire, seuls des fichiers texte sont générés. Mais si LDIF est facile à utiliser pour les ajouts, il devient un enfer de volubilité quand il s'agit de faire des modifications en masse dans un annuaire. Et ce, d'autant plus quand il s'agit de faire la même modification pour toute une branche, ou tous les objets couverts par un filtre de sélection.
Il m'est arrivé d'avoir à modifier en masse la structure de l'arbre des
informations de l'annuaire (le DIT), car le modèle en OU
choisi au départ
s'est avéré malheureux, malaisé, bref à changer. Si cette opération devait être
réalisée manuellement, ce serait une requête pour obtenir les DN des objets à
renommer, puis autant de requêtes à l'utilitaire ldapmodrdn
. Pas pratique,
alors qu'en Perl, on interroge, on boucle sur les DN, et on renomme dans la
foulée. Plus rapide, plus pratique, mais aussi plus dangereux en cas de mauvaise
manipulation, encore qu'il soit tout à fait possible de tester sur quelques
objets de test dans une OU
de test.
C'est ainsi que nous allons voir comment utiliser Net::LDAP
afin de faire nos
modifications directement dans notre annuaire.
Net::LDAP
Net::LDAP
(aussi connu sous le nom de sa distribution sur CPAN, perl-ldap
)
est donc l'une des deux API LDAP pour Perl. En tant que module non-standard
(i.e. pas distribué avec l'interpréteur Perl lui-même), il faut l'installer
avant utilisation :
root@ldap1# cpan Net::LDAP¶
Attention, il y a plusieurs dépendances, dont Convert::ASN1
.
Net::LDAP
est un module orienté objet, donc nous créerons un objet d'accès à
l'annuaire. Nous utiliserons ensuite les méthodes associées à cet objet pour
lancer nos requêtes sur l'annuaire.
La connexion à l'annuaire est très simple, il suffit d'instancier un objet
Net::LDAP
puis d'utiliser la méthode bind
:
my $ldap = Net::LDAP->new('ldap1.example.com', port => 389, version => 3) or die "erreur LDAP: Impossible de contacter l'annuaire ($@)";
La variable $@
indiquée dans le die
est le message d'erreur du dernier
appel à la fonction eval
. Sa présence ici n'est pas forcément nécessaire,
mais elle permet d'attraper le cas échéant les erreurs sur un require
qui est
utilisé par Net::LDAP
. Dit plus court, ça sert (dans le cas de
Net::LDAP
) à savoir s'il y a des erreurs de syntaxe dans les modules.
Le premier paramètre est le nom du serveur LDAP. Ce premier paramètre peut aussi
être une URI, telle que ldap://localhost:389/
, ce qui dispenserait de
spécifier le port dans les options. La notation par URI est surtout intéressante
dans le cas où votre script Perl Net::LDAP
devra s'exécuter sur la même machine
que le serveur LDAP, et de communiquer avec lui sans utiliser le réseau. En
effet, c'est le seul moyen de donner le nom du socket Unix pour une connexion
LDAPI :
my $ldap = Net::LDAP->new('ldapi://%2fvar%2frun%2fldapi_socket', version => 3)
La méthode new
peut prendre d'autres paramètres, dont le plus intéressant à
connaître est timeout
.
La connexion proprement dite (bind
) est tellement simple que je ne
détaillerai pas :
$ldap->bind ( "cn=Manager,dc=example,dc=com", password => "secret" );
Pour une connexion anonyme, il suffit de ne pas passer d'argument à bind
.
La méthode search
permet d'effectuer vos requêtes, elle retourne un objet de
classe Net::LDAP::Search
. Cet objet référencera ensuite les objets de
l'annuaire répondant aux filtres passés pour la recherche, qui seront de type
Net::LDAP::Entry
.
Net::LDAP::Search
permet plusieurs méthodes d'accès aux objets retournés,
que ce soit avec une sémantique tableau (méthode entries()
, ou une
sémantique pile ou liste, avec la méthode pop_entry()
.
Cette classe d'objet Net::LDAP::Search
permet aussi la gestion des erreurs
avec leur détection (méthode code()
) et leur documentation (méthode
error()
). Ceci constitue une adaptation de la méthologie habituelle
consistant à faire retourner une valeur scalaire (un entier en l'occurrence)
véhiculant le code erreur. En C comme en Perl, on a souvent pour permettre des
constructions des langages telle que :
if ( $mon_objet->sa_methode(\%entrees, \%sorties) ) { # Nous traitons ici dans ce bloc de code une erreur. ... }
Ici, la méthode (ou la fonction en C) doit retourner 0 en cas de succès, et une
valeur non nulle en cas d'erreur. Dans le cas de Net::LDAP
, ce qui est
retourné par les méthodes de l'accesseur Net::LDAP
est donc un objet de
classe Net::LDAP::Search
. Or il se trouve que cette classe dérive de la
classe Net::LDAP::Message
, classe qui définit entre autres deux méthodes
particulières : code()
et error()
. De la même façon, les autres
méthodes retournent un objet dont la classe, adaptée au contexte de la méthode
Net::LDAP
appelée, dérive de Net::LDAP::Message
. La référence sur le
message, retournée par toutes les opérations hormis l'instanciation de l'objet
d'accès à l'annuaire, permet ainsi de gérer le contrôle d'erreur. On peut se
permettre de l'oublier rapidement (nous sommes dans le cadre de scripts rapides
à développer et à jeter) grâce à la simple ligne :
$mesg->code && die $mesg->error;
Ce qui diffère des habitudes. Mais nous sommes dans un contexte objet, et cette manière de retourner des objets ubiquitaires, retournant à la fois le code retour de l'opération les ayant créé en plus des données permet de tout gérer en même temps, au prix d'une ligne de code de plus.
Dans le cadre de programmes nécessitant une gestion plus fine des erreurs,
utilisez un autre moyen d'informer l'utilisateur que die
et optez pour des
comportements différents suivant les valeurs de $mesg->code
.
Les différentes valeurs du code erreur sont disponibles dans la page de manuel
de Net::LDAP::Constant
. Par exemple :
LDAP_SUCCESS
(0)
Opération terminée sans erreur
LDAP_OPERATIONS_ERROR
(1)
Le serveur a rencontré une erreur interne.
Des informations complémentaires vous sont aussi données dans les pages de
manuel Net::LDAP::Message
et Net::LDAP::FAQ
.
Mais passons à notre première requête : ou=Paris
#!/usr/bin/perl -w use strict; use Net::LDAP; my $mesg; my $ldap = Net::LDAP->new("ldap1.example.com", port => 389, version => 3) or die "erreur LDAP: Can't contact master ldap server ($@)"; $mesg = $ldap->bind( "cn=Manager,dc=example,dc=com", password => 'secret' ); $mesg->code && die $mesg->error; $mesg = $ldap->search( base => 'dc=example,dc=com', scope => 'sub', filter => '(ou=Paris)' ); $mesg->code && die $mesg->error; foreach my $entry ($mesg->all_entries) { print "dn: ".$entry->dn()."\n"; foreach my $attr ($entry->attributes) { foreach my $value ($entry->get_value($attr)) { print "$attr: $value\n"; } } } $ldap->unbind();
Notez l'imbrication des méthodes pour accéder au contenu des objets LDAP.
Un fait qui peut être troublant est que la méthode attributes
renvoie toujours un
tableau (ou une référence sur un tableau si l'option asref => 1
est
passée à la méthode get_values
). Cela est dû au fait qu'il n'y a pas de
différence de traitement entre les attributs multivalués et ceux qui ne le sont
pas.
L'appel à la méthode unbind
signifie la demande de déconnexion de l'annuaire
par le programme.
La méthode search
peut aussi prendre un paramètre nommé attrs
qui permet
de limiter la liste des attributs retournés par la requête. Par défaut, tous les
attributs auxquels les listes de contrôle d'accès vous permettent d'accéder sont
retournés. Si vous ne voulez que tester la présence ou non d'objets par rapport
à votre filtre, spécifiez attrs => [ '1.1' ]
. Vous pouvez aussi accéder
aux attributs de gestion de l'annuaire, comme les horodatages sur les objets :
$mesg = $ldap->search( base => 'dc=example,dc=com', scope => 'sub', filter => '(&(objectClass=posixAccount)(ou=Paris))' attrs => [ qw( * createTimestamp modifyTimestamp ) ] );
L'étoile est le joker désignant tous les attributs retournés par défaut. Ainsi,
tous les attributs « normaux » sont retournés, plus createTimestamp
et
modifyTimestamp
.
L'ajout est tout aussi simple que le reste. Il suffit de spécifier à la méthode
add
le DN du nouvel objet, ainsi que la liste des attributs.
$mesg = $ldap->add( "uid=dmitri,ou=Moscou,ou=People,dc=example,dc=com", attr => [ uid => 'dmitri', cn => 'dmitri', objectClass => [ 'top', 'person', 'inetOrgPerson', 'posixAccount', 'shadowAccount' ], uidNumber => 1007, gidNumber => '513', userPassword => '$pass', cn => 'dmitri', loginShell => '/bin/bash', gecos => 'Charlie dmitri', description => 'System User', homeDirectory => '/home/dmitri', sn => 'dmitri' ] );
Lorsqu'un attribut est multivalué, on passe une référence à un tableau
(éventuellement anonyme comme ici) contenant toutes les valeurs de l'attribut au
lieu de la simple valeur. Nous sommes en contexte scalaire, attr
est donc une référence à un tableau contenant tous les attributs de l'objet.
De façon similaire, il est possible de modifier la valeur d'un attribut ou
d'enlever un attribut (non nécessaire, i.e. en MAY
dans le schéma).
Ces trois méthodes add
, modify
et delete
doivent être suivies d'un
appel à la méthode update
de façon à lancer la mise à jour de l'annuaire.
Dans le cas contraire, les modifications effectuées sur l'objet courant sont
perdues.
Mais voyons plutôt ces trois méthodes en action.
Un besoin courant dans la mise en place d'un annuaire est l'ajout ou la modification en masse d'attributs sur tous les objets de l'annuaire.
Le script qui suit, volontairement non générique, permet donc de modifier/ajouter/enlever un attribut à la fois sur tous les objets de l'annuaire qui répondent au filtre spécifié dans la requête.
#!/usr/bin/perl -w use strict; use Getopt::Std; use Net::LDAP; use vars qw($attr $val $opt_h $opt_a $opt_m $opt_d); sub say_usage { print "modldap - Modificateur simple d'attributs LDAP\n"; print "\n\n\t$0 [-a] attribut valeur ...\n"; print << 'EOT'; Options: [-d] : Détruire un attribut [-m] : Modifier un attribut [-a] : Ajouter un attribut (pour les attributs multivalués) [-h] : help : montre ce texte d'aide EOT exit 0; } getopts('admh'); # Sets opt_* say_usage if $opt_h; if (! defined($attr=shift)) { print "ERREUR : Spécifiez un attribut LDAP à modifier\n"; say_usage; }; if (! $opt_d && ! defined($val=shift)) { print "ERREUR : Spécifiez une valeur pour l'attribut\n"; say_usage; }; my $ldap = Net::LDAP->new('ldap1.example.com', port => 389, version => 3) or die "ERREUR LDAP : Impossible de contacter l'annuaire ($@)"; $ldap->bind ( "cn=Manager,dc=example,dc=com", password => "secret" ); my $mesg = $ldap->search( base => 'ou=People, dc=example,dc=com', scope => 'sub', filter => '(objectClass=*)' ); $mesg->code && die $mesg->error; LOOP: foreach my $entry ($mesg->all_entries) { my $uid = lc($entry->get_value('uid')); my $action = 'nop'; my @attrl; $action = 'add' if $opt_a; $action = 'modify' if $opt_m; $action = 'delete' if $opt_d; @attrl = $entry->get_value($attr); print "Old values :".join(" ", @attrl)."\n" if @attrl; my $newval = $val; $newval =~ s/\%U/$uid/g; ACTION: for ($action) { /nop/ and do { print "Rien à faire\n"; last ACTION; }; /add/ and do { print "Ajout attribut $attr à $newval\n"; $entry->add($attr, $newval); $entry->update($ldap); last ACTION; }; /modify/ and do { next LOOP if $newval eq $attrl[0]; # Attention ici, pas de modification # si l'attribut a déjà la bonne valeur print "Modification attribut $attr à $newval\n"; $entry->replace($attr, $newval); $entry->update($ldap); last ACTION; }; /delete/ and do { if (@attrl) { print "Destruction attribut $attr\n"; $entry->delete($attr, [ ]); $entry->update($ldap); } last ACTION; }; } print "-" x 60 . "\n"; } $ldap->unbind;
Ce script m'a rendu maintes fois service sur des annuaires importants.
N'oubliez pas que Perl sait facilement faire des manipulations (des
filtres ?) sur le texte, comme par exemple la ligne
$newval =~ s/\%U/$uid/g;
qui permet de modifier un %U
présent dans la
valeur de l'attribut en l'uid
du compte : pratique pour modifier un attribut
tel que le répertoire racine (home directory) des comptes utilisateurs.
Le renommage et le déplacement d'objets dans l'arbre de l'annuaire se fait
facilement à la main avec un outil graphique, avec un simple tirer-lâcher
(drag'n drop). Cependant, au début d'un déploiement d'annuaire, on peut
vouloir changer l'organisation du DIT, comme passer d'une organisation
hiérarchisée avec plusieurs sous-OU
à une organisation à plat de l'annuaire.
Mais quand on a plus de 10 entrées à déplacer, il vaut mieux commencer à
automatiser.
L'automatisation peut se faire par le biais d'un vidage de l'annuaire (avec
un slapcat
ou un ldapsearch
), suivi d'un tripotage de tout le LDIF résultant
(presque) manuel (quelques expressions rationnelles Perl aideront). Le résultat
sera ensuite réinjecté dans l'annuaire, nécessitant un arrêt de production et
l'indisponibilité de l'annuaire qui vont avec.
Une autre solution est de déplacer les objets dans l'annuaire (en ayant prêté
attention aux requêtes telles que celles faites par PAM).
Comme sous Unix, où la commande mv
permet à la fois de renommer et de
déplacer des fichiers entre répertoires, la méthode moddn
permet de renommer
voire de déplacer les objets entre différentes OU
dans LDAP. C'est cette
fonctionnalité qui est derrière le tirer-lâcher des outils graphiques.
Prêtons-nous, pour illustrer la solution à ce besoin, à un petit exercice
d'utilisation de moddn
. Nous allons faire passer tout notre petit monde dans
une nouvelle OU
qui va s'appeler Mongueurs. Voici comment faire :
#!/usr/bin/perl -w use strict; use Net::LDAP; my $ldap = Net::LDAP->new('ldap1.example.com', port => 389, version => 3) or die "erreur LDAP : Impossible de contacter l'annuaire ($@)"; $ldap->bind ( "cn=Manager,dc=example,dc=com", password => "secret" ); my $mesg = $ldap->search( base => "ou=People, dc=example,dc=com", scope => 'sub', filter => '(objectclass=person)' ); $mesg->code && die $mesg->error; foreach my $entry ($mesg->all_entries) { my $dn = $entry->dn(); my $newrdn = $1 if $dn =~ m/([^,]+)/; print STDERR "$dn sera renommé en $newrdn\n"; $mesg = $ldap->moddn( $dn, newrdn => $newrdn, newsuperior => "ou=Mongueurs,ou=People,dc=example,dc=com"); $mesg->code && die "Impossible de renommer $dn : ".$mesg->error; } $ldap->unbind;
Ici nous récupérons le RDN (en enlevant tout ce qui vient après la première
virgule) sans pour autant le modifier. Si on modifie le RDN, il ne faudra
pas oublier de penser à l'option deleteoldrdn
de la méthode moddn
, qui
permet de ne pas conserver l'ancien RDN en plus du nouveau (attention à respecter le
schéma et gare aux collisions). Cela peut permettre une certaine période de migration
sur un annuaire.
Faites néanmoins attention à ce que vous faites, et n'hésitez jamais à faire vos
tests sur un annuaire contenant une réplique des données de production, ou si
vous n'avez pas de troisième serveur (car vous en avez déjà deux,
n'est-ce-pas ?), ou à défaut sur une copie des données de production dans une
OU
ou mieux, une database
LDAP de test sur votre annuaire OpenLDAP.
Une autre des utilisations de Net::LDAP
peut être de vous permettre de
synchroniser deux annuaires ne sachant ou ne pouvant pas forcément parler entre
eux (que ce soit pour une raison technique ou politique, la sécurité étant un
exemple).
La solution est donc d'aller interroger les deux annuaires se présentant à vous,
de faire les comparaisons sur les entrées de chacun, et ensuite soit de générer
du LDIF permettant la synchronisation, soit d'aller ajouter directement les
objets dans l'annuaire devant reprendre les informations de l'autre. L'approche
pour ce faire va donc être de récupérer en masse les noms des objets, de façon à
passer le moins de temps possible dans l'annuaire, de faire les comparaisons de
présence/absence (je ne parle pas des modifications sur les objets en eux-même),
puis de copier les objets du premier annuaire dans le second, voire de détruire
les objets ayant disparu du premier. Un détail avant de commencer :
n'escomptez pas sans outil particulier répliquer les mots de passe si votre
annuaire source est un Active Directory. Celui-ci les stocke dans un attribut
unicodePwd
, mais ne les restitue pas. Tant pis pour la migration
transparente à Samba...
La première passe de recherche va donc simplement consister en la requête sur le
UID
de chacun des deux annuaires, à les mettre dans des hachages, et à
compter les clés d'un côté et de l'autre. Pour ce faire, on lance successivement
sur chacun des deux annuaires une requête, et on stocke l'UID
dans un
hachage.
Pour émuler avec un seul annuaire deux arborescences de comptes, il nous suffit
de dupliquer dans une nouvelle OU
une partie de nos comptes. Ce qui peut nous
donner ceci :
$ ldapsearch -D "cn=Manager,dc=example,dc=com" -s sub -b 'dc=example,dc=com' \ > -w secret -s sub -x '(objectClass=posixAccount)' -LLL dn | sed '/^$/d' | sort¶ dn: uid=book,ou=Europe,dc=example,dc=com dn: uid=grinder,ou=Europe,dc=example,dc=com dn: uid=grinder,ou=Paris,ou=People,dc=example,dc=com dn: uid=leon,ou=Bath,ou=People,dc=example,dc=com dn: uid=leon,ou=Europe,dc=example,dc=com dn: uid=nicholas,ou=Europe,dc=example,dc=com dn: uid=nicholas,ou=London,ou=People,dc=example,dc=com dn: uid=sniper,ou=Paris,ou=People,dc=example,dc=com dn: uid=ymmv,ou=Europe,dc=example,dc=com dn: uid=ymmv,ou=Paris,ou=People,dc=example,dc=com
Nous avons donc cinq comptes dans l'OU
Europe (book, grinder, leon, nicholas,
et ymmv), et cinq autres dans l'arborescence créée auparavant (leon, nicholas,
grinder, sniper et ymmv). BooK n'est plus dans l'arborescence originelle qui
nous servira de source, alors que Sniper n'est que dans l'annuaire source.
Le petit script suivant vous permet de voir comment nous pouvons procéder pour détecter les présences ou absences d'un compte dans un annuaire ou l'autre :
#!/usr/bin/perl -w use strict; use Net::LDAP; use Data::Dumper; my %comptes; my $debug = 0; sub ldirread($$$) { my ( $ldapconn, $pow2, $comptes) = @_; print STDERR "Lecture annuaire sur ". $ldapconn->{host} ."\n" if $debug; my $ldap = Net::LDAP->new( $ldapconn->{host} , port => 389, version => 3 ); $ldap->bind( $ldapconn->{cn}, password => $ldapconn->{pass} ); my $mesg = $ldap->search( base => $ldapconn->{base}, scope => 'sub', filter => $ldapconn->{filter} ); $mesg->code && die $mesg->error; foreach my $entry ( $mesg->all_entries ) { $comptes->{$entry->get_value('uid')} |= $pow2; } } my $annuaire = 0; ldirread( { host => 'localhost', cn => 'cn=Manager, dc=example, dc=com', filter => '(uid=*)', pass => 'secret', base => 'ou=People,dc=example,dc=com', }, 1 << $annuaire, \%comptes); $annuaire++; ldirread( { host => 'localhost', cn => 'cn=Manager, dc=example, dc=com', filter => '(uid=*)', pass => 'secret', base => 'ou=Europe,dc=example,dc=com', }, 1 << $annuaire, \%comptes); foreach ( keys %comptes ) { print " ! $_ uniquement dans l'annuaire source\n" if $comptes{$_} == 1; print "!! $_ uniquement dans l'annuaire cible\n" if $comptes{$_} == 2; print " $_ dans les deux annuaires\n" if $comptes{$_} == (1|2); } print Dumper \%comptes if $debug;
Ainsi que vous pouvez le voir, nous créons une fonction ldirread
qui se
charge de lire l'un ou l'autre des annuaires. On lui passe en paramètre une
référence sur un hachage qui contient l'essentiel des informations de connexion
(%ldapconn
dans la fonction). En plus de ces informations, nous passons aussi
une variable contenant la valeur à ajouter aux valeurs existantes (quand elles
existent) du troisième paramètre : le hachage global %comptes
, passé par
référence, qui est vu comme $comptes
dans la fonction.
Ainsi, nous appelons deux fois la fonction ldirread
, avec des informations
de connexion différentes (dans notre cas, seule la base de recherche diffère),
en ne faisant qu'incrémenter le numéro de l'annuaire. Et par la magie des
puissances de deux, nous allumons un bit dans %comptes
pour la présence d'un
enregistrement dans les annuaires. Pour allumer ce bit, nous utilisons un
ou logique, de façon à conserver la valeur précédente, et à y ajouter la valeur
courante. Si la valeur pour la clé n'existe pas, ce qui est le cas lors de la
lecture du premier annuaire, l'auto-vivification de Perl la crée.
Une fois que les annuaires sont lus, il suffit ensuite de relire le hachage, en
regardant les valeurs pour chaque clé. Si la valeur est à 1, l'enregistrement
n'est que dans le premier annuaire, si elle est à 2, il n'est que dans le
second, et si elle est à 3 (en fait 1|2
qui a le bon goût de valoir 1+2
en
arithmétique booléenne), l'enregistrement dont l'uid
est la clé considérée
est présent dans les deux annuaires.
Le résultat à l'exécution donne ceci :
$ ./ds.pl grinder dans les deux annuaires nicholas dans les deux annuaires 1 sniper uniquement dans l'annuaire source leon dans les deux annuaires ymmv dans les deux annuaires 2 book uniquement dans l'annuaire cible
Libre à vous d'étendre ensuite le script avec éventuellement une troisième source, du malaxage sur les données, etc. La technique employée permet de gagner en espace mémoire, et donc en performance, en stockant pour une clé donnée une seule valeur : l'addition des annuaires dans lequel il est présent.
Une autre tâche qui peut être confiée à Perl pour éviter les non-régressions est la vérification du paramétrage des ACL. En effet, la syntaxe de ces ACL n'est pas toujours intuitive et leur comportement dépend en plus de leur ordre de spécification. Il est donc important pour la sécurité d'un système d'information d'en contrôler leur bon fonctionnement.
Le principe est le suivant : lors de l'écriture des différentes ACL, vous
savez ce que vous voulez ouvrir à tel ou tel public. Ce public est soit anonyme,
soit nommé, et on veut lui masquer le maximum d'informations dont il n'a pas besoin.
À noter que cela comprend, par exemple, l'attribut contenant le mot de passe
userPassword
, mais aussi des objets voire des branches complètes, et que ces
choix se font aussi en fonction du contenu de votre annuaire. Par exemple, un
utilisateur non authentifié ne doit pas pouvoir lire les mots de passe des
divers comptes utilisateurs présents dans l'annuaire, mais seulement pouvoir
s'authentifier à leur encontre lors d'un bind
. De même, un utilisateur sans
privilège particulier ne doit pas voir les mots de passe de ses collègues (ce
fut la raison de la création du fichier /etc/shadow). Au contraire, le DN
d'un compte de réplication de l'annuaire doit pouvoir lui lire les mots de passe
de tout le monde afin de pouvoir les répliquer.
Ainsi, au vu des diverses combinaisons possibles, il vaut presque mieux, avec la connaissance que vous avez de votre annuaire, d'abord créer les vérifications qui vous semblent importantes avant de les transformer en ACL. Ces vérifications seront (bien sûr) écrites en Perl, de la manière la plus simple possible, ce dans le but de masquer la complexité des tests et de façon à éviter le dégoût de l'écriture du test ; et par là même de favoriser ces écritures.
Le principe est donc le même que pour l'écriture de tests permettant de valider le bon fonctionnement de vos modules Perl : (dans l'idéal) on écrit d'abord les tests, qui va d'abord échouer, puis on modifie le code (dans notre cas, les ACL) jusqu'à ce que tous les tests passent. Pourquoi tous ? Pour valider l'absence d'effet de bord.
Pour ces tests, un cadre est donc nécessaire de façon à simplifier au maximum l'écriture des tests. Un test devra donc pouvoir n'être qu'un appel à une simple fonction, ayant une sémantique expressive, par exemple :
check( name => 'userPassword de grinder visible par manager', ldap => $ldap{manager}, base => 'ou=People, dc=example, dc=com', scope => 'sub', filter => '(&(objectclass=posixAccount)(uid=grinder))', has => { userPassword => [ '{crypt}$1$BsSFeKPv$9SMR3ck.AA71EPG6vvA3N0' ] }, code => 0 );
Un test a un nom (paramètre nommé name
, désolé de coder en anglais, mais
c'est la langue de CPAN), une référence à l'objet Net::LDAP
permettant la connexion à l'annuaire, la base
de la recherche, son scope
,
un filtre, et une référence sur un hachage contenant un ou plusieurs attributs,
et la ou les valeurs (cas d'attribut multivalué) qu'il doit avoir dans le
contexte courant.
On sort de la vérification elle-même les points suivants : initialisation des
accesseurs Net::LDAP
et authentification auprès de l'annuaire. En effet,
ces phases ne sont que des à-côtés (certes importants car définissant le contexte) des
vérifications.
Les différentes vérifications seront donc intercalées par les connexions et déconnexions à l'annuaire.
Cela donne le script suivant :
#!/usr/bin/perl -w # vim: et ai tw=100 sw=4 ts=4 use strict; use Net::LDAP; use Data::Dumper; my %comptes; my $debug = 1; sub check(@) { my @args = @_; my %parms; my $i = 0; # liste des paramètres nommés autorisés my $re = join "|", qw( name ldap base scope filter has code ); $re = qr/^(?:$re)$/; while ( $i < @args ) { if ( $args[$i] =~ $re ) { my ( $k, $v ) = splice( @args, $i, 2 ); $parms{$k} = $v; } else { $i++; } } # Il ne devrait plus rien rester dans @args die "Paramètre(s) inconnu(s): " . join( ", ", @args ) if @args; # Il n'y a pas de façon directe de savoir si on est connecté ou non die "Authentification non réalisée" if !defined $parms{ldap}->{net_ldap_mesg}; # construction de la requête, profitant de la dualité # liste / paire (clé => valeur) my $mesg = $parms{ldap}->search( ( $parms{base} ? ( base => $parms{base} ) : () ), ( $parms{scope} ? ( scope => $parms{scope} ) : () ), ( $parms{filter} ? ( filter => $parms{filter} ) : () ), ( $parms{has} ? ( attrs => keys %{$parms{has}} ) : () ), ); my $rc=0; $rc++ if ( $parms{code} != $mesg->code() ); foreach my $entry ( $mesg->all_entries ) { foreach my $attr ( keys %{$parms{has}} ) { my %tab = map { $_ => 1 } $entry->get_value($attr); $tab{$_}++ foreach (@{ $parms{has}->{$attr} } ); foreach (keys %tab) { $rc++ if $tab{$_} != 2 } } } print ((($rc == 0) ? 'ok' : 'not ok'), (defined($parms{name})?" - $parms{name}":''),"\n"); $rc; } my %ldap; my $mesg; $ldap{anonymous} = Net::LDAP->new( 'localhost', port => 389, version => 3 ); $mesg = $ldap{anonymous}->bind(); $mesg->code && die $mesg->error; check( name => 'userPassword de grinder visible par anonymous bind', ldap => $ldap{anonymous}, base => 'ou=People, dc=example, dc=com', scope => 'sub', filter => '(&(objectclass=posixAccount)(uid=grinder))', has => { userPassword => [ ] }, code => 0 ); $ldap{anonymous}->unbind; $ldap{ymmv} = Net::LDAP->new( 'localhost', port => 389, version => 3 ); $mesg = $ldap{ymmv}->bind('uid=ymmv,ou=Paris,ou=People,dc=example,dc=com', password => 'topsecret') ; $mesg->code && die $mesg->error; check( name => 'userPassword de grinder visible par ymmv', ldap => $ldap{ymmv}, base => 'ou=People, dc=example, dc=com', scope => 'sub', filter => '(&(objectclass=posixAccount)(uid=grinder))', has => { userPassword => [ ] }, code => 0 ); $ldap{ymmv}->unbind; $ldap{manager} = Net::LDAP->new( 'localhost', port => 389, version => 3 ); $mesg = $ldap{manager}->bind( 'cn=Manager,dc=example,dc=com', password => 'secret' ); $mesg->code && die $mesg->error; check( name => 'userPassword de grinder visible par manager', ldap => $ldap{manager}, base => 'ou=People, dc=example, dc=com', scope => 'sub', filter => '(&(objectclass=posixAccount)(uid=grinder))', has => { userPassword => [ '{crypt}$1$BsSFeKPv$9SMR3ck.AA71EPG6vvA3N0' ] }, code => 0 ); check( name => 'membres du groupe posix tea', ldap => $ldap{manager}, base => 'ou=Group, dc=example, dc=com', scope => 'sub', filter => '(&(objectclass=posixGroup)(cn=tea))', has => { memberUid => [ qw(nicholas leon book ymmv) ] }, code => 0 ); $ldap{manager}->unbind;
La première partie de la fonction check
s'occupe de vérifier les paramètres
nommés qui sont passés, et de les récupérer dans le hachage %parms
.
Ensuite, la requête LDAP est construite en fonction de l'existence d'un
paramètre ou d'un autre. Un point : aucune précaution n'est prise pour
vérifier que des paramètres obligatoires ont bien été passés. Un simple
return if not defined($parms()
fera l'affaire si vous en avez besoin.
Ensuite, un mouchard, $rc
, est positionné à 0 et sera incrémenté en cas de
problème. Viennent enfin les boucles imbriquées de vérification du contenu des
hachages attributs vers listes de valeurs. Les listes de valeurs d'attributs
multivalués sont vérifiés quel que soit l'ordre retourné par la méthode
get_values
ou par ce qui se trouve dans $parms{has}
. Cette vérification
passe par la création d'un hachage temporaire %tab
, dans lequel on ajoute 1 à
chaque fois qu'on rencontre une valeur d'attribut. Chaque valeur étant censée
avoir été rencontrée deux fois (une fois dans $parms{has}
, la seconde dans
get_values
), il suffit de tester que toutes les valeurs du hachage %tab
sont bien à 2.
À la fin de ces boucles, on affiche ok
ou not ok
suivi d'un éventuel nom
de test, et l'on retourne $rc
. Le code retour de check
n'est pas testé
dans notre cas, car l'affichage (?:not\s)?ok
permet par la suite d'intégrer
la séquence de tests avec les modules Test::*
de Perl. Mais ceci est une
autre histoire.
Afin d'être complet, je vous précise avoir utilisé un hachage (%ldap) pour
stocker les accesseurs Net::LDAP
afin d'être sûr que le test de vérification
de la connexion à l'annuaire qui est fait dans la fonction check
se passe
bien. Ça permet en plus d'être expressif via la clé de hachage sans pour autant
consommer des variables globales (et tous les my
qui vont avec, il n'y a pas
de petite économie ^_^
).
La sortie du script ressemble à ça, ce qui permet déjà de voir d'un rapide coup d'œil ce qui passe ou non :
ok - userPassword de grinder visible par anonymous bind ok - userPassword de grinder visible par ymmv not ok - userPassword de grinder visible par manager ok - membres du groupe posix tea
Pour la petite histoire, le troisème test échoue sur un des deux annuaires utilisés pour écrire cet article, mais passe sur l'autre (test écrit sur l'un, pour un mot de passe, qui est différent de l'autre annuaire, car créé aléatoirement).
Net::LDAP
Nous avons donc vu pour l'essentiel les deux modules Net::LDAP
et
Net::LDAP::LDIF
. Sachez qu'il existe d'autres sous-modules à Net::LDAP
, que vous
pouvez voir avec i /Net::LDAP/
sur la ligne de commande de cpan
.
Net::LDAP::LDIF
:
Création et manipulation de LDIF à partir d'objets de type Net::LDAP::Entry
.
Cela peut être intéressant à utiliser en complément ou en remplacement de
Data::Dumper
. Le module permet de générer du LDIF sous diverses formes, comme
par exemple avec un encodage base 64, ce qui autorise la sortie de chaînes de
caractères UTF-8 sur un système iso-8859-1, mais aussi les photos et autres
attributs binaires.
Net::LDAP::Schema
:
Récupération et manipulation d'un schéma LDAP depuis le serveur ou un fichier
LDIF (qui est un format différent de ce que demande OpenLDAP dans ses fichiers
de configuration). Ce module permet d'écrire facilement sur disque un schéma
OpenLDAP au format LDIF, tout en sélectionnant les classes d'objets que l'on
désire. En effet, il fournit au sein de la même structure de données (un
hachage) le vidage du schéma au format Net::LDAP::Entry
(qui se transforme
aisément en format LDIF, cf. point ci-dessus), ainsi que le découpage des
objets, attributs, syntaxes, OID et règles de correspondances en hachages. Ce
qui permet de retrouver rapidement ses petits.
Net::LDAP::DSML
:
Pour ceux qui font des annuaires leur métier, ce qui comprend l'écriture de schéma. DSML (Directory Service Markup Language) est la vision XML des schémas et des données LDAP.
Net::LDAP::RootDSE
:
Permet d'interroger la racine de l'annuaire, là où sont présentées certaines données intéressantes comme le schéma (si on veut le faire soi-même) ou les versions de protocoles supportées par le serveur.
jfenal@free.fr et jerome.fenal@logicacmg.com.
Membre de Paris.pm, Jérôme Fenal est utilisateur de GNU/Linux depuis 1994, de divers Unix (OSF/1 2.0) ou Unix-like depuis un peu plus longtemps.
Merci aux Mongueurs Marseillais, Lyonnais, Parisiens et Grenoblois qui ont assuré la relecture de cet article.
À mon père.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.