J'ai clôturé ma formation de Technicien Supérieur de Support en Informatique par un stage au Laboratoire de Physique et de Cosmologie de Grenoble. Je remercie d'ailleurs encore une fois toute l’équipe informatique qui m'a permis d'effectuer cette période en entreprise dans les meilleurs conditions.

Dans le cadre de ce stage, il m'a été demandé de mener à bien le projet suivant :

Le projet


Cahier des charges

L’authentification des utilisateurs (Windows ou Linux) du laboratoire se fait via un AD (Active Directory) installé sur un serveur Windows Server 2008. La gestion des boites de messagerie se fait quant à elle via Zimbra, ce service est externalisé au Centre de Calcul de Villeurbanne depuis un peu plus d’un an. L’authentification pour les services de messagerie Zimbra s’effectue via le serveurs AD cité précédemment. Ces deux services cohabitent très bien mais quelques améliorations peuvent être apportées afin d’améliorer l’administration des comptes de messagerie.

Lorsqu’un utilisateur quitte le laboratoire, son compte AD est désactivé mais son compte mail reste actif et ses mails restent stockés sur les serveurs ce qui occasionne un stockage inutile considérable, jusqu’à ce que ceux-ci soient supprimés manuellement par un administrateur.

Il serait judicieux que les comptes et les boites mails soient supprimés automatiquement, au bout d’un an de désactivation du compte AD par exemple.

Mon tuteur m’a chargé de résoudre cette problématique.

La solution retenue est la création d’un script d’automatisation écrit en Bash.

Après étude il s’avère que deux scripts vont être nécessaires.

Le premier aura en charge d’identifier et de lister dans un fichier au format texte les comptes AD désactivés.

Le second se chargera de supprimer les comptes et boites mails Zimbra en utilisant la liste des comptes générée par le premier script.

Ces deux scripts ne peuvent pas être fusionnés car avant la suppression d’un compte il faut impérativement une approbation de la part du responsable du groupe du compte supprimé. Un délai doit donc être respecté entre l’identification des utilisateurs et les opérations de suppression.

Ces scripts devront être exécutés automatiquement de façon régulière afin  de minimiser le nombre de comptes mails.

 

Création des scripts

Schéma

 

Le schéma ci-dessous explique le principe de fonctionnement de mon projet :

checkOldUid.sh : Identification des utilisateurs

La requête ‘ldapsearch’

Avant d’écrire ce code, j’ai dû passer par des étapes d’étude et de réflexion afin de bien comprendre la problématique et le résultat attendu. Sur les conseils de  Bernard, mon tuteur, j'ai documenté au maximum mon script afin qu'il soit le plus compréhensible possible et qu'il puisse ainsi être repris et modifié plus tard par une tierce personne.

Dans un premier temps il a fallu réfléchir sur les outils et les méthodes permettant d’interroger l'annuaire LDAP d'Active Directory afin de faire le choix le plus adéquat.

En effet, il existe plusieurs méthodes pour effectuer des recherches dans l'annuaire LDAP : Par exemple via une interface WEB SE3 (SambaEdu), en PHP ou bien encore en ligne de commande avec des requêtes de type 'ldapsearch'.

Le plus adapté s'est révélé être 'ldapsearch', de par sa facilité d'intégration dans un script Bash. J’ai donc opté pour cette solution après l’avoir présentée à mon tuteur.

La commande 'ldapsearch' permet donc d'effectuer des recherches dans un annuaire LDAP et de présenter le résultat selon le format LDIF (format permettant de présenter le contenu d'un annuaire LDAP sous la forme d'un fichier texte).

Je liste ici quelques-unes des options de 'ldapsearch' qui m'ont été utiles :

'-x' pour utiliser une authentification simple.

'-LLL' pour supprimer tous les commentaires.

'-b' pour préciser la branche dans laquelle effectuer la recherche.

'-D' pour indiquer le compte administrateur du LDAP et ainsi accéder à davantage d'attributs des entrées.

'-W' pour se voir demander le mot de passe du compte administrateur du LDAP.

'-w' pour fournir dans la ligne de commande le mot de passe du compte administrateur du LDAP

'-h' pour donner l'adresse du serveur LDAP à interroger.

Voici ma requête LDAP v1 :

ldapsearch -x -LLL -D "CN=RO_LDAP,CN=Users,DC=grenoble,DC=in2p3,DC=fr" -w ******** -h lpsc0200w.grenoble.in2p3.fr -s sub -b "DC=grenoble,DC=in2p3,DC=fr" '(&(!(objectClass=computer))(objectClass=person))' "*"

La requête ci-dessus me permet d'extraire la totalité du contenu de l'annuaire. J'optimiserai plus tard cette commande afin de réduire les temps de traitement.

J'enregistrerai aussi certaines données dans des variables afin de rendre le code plus lisible et plus facilement modifiable.

Voici une partie des résultats de cette requête concernant mon propre compte :

accountExpires: 130934016000000000

logonCount: 442

sAMAccountName: bronsard

sAMAccountType: 805306368

userPrincipalName: Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser.

lockoutTime: 0

objectCategory: CN=Person,CN=Schema,CN=Configuration,DC=in2p3,DC=fr

dSCorePropagationData: 16010101000000.0Z

lastLogonTimestamp: 130885073431672389

msDS-SupportedEncryptionTypes: 0

uid: bronsard

msSFU30Name: bronsard

msSFU30NisDomain: grenoble

uidNumber: 10054

gidNumber: 1020

gecos: Gilles BRONSARD

unixHomeDirectory: //informatique/bronsard

loginShell: /bin/bash

 

Les attributs  qui vont m'être particulièrement utiles sont les suivants :

  • uid
  • accountExpires
  • gidNumber
  • unixHomeDirectory
  • loginShell

L'uid est l'identifiant de l'utilisateur, j'en aurai forcément besoin pour identifier les comptes à traiter.

L'accountExpires est la date à laquelle le compte expire. Cette valeur représente le nombre de paquets de 100 nanosecondes depuis le 1er Janvier 1601.

Le gidNumber est le numéro de groupe auquel appartient le compte

L'unixHomeDirectory contient le chemin absolu du répertoire de l'utilisateur

Le loginShell contient le chemin absolu du shell par défaut.

Les deux attributs qui vont nécessiter le plus de traitement sont accountExpires et dans une moindre mesure gidNumber.

En effet les valeurs numériques renvoyées, bien adaptées pour le traitement, devront être, à un moment ou un autre, transformées afin d’être plus humainement compréhensible.

Les Fonctions

Pour la réalisation de ce script, la création de fonctions est vite devenue une évidence. En effet, plutôt que de dupliquer les lignes de code il est préférable de les regrouper dans une fonction que je pourrai appeler à tout moment dans le script. Quelques règles sont à connaitre si l’on souhaite utiliser les fonctions en Bash (merci à Mickael Dorigny du site www.it-connect.fr pour ses articles dont je me suis inspiré ici) :

Une fonction peut être placée n’importe où dans le script, au début comme à la fin, avant comme après son appel.

Les variables qu’une fonction utilise sont globales par défaut, cela signifie qu’elles sont les mêmes hors et à l’intérieur d’une fonction.

Les fonctions, au même titre qu’un script entier, utilisent les variables spéciales.

Ci-dessous la fonction qui permet de rendre plus compréhensible la valeur retournée par accountExpires dans mon script :

#FONCTION convert_accountExpires_date

#en entrée reçoit la valeur de "accountExpires"

#exemple : convert_accountExpires_date 130190112000000000 - résultat : Mon Jul 22 23:13:20 CEST 2013

function convert_accountExpires_date() {

      #nombre de seconde apres 1601

      secsAfterADEpoch=$(( $1/10000000 ))

      #difference en seconde entre 1970 et 1601

      AD2Unix=11644473600

      #passage de la date AD en date Unix

      unixTimeStamp=$(( $secsAfterADEpoch-$AD2Unix ))

      #date du jour

      date_jr=$( /usr/bin/date +%s )

      #nb de secondes d'écart entre la date Unix et la date du jour

      #moins 10000 car il y avait un delta d'un jour par rapport au resultat reel

      inter=$(( $unixTimeStamp-date_jr-10000 ))     

      #date du jour moins intervalle entre la date Unix

      date_acc_exp=$(( $date_jr+$inter ))

      #convertion en format date

      date -d @$date_acc_exp

      }

Le résultat : Mon Jul 22 23:13:20 CEST 2013 , est quand même bien plus parlant que : 130190112000000000 !

Une fonction est délimitée par deux accolades { }. Nous pouvons définir autant de fonctions qu’on le souhaite pourvu qu’elles aient toutes un nom différent.

Passage de paramètres

Les scripts Bash ont un certain nombre de variables spéciales qui permettent de gérer les paramètres passés lors de leur appel. Nous pouvons donc appeler notre fonction avec des paramètres pour que celle-ci les traites avec les variables spéciales (récupérer le nombre de paramètres, récupérer la valeur du premier ou du second paramètre, etc…).

Ici ma fonction  convert_accountExpires_date recevra en paramètre « $1 » la valeur de accountExpires.

Un paramètre est donc une valeur que l’on envoie à la fonction lors de son appel en lui disant : “tu vas traiter cette information“. Il faut bien sûr que dans la fonction, on mette les variables spéciales adéquat au traitement ( $1 pour récupérer la valeur du premier paramètre passé, $2 pour le second, etc...).

Les variables locales

Comme dit précédemment, les variables par défaut sont globales dans les fonctions.

Si on définit et initialise la variable hors de la fonction (en dehors des accolades), la valeur sera la même si on l’appelle (afficher son contenu par exemple avec “echo”) dans la fonction qu’à l’extérieur de celle-ci. De la même façon, si on modifie cette variable dans la fonction, la modification est valable pour tout le reste du script après l’appel de la fonction.

Bien que je n'en ai pas eu besoin dans ce script, il peut être utile de savoir que pour définir une variable comme locale (valable uniquement pour l’intérieur de la fonction), on utilisera le terme “local” avant la définition de la fonction comme suivant :

local AD2Unix=11644473600

L’utilisation qu’on fait des fonctions en Bash est généralement très variée mais le principe de construction d’une fonction reste toujours le même.

Les Boucles

Voici d’autres fonctionnalités dont je vais devoir user et abuser.

Ces structures permettent de répéter autant de fois que nécessaire une partie du code.

Celle que j'ai le plus utilisé dans ce script est la boucle WHILE qui permet de boucler tant qu'une condition est remplie.

Le principe est de faire un code qui ressemble à ceci :

TANT QUE test
FAIRE
------> effectuer_une_action
RECOMMENCER

Ce qui dans le script va s’écrire:

while [ test ]
do
echo 'Action en boucle'
done

J'ai surtout utilisé la boucle WHILE pour lire le contenu de fichier ligne par ligne, à l'aide de sa commande interne «read» :

while read ligne 
do
      commande
done < fichier

ce qui donne :

while read uid nom
do
      echo -e "$nom\n"
done < uid_expi

Il  est tout à fait possible à partir d'un fichier structuré, de récupérer les valeurs de chaque champ et de les affecter à plusieurs variables avec la commande "read". Attention toutefois de bien assigner à la variable "IFS" le bon séparateur de champ (espace par défaut).

while IFS=: read user uid gid full

do 

echo -e "$full :\n\ 

 Pseudo : $user\n\ 

 UID :\t $uid\n\ 

 GID :\t $gid\n\   

done < all_uid

Voici la première boucle  que j'ai créée :

#requête pour récupérer tous les UID de l'AD dans le fichier temporaire all_uid

      $myLdapSearch uid=* | grep uid: | sed -e "s/uid: //g" >> all_uid

#création de $loginShell $accountExpires $homeDirectory $gidNumero $expire $lisible_date, qui vont servir à alimenter le fichier accountExpi

while read uid

      do  

loginShell=`$myLdapSearch uid=$uid loginShell | grep loginShell | sed -e "s/loginShell: //g"`

accountExpires=`$myLdapSearch uid=$uid accountExpires | grep accountExpires | sed -e "s/accountExpires: //g"`

homeDirectory=`$myLdapSearch uid=$uid homeDirectory | grep homeDirectory | sed -e "s/homeDirectory: //g"`

gidNumero=`$myLdapSearch uid=$uid gidNumber | grep gidNumber | sed -e "s/gidNumber: //g"`

expire=`if check_expire_date $accountExpires; then echo expire; else echo actif; fi`

lisible_date=`convert_accountExpires_date $accountExpires`

       #creation de 2 fichiers de données (1 seul fichier générant plusieurs erreurs)

echo -e "$expire _"$gidNumero"_ ¤ $uid ¤ depuis $lisible_date ¤" | grep expire >> accountEx

echo "$expire $homeDirectory" | grep expire >> homeDirector

      done < all_uid

Cette boucle m'a permis, après avoir récupéré tous les UID via la première requête LDAP, de créer les variables qui vont me permettre d’alimenter mon script.

La boucle FOR, quant à elle, permet de parcourir une liste de valeurs et de boucler autant de fois qu'il y a de valeurs. À l'intérieur de la boucle, une variable prend successivement les valeurs indiquées.

POUR variable PRENANT valeur1 valeur2 valeur3

FAIRE

------> effectuer_une_action

VALEUR_SUIVANTE

Ce qui dans le script va s’écrire:

for variable in 'valeur1' 'valeur2' 'valeur3'

do

      echo "La variable vaut $variable"

done

La condition SI

Je vais aussi beaucoup utiliser la condition SI :

Les conditions constituent un moyen de dire dans le script « SI cette variable vaut tant, ALORS fais ceci, SINON fais cela ».

La condition IF prend cette forme:

SI test_de_variable

ALORS

------> effectuer_une_action

FIN SI

La syntaxe en Bash est la suivante :

if [ test ]

then

      echo "C'est vrai"

fi

Le mot fi à la fin indique que le if s'arrête là. Tout ce qui est entre le then et le fi sera exécuté uniquement si le test est vérifié.

Il y a des espaces à l'intérieur des crochets. On ne doit pas écrire [test] mais [ test ]

Il existe une autre façon d'écrire le if : en plaçant le then sur la même ligne. Dans ce cas, il ne faut pas oublier de rajouter un point-virgule après les crochets :

if [ test ]; then

      echo "C'est vrai"

fi

C'est à la place du mot « test » qu'il faut tester la valeur d'une variable.

ville="Grenoble"

 

if [ $ville = "Grenoble" ]

then

      echo "Bienvenue à Grenoble!"

fi

Comme $ville est bien égal à « Grenoble », ce script affichera :

Bienvenue à Grenoble!

On peut aussi tester deux variables à la fois dans le if :

#dédicace à mes formateurs de l'AFPA ;-)

nom1="Daniel"

nom2="Olivier"

if [ $nom1 = $nom2 ]

then

      echo "Salut les jumeaux !"

fi

Comme ici $nom1 est différent de $nom2, le contenu du if ne sera pas exécuté. Le script n'affichera donc rien.

Sinon

Si l'on souhaite faire quelque chose de particulier quand la condition n'est pas remplie, on peut rajouter un else qui signifie « sinon ».

SI test_de_variable

ALORS

------> effectuer_une_action

SINON

------> effectuer_une_action

FIN SI

if [ test ]

then

      echo "C'est vrai"

else

      echo "C'est faux"

fi

Voici comment j'ai utilisé la condition IF dans une de mes fonctions :

#FONCTION check_expire_date

#en entrée reçoit la valeur de "accountExpires"

#exemple : check_expire_date #130190112000000000

      #130854528000000000 = dimanche 30 aout 2015

      #130190112000000000 = Lundi 22 juillet 2013

function check_expire_date() {

      #nombre de seconde apres 1601

      secsAfterADEpoch=$(( $1/10000000 ))

      #difference en seconde entre 1970 et 1601

      AD2Unix=11644473600

      #passage de la date AD en date Unix

      unixTimeStamp=$(( $secsAfterADEpoch-$AD2Unix ))

      #date du jour

      date_jr=$( /usr/bin/date +%s )

      #nb de secondes d'écart entre la date Unix et la date du jour

      #moins 10000 car il y avait un delta d'un jour par rapport au resultat reel

      inter=$(( $unixTimeStamp-date_jr-10000 ))

      # conversion en jours

      inter=$(( $inter/86400 ))

      #nb de jours depuis expiration (-365 = un an)

      delais=-365

      #test si la date d'expiration du compte date de plus d'un an

            #-gt = greater than; -lt = less than; -eq = equal

            #-ge = greater or equal; -le = less or equal; -ne = not equal

            #les valeurs 0 et 9223372036854775807 correspondent à des valeurs nulles du champ "Date d'expiration du compte" dans l'AD

      if (test $1 -ne 9223372036854775807) && (test $1 -ne 0) && (test $delais -gt $inter);

            then return 0

            else return 1    

      fi

      }

J'ai remarqué que deux valeurs d'accountExpires reviennent souvent :  9223372036854775807 et 0. Après vérification dans l'AD, il s’avère que celles-ci correspondent à des valeurs nulles du champ "Date d'expiration du compte".

Je l’ai pris en compte dans ma fonction : Si la valeur retournée par $1 n'est pas  9223372036854775807, 0 ou que la valeur de $delais est supérieure à la valeur de $inter, elle retourne 0 pour vrai et 1 pour faux.

On évite ainsi des traitements inutiles et cela permet de gagner quelques millisecondes sur le temps d’exécution de cette fonction.

Je vais maintenant vous présenter d'autres fonctions que j'ai utilisées dans ce script :

function users_exp_by_group() {

 

      cat uid_expi| cut -d\  -f1| sort -u | \

      while read GID

            do

      users=`grep ^$1 uid_expi| cut -d\  -f3-15`

      echo $users        

            done

}

Cette fonction a pour but de trier les UID des comptes expirés par groupe (GID), afin de pouvoir par la suite envoyer des mails aux responsables de service des personnes concernées.

J'alimente ma fonction avec le fichier uid_expi que j'ai créé comme ceci :

#création de 2 fichiers de données (1 seul fichier générant plusieurs erreurs)

      echo -e "$expire _"$gidNumero"_ ¤ $uid ¤ depuis $lisible_date ¤" | grep expire >> accountEx

      echo "$expire $homeDirectory" | grep expire >> homeDirector

      done < all_uid

#rajoute le caractère § qui permettra plus tard de faire des sauts de lignes

sed -i 's/$/§/' homeDirector

#supprime le mot expire qui fait doublon

cat homeDirector | sed -e "s/expire//g" >> homeDirectory

#fusion des 2 fichiers en un

paste accountEx homeDirectory >> accountExpi

#récupère le gid et l'uid des comptes expirés

      cat accountExpi | cut -d\  "-f2-15" | sort | sed -e "s/^ //g" >> uid_exp

      cat uid_exp | sed -e "s/\t / /g" >> uid_expi

Je récupère la colonne qui m’intéresse grâce à la commande CUT

Ensuite je trie et supprime les doublons grâce à la commande SORT et son option -u

Une nouvelle fois à l'aide d'une boucle WHILE et de l'option READ je vais lire ligne par ligne chaque GID et trouver les UID correspondants au GID qui sera passé en paramètre de ma fonction, comme ci-dessous :

#boucle pour envoyer les mails aux services connus

      cat uid_expi | cut -d\  -f1| sort -u | \

      while read GID;

            do

            uidd=`users_exp_by_group "$GID" | sort -u | sed 's/¤/\n/g'| sed 's/§/\n\n\n/g'` && destinataire=`grep "$GID" table_groupe.txt| cut -d\  -f3` && service=`grep "$GID" table_groupe.txt| cut -d\  -f2` && envoi_mail "$destinataire" "$uidd" "$service" 

            done

La fonction ci-dessous sert à envoyer des mails aux responsables concernés par les comptes expirés :

#FONCTION envoie de mail

#reçoit en $1 le mail du destinataire, en $2 la variable contenant les uid expirés

function envoi_mail() {

mail_destinataire=$1; nom_compte_exp=$2; nom_service=$3

dest_Cc="$mail_destinataire"

mail_from="MAIL FROM: $1"

for rcpt in $dest_Cc; do

com_rcpt="RCPT TO: $rcpt\n${com_rcpt}"

done

(

      cat<<END

$mail_from

`echo -e RCPT TO: $mail_destinataire`

DATA

To: $mail_destinataire

Subject: Fermeture de compte mail $nom_service

Bonjour,

 

Ce message a été envoyé automatiquement par le service informatique.

 

Les comptes des utilisateurs ci-dessous, sont désactivés depuis plus d'un an.

Par conséquent, sans manifestation de votre part dans les plus brefs délais, le service informatique va procéder à la suppression définitive des courriers électroniques liés à ces comptes.

 

 

Service concerné : $nom_service

 

 $nom_compte_exp

 

 

Cordialement.

 

Le service informatique

.

END

)| /usr/bin/iconv -f UTF-8 -t ISO-8859-1 | /usr/bin/nc lpsc-mail 25 >& /dev/null

}

En effet, afin d'éviter toute erreur, il est plus prudent de faire vérifier les listes des utilisateurs concernés par leurs anciens chefs de services respectifs.

Cette fonction sera utilisée comme ceci :

while read GID;

            do

            uidd=`users_exp_by_group "$GID" | sort -u | sed 's/¤/\n/g'| sed 's/§/\n\n\n/g'` && destinataire=`grep "$GID" table_groupe.txt| cut -d\  -f3` && service=`grep "$GID" table_groupe.txt| cut -d\  -f2` && envoi_mail "$destinataire" "$uidd" "$service" 

            done

 

zimbraSuppr.sh : Suppression des comptes mails

Maintenant que les comptes obsolètes sont identifiés, que des mails d'avertissements ont été envoyés aux différents chefs de services concernés, nous pouvons, sauf contre ordre de ces derniers, supprimer les comptes mails correspondants. Pour plus de sécurité, lors de l’exécution de ce script une confirmation sera demandée. Cependant, une option sera implémentée afin de passer outre cette confirmation.

La liste des comptes à supprimer générée par mon premier script est au format texte et servira de données sources pour la suppression des comptes mails.

Pour mon second script j’ai utilisé la commande zmprov de Zimbra. Cette commande permet d’effectuer une multitude d’opérations sur les comptes mails comme le montrent les exemples ci-dessous :

# création du compte Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser.

      ssh Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser. -i .ssh/id_dsa_zimbra_gb "/usr/bin/sudo /opt/zadm/bin/zmprov.sh createAccount Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser. \"\" cn \"bob leponge\" givenName \"bob\" sn \"leponge\" displayName \"bob leponge\" company \"LPSC\" "

#pour vérifier que l'adresse de toto a bien été créé

      ssh Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser. -i .ssh/id_dsa_zimbra_gb "/usr/bin/sudo /opt/zadm/bin/zmprov.sh gaa -v lpsc.in2p3.fr | grep toto"

# Suppression du compte Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser. 

                ssh Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser. -i id_dsa_zimbra_gb "/usr/bin/sudo /opt/zadm/bin/zmprov.sh deleteAccount Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser."

La commande qui va nous intéresser est deleteAccount.

Comparé au premier script, celui-ci est plus simple à créer, en effet j'ai juste à utiliser l'option de suppression de compte de zmprov. Je l'ai inséré dans une boucle puis j'en ai fait une fonction afin de facilité son intégration dans un test demandant la confirmation pour supprimer ces comptes.

L'option --force passée en attribut au script permet de s’affranchir de cette confirmation.

Conclusion