Contexte
Dans le cadre d'une migration d'annuaire, si on ne veut pas procéder au changement de produit en mode "big bang", il est généralement obligatoire de mettre en place un mécanisme de synchronisation entre l'ancienne et la nouvelle infrastructure.
Sachant que la réplication est propre à chaque produit, il est fort probable qu'il ne soit pas possible d'utiliser la réplication native des annuaires en tant que telle.
Il faut donc passer par une solution externe, soit fournie par l'éditeur lui-même (c'est le cas de DataSync chez Ping Identity, ou de ISDI chez IBM), soit fournie par un autre produit.
Nous allons étudier ici le fonctionnement de LSC Project lsc-project.org, qui permet ce genre de choses.
La documentation (https://lsc-project.org/documentation/latest/start) du produit a été utilisée pour réaliser ces tests.
Pré-requis
LSC est écrit en Java, et utilise une JRE en version 1.6 au minimum. Avant d'installer, on s'assure que la variable JAVA_HOME est bien définie. Note : LSC n'est pas compatible avec Java 11. Il faut donc au maximum un Java 8:
yum install -y java-1.8.0-openjdk.x86_64
export JAVA_HOME=/usr/lib/jvm/jre-openjdk
echo $JAVA_HOME
/usr/lib/jvm/jre-openjdk
java -version
openjdk version "1.8.0_345"
OpenJDK Runtime Environment (build 1.8.0_345-b01)
OpenJDK 64-Bit Server VM (build 25.345-b01, mixed mode)
Description de l'environnement de test
Notre environnement de test se compose donc de 3 serveurs (containers LXC) :
- un serveur CentOS 7, avec ODSEE 11 (Sun-Directory-Server/11.1.1.5.0)
- un serveur AlmaLinux 8, avec 389DS 2.0.15
- un serveur AlmaLinux 8, avec Java 8 et LSC (LSC ne fonctionne pas avec une version Java supérieure)
Les données ont été exportées depuis l'annuaire ODSEE :
/opt/dsee7/bin/dsadm export /path/to/{instance} {root suffix} /path/to/LDIF.file
Par exemple : /opt/dsee7/bin/dsadm export /var/lib/dsee/example dc=example,dc=com /opt/backup/example/Export-${TODAY}.ldif
Le serveur 389DS a été installé, une instance a été créée, avec le même root suffix que l'ancien annuaire. Les données ont ensuite été importées :
dsctl {instance} ldif2db {backend} /xchange/ldap/Export-20220919.ldif
Note : l'import initial n'est pas nécessaire, puisque la synchronisation permettrait de créer les entrées. Selon la volumétrie cependant, ceci peut être plus rapide. En faisant un import, on va également récupérer les attributs opérationnels d'origine (ce qui n'est pas le cas avec la synchronisation).
On aura donc les éléments suivants :
Hostname | IP | Port | Role |
---|---|---|---|
odsee | 10.10.27.173 | 389 | Annuaire LDAP DSEE |
rhds001 | 10.10.27.73 | 389 | Annuaire LDAP 389DS |
lsc | 10.10.27.227 | Serveur de synchro LSC |
Note : on aurait pû également installer LSC sur l'un des serveurs d'annuaire.
Installation LSC
On peut télécharger à partir de la page de téléchargement lsc-project.org/download.
Plusieurs options sont possibles : archive, package RPM ou DEB, repository. Dans notre exemple, nous allons utiliser l'archive.
# cd /opt
# wget https://lsc-project.org/archives/lsc-core-2.1.6-dist.tar.gz
# tar zxf lsc-core-2.1.6-dist.tar.gz
# ls lsc-2.1.6/
LICENSE.txt README.md bin doc etc lib sample
Les répertoires contiennent donc :
- bin : contient les programmes (essentiellement en java + perl)
- doc : contient la documentation en html
- etc : configuration de LSC
- lib : librairies Java LSC
- sample : contient des exemples de configuration.
Configuration de LSC
La configuration de LSC se trouve dans un fichier principal : etc/lsc.xml
.
Comme stipulé dans la documentation :
- l'ordre des paramètres est important
- les entités XML telles que & ou < doivent être converties en leur équivalent html (&) ou incluses dans un tag CDATA.
Un fichier lsc.xml-sample est fourni avec l'archive. On peut donc commencer par le copier :
cp etc/lsc.xml-sample etc/lsc.xml
Contenu de la configuration
Le fichier comporte plusieurs sections :
- connections : définit la liste des connexions à utiliser. On doit en avoir 2 au minimum
- tasks : propriétés décrivant la manière de synchroniser une source avec une cible.
- audits : définition du nom du fichier d'audit pour les opérations
- security : définit la clé de chiffrement et l'algorithme pour les attributs à chiffrer
Connexions
Pour une connexion de type LDAP configuration/connections/ldap, on aura les informations suivantes :
- name : nom de la connexion
- url : URL de connexion, au format ldap://{host}:{port}/{root suffix}
- username : DN du compte utilisé pour la connexion
- password : mot de passe du compte utilisé pour la connexion
- authentication : mode d'authentification (SIMPLE, NONE, SASL; etc)
- referral : mode de gestion du referral
- derefAliases : mode de gestion des alias
- version : précise la version LDAP. Utiliser VERSION_3
- pageSize : -1 ou la taille de la page
- factory : nom de la factory Java
- tlsActivated : true si on utilise le SSL, false sinon.
Si on veut sychroniser les données avec un compte de l'annuaire, il faudra démarrer en ayant chargé le compte dans les données au début, ou démarrer avec le compte système (ex : cn=Directory manager), puis une fois la première sychro lancée, utiliser un compte ayant suffisamment de droits.
Exemple de définition pour les annuaires LDAP de notre exemple :
<connections>
<ldapConnection>
<name>ldap-odsee</name>
<url>ldap://odsee:1389/dc=example,dc=com</url>
<username>cn=Directory Manager</username>
<password>secret</password>
<authentication>SIMPLE</authentication>
<referral>IGNORE</referral>
<derefAliases>NEVER</derefAliases>
<version>VERSION_3</version>
<pageSize>-1</pageSize>
<factory>com.sun.jndi.ldap.LdapCtxFactory</factory>
<tlsActivated>false</tlsActivated>
</ldapConnection>
<ldapConnection>
<name>ldap-rhds</name>
<url>ldap://rhds001:389/dc=example,dc=com</url>
<username>cn=Directory Manager</username>
<password>Pa55word_4_DS</password>
<authentication>SIMPLE</authentication>
<referral>IGNORE</referral>
<derefAliases>NEVER</derefAliases>
<version>VERSION_3</version>
<pageSize>-1</pageSize>
<factory>com.sun.jndi.ldap.LdapCtxFactory</factory>
<tlsActivated>false</tlsActivated>
</ldapConnection>
</connections>
Tasks
Les tâches définissent les actions à réaliser entre une source et une destination. Une tâche contient les paramètres suivants :
- name : nom de la tâche
- bean : nom du JavaBean. Par défaut, org.lsc.beans.SimpleBean
- cleanHook : nom d'une méthode à invoquer après la tâche de nettoyage
- syncHook : nom d'une méthode à invoquer après la synchronisation
Pour chaque tâche on définit également :
- un service source
- un service cible
Dans chaque service on va paramétrer le DN de base, les requêtes de recherche, la liste des attributs à récupérer et les conditions.
On peut donc filtrer sur les attributs que l'on veut comparer, et les DN de base.
On peut utiliser du script (JavaScript ou Groovy) dans différents endroits du traitement :
- mainIdentifier
- conditions
- dataset (opérations sur les attributs).
Il est donc possible de calculer des attributs de la cible en fonction d'attributs de la source. Par exemple, si on veut calculer un attribut displayName en concaténant le prénom et le nom en majuscules :
<dataset>
<name>displayname</name>
<policy>FORCE</policy>
<defaultValues>
</defaultValues>
<forceValues>
<string>js:srcBean.getDatasetFirstValueById("givenname") + " " + srcBean.getDatasetFirstValueById("sn").toUpperCase()
</string></forceValues>
<createValues></createValues>
</dataset>
Note : dans le cas de calcul, il faut également que l'attribut soit dans la liste des attributs récupérés (fetchedAttributes).
Les conditions déterminent quelles actions sont possibles, et sous quelle condition. Dans l'exemple suivant, on gère toujours les créations, on ne fait la mise à jour que si la source a été modifiée ultérieurement à la cible, et on traite pas les suppressions ou changements de UID :
<conditions>
<create>true</create>
<update>srcBean.getDatasetFirstValueById('modifyTimeStamp') > dstBean.getDatasetFirstValueById('modifyTimeStamp')</update>
<delete>false</delete>
<changeId>false</changeId>
</conditions>
Extrait de ma configuration d'exemple avec la tâche de sychronisation DSEE vers 389DS :
<tasks>
<task>
<name>odsee-people-2-rhds</name>
<bean>org.lsc.beans.SimpleBean</bean>
<ldapSourceService>
<name>dsee-src-service</name>
<connection reference="ldap-odsee" />
<baseDn>ou=People,dc=example,dc=com</baseDn>
<pivotAttributes>
<string>uid</string>
</pivotAttributes>
<fetchedAttributes>
<string>cn</string>
<string>givenname</string>
<string>sn</string>
<string>objectClass</string>
<string>uid</string>
<string>mail</string>
<string>employeenumber</string>
<string>telephonenumber</string>
<string>homephone</string>
<string>mobile</string>
<string>displayname</string>
<string>userPassword</string>
</fetchedAttributes>
<getAllFilter>(objectClass=inetorgperson)</getAllFilter>
<getOneFilter>(&(objectClass=inetorgperson)(uid={uid}))</getOneFilter>
<cleanFilter>(&(objectClass=inetorgperson)(uid={uid}))</cleanFilter>
</ldapSourceService>
<ldapDestinationService>
<name>ldap-rhds-service</name>
<connection reference="ldap-rhds" />
<baseDn>ou=People,dc=example,dc=com</baseDn>
<pivotAttributes>
<string>uid</string>
</pivotAttributes>
<fetchedAttributes>
<fetchedAttributes>
<string>cn</string>
<string>givenname</string>
<string>sn</string>
<string>objectClass</string>
<string>uid</string>
<string>mail</string>
<string>employeenumber</string>
<string>telephonenumber</string>
<string>homephone</string>
<string>mobile</string>
<string>displayname</string>
<string>userPassword</string>
</fetchedAttributes>
<getAllFilter>(objectClass=inetorgperson)</getAllFilter>
<getOneFilter>(&(objectClass=inetorgperson)(uid={uid}))</getOneFilter>
</ldapDestinationService>
<propertiesBasedSyncOptions>
<mainIdentifier>"uid=" + srcBean.getDatasetFirstValueById("uid") + ",ou=People,dc=example,dc=com"</mainIdentifier>
<defaultDelimiter>;</defaultDelimiter>
<defaultPolicy>FORCE</defaultPolicy>
<conditions>
<create>true</create>
<update>true</update>
<delete>true</delete>
<changeId>false</changeId>
</conditions>
<dataset>
<name>objectClass</name>
<policy>KEEP</policy>
<defaultValues></defaultValues>
<forceValues></forceValues>
<createValues>
<string>"inetorgperson"</string>
<string>"organizationalperson"</string>
<string>"person"</string>
<string>"top"</string>
</createValues>
<delimiter>,</delimiter>
</dataset>
<dataset>
<name>userPassword</name>
<policy>KEEP</policy>
<defaultValues>
<string>"change_this_2022"</string>
</defaultValues>
<forceValues></forceValues>
<createValues></createValues>
</dataset>
<dataset>
<name>displayname</name>
<policy>FORCE</policy>
<defaultValues>
</defaultValues>
<forceValues>
<string>js:srcBean.getDatasetFirstValueById("givenname") + " " + srcBean.getDatasetFirstValueById("sn").toUpperCase()
</string></forceValues>
<createValues></createValues>
</dataset>
</propertiesBasedSyncOptions>
</task>
Sur la destination on définit des opérations spécifiques sur certains attributs (ce sont les tags dataset) :
- objectClass : en création, on va positionner les classes d'objets (ça peut être utile si les annuaires n'ont pas le même schéma)
- userPassword : on met une valeur par défaut s'il n'en existe pas
- displayname : il est toujours recalculé, sur la base du prénom et du nom de famille en majuscules.
On voit dans les conditions que l'on gère les opérations de création, mise à jour et suppression.
Validation de la configuration
bin/lsc -f /opt/lsc-2.1.6/etc/ -v
L'option -f- ou --config permet de définir le répertoire de configuration. Ceci peut être utilise si on veut gérer des versions bien distinctes des connecteurs et tâches.
Lancement de la synchronisation
LSC peut fonctionner en mode synchrone , asynchrone ou clean. Dans ce deuxième cas, LSC tourne en mode daemon et relance les tâches toutes les 5 secondes, avec un filtre de recherche sur l'attribut modifyTimestamp.
Note : il faudra penser dans ce cas à indexer l'attribut.
En mode clean, LSC va rechercher les objets qui ont disparu, et va les supprimer sur la destination.
En mode synchrone
Une fois que la configuration est faite, on peut lancer avec :
/path/to/lsc [-f /path/to/configuration] -s all # pour lancer toutes les tâches
/path/to/lsc -s task1,task3 # pour lancer les tâches nominativement, et dans l'ordre affiché
Par exemple, si on a modifié une entrée côté ODSEE :
bin/lsc -f /opt/lsc-2.1.6/etc -s odsee-people-2-rhds
.../...
Sep 19 14:16:05 - INFO - Logging configuration successfully loaded from /opt/lsc-2.1.6/etc/logback.xml
Sep 19 14:16:05 - INFO - LSC configuration successfully loaded from /opt/lsc-2.1.6/etc/
Sep 19 14:16:05 - INFO - Connecting to LDAP server ldap://rhds001:389/dc=example,dc=com as cn=Directory Manager
Sep 19 14:16:05 - INFO - Connecting to LDAP server ldap://odsee:1389/dc=example,dc=com as cn=Directory Manager
Sep 19 14:16:05 - INFO - Starting sync for odsee-people-2-rhds
Sep 19 14:16:12 - INFO - # Updating object uid=user.876,ou=People,dc=example,dc=com for odsee-people-2-rhds
# Mon Sep 19 14:16:12 UTC 2022
dn: uid=user.876,ou=People,dc=example,dc=com
changetype: modify
replace: l
l: Croix
-
replace: street
street: 876 Avenue de la république
-
replace: postalcode
postalcode: 59170
-
Sep 19 14:16:34 - INFO - All entries: 10154, to modify entries: 1, successfully modified entries: 1, errors: 0
En mode asynchrone
La syntaxe est sensiblement la même :
/path/to/lsc -a all # pour lancer toutes les tâches
/path/to/lsc -a task1,task3 # pour lancer les tâches nominativement, et dans l'ordre affiché
Dans l'exemple ci-après, on voit que LSC démarre en mode asynchrone les 2 tâches :
/opt/lsc-2.1.6/bin/lsc -s odsee-people-2-rhds,rhds-people-2-odsee
.../...
Sep 22 15:14:58 - INFO - LSC configuration successfully loaded from /opt/lsc-2.1.6/bin/../etc/
Sep 22 15:14:58 - INFO - Connecting to LDAP server ldap://rhds001:389/dc=example,dc=com as cn=Directory Manager
Sep 22 15:14:58 - INFO - Connecting to LDAP server ldap://odsee:1389/dc=example,dc=com as cn=Directory Manager
Sep 22 15:14:58 - INFO - Starting async for odsee-people-2-rhds
Sep 22 15:14:58 - INFO - Starting async for rhds-people-2-odsee
Sep 22 15:15:16 - INFO - # Updating object uid=user.1298,ou=People,dc=example,dc=com for odsee-people-2-rhds
Sep 22 15:16:59 - INFO - # Updating object uid=user.1298,ou=People,dc=example,dc=com for rhds-people-2-odsee
# Thu Sep 22 15:16:59 UTC 2022
dn: uid=user.1298,ou=People,dc=example,dc=com
Comme les modifications se basent sur les valeurs d'attributs, il n'y a pas de risque de boucle infinie, sauf dans le cas où les attributs sont recalculés, avec des formules différentes.
Un script de démarrage (au format init.d) est disponible, afin de pouvoir automatiser le lancement en mode daemon.
En mode clean
Le lancement se fait avec l'option -c :
/path/to/lsc -c all # pour lancer toutes les tâches
/path/to/lsc -c task1,task3 # pour lancer les tâches nominativement, et dans l'ordre affiché
/opt/lsc-2.1.6/bin/lsc -c odsee-people-2-rhds
.../...
Sep 22 15:37:14 - INFO - Starting clean for odsee-people-2-rhds
Sep 22 15:37:15 - INFO - # Removing object uid=user.1298,ou=People,dc=example,dc=com for odsee-people-2-rhds
# Thu Sep 22 15:37:15 UTC 2022
dn: uid=user.1298,ou=People,dc=example,dc=com
changetype: delete
On peut ajouter l'option -c en mode synchrone. Par exemple :
/opt/lsc-2.1.6/bin/lsc -s odsee-people-2-rhds -c odsee-people-2-rhds
En synthèse
LSC project est une solution intéressante pour synchroniser des services hétérogènes. La configuration est assez explicite, la documentation est bien faite.
Si on veut avoir un fonctionnement en quasi temps réel, le mode asynchrone est à privilégier. Tout dépend donc du nombre de mises à jour réalisées sur les services.
Mais cette solution permet d'éviter de dépendre de produits éditeurs, ou d'installer un produit plus lourd type IDM.