Mon CV

LdapSync : une synchronisation d’un groupe OpenDirectory vers une branche LDAP (OrganizationalUnit / groupOfNames) avec une alternative à Overlay / memberOf

3 janvier 2014

Je continue mon épopée avec l’utilisation du LDAP fourni par l’OpenDirectory de Mac OS X Server. J’ai eu besoin récemment d’interfacer à celui-ci une application qui ne sait travailler qu’avec certains objets particuliers du LDAP (ici des unités d’organisation et des groupOfNames).

J’avais repéré il y a quelques temps un article de Yoann Gini qui traitait déjà de ce sujet et proposait à la fois un script pour synchroniser un groupe de l’OpenDirectory avec un groupe de type groupOfNames du LDAP. Je suis reparti de cette proposition fort pertinente.

Mais, j’ai essayé d’ajouter une fonction qui me manque régulièrement sur les groupes de l’OpenDirectory, à savoir la présence d’un attribut de type memberOf dans les entrées utilisateurs, permettant ainsi de faire des requêtes de type '(memberOf=xxxx)'. C’est particulièrement utile pour restreindre l’accès à l’authentification pour certaines applications à un certain groupe d’utilisateurs par exemple.

Etape 1 : créer sur le LDAP une branche de classe organizationalUnit

Dans mon cas, j’ai choisi, comme l’avais fait Yoann Gini, de créer un objet de type organizationalUnit au moyen d’Apache Directory Studio.

Une fois positionné à la racine de notre LDAP, il faut utiliser le menu LDAP > nouvelle entrée :

LDAPSync : créer une branche organizationalUnit avec Apache Directory Studio

Ensuite, sélectionnez la classe de type organizationalUnit :

LDAPSync : créer une branche organizationalUnit avec Apache Directory Studio

LDAPSync : créer une branche organizationalUnit avec Apache Directory Studio

Puis entrez un nom pour cette branche (ici ou=lists) :

LDAPSync : créer une branche organizationalUnit avec Apache Directory Studio

Etape 2 : ajouter un groupe de type groupOfNames à la branche organizationalUnit

Positionnez-vous dans la branche nouvellement créée, et utilisez à nouveau la commande LDAP > nouvelle entrée, en choisissant cette fois-ci la classe groupOfNames :

LDAPSync : créer un groupOfNames dans une organizationalUnit avec Apache Directory Studio

Puis nommez ce groupe (ici cn=workgroup) :

LDAPSync : créer un groupOfNames dans une organizationalUnit avec Apache Directory Studio

Il vous sera ensuite demandé de populer ce groupe en y affectant au minimum un premier membre, soit en rentrant manuellement son distinguished name soit en le sélectionnant avec l’interface de navigation au sein du LDAP.

Etape 3 : ajouter des utilisateurs dans l’objet de type groupOfNames au moyen de ldapSync

Mon adaptation du script de Yoann Gini est la suivante :

#!/bin/bash

#-------------------------------------------#
#                ldapSync                   #
#-------------------------------------------#
#                                           #
# Synchronize OD Group to LDAP GroupOfNames #
#				 (One way)                  #
#                                           #
#	fork of a script written by Yoann Gini  #
#      (VERSION 0.1 -- Dec. 15, 2010)       #
# 	       http://goo.gl/lVnjFw             #
#                                           #
#	     VERSION 0.4 -- Jan. 2, 2013        #
#             Licenced under                #
#       Creative Commons 4.0 BY NC SA       #
#                                           #
#           http://goo.gl/9Jgf7u            #
#               Yvan Godard                 #
#           godardyvan@gmail.com            #
#                                           #
#-------------------------------------------#

# Variables initialisation
VERSION="ldapSync v0.4 - http://goo.gl/9Jgf7u - godardyvan@gmail.com"
help="no"
SCRIPT_DIR=$(dirname $0)
SCRIPT_NAME=$(basename $0)
LDAP_SERVER="ldap://127.0.0.1"
LDAP_DN_BASE=""
LDAP_GROUP_DN=""
LDAP_ADMIN_UID="diradmin"
LDAP_ADMIN_PASS=""
LDAP_DN_USER_BRANCH="cn=users"
DSCL_SEARCH_PATH="/Search"
DSCL_GROUP_NAME=""
EMAIL_REPORT="nomail"
EMAIL_LEVEL=0
LOG="/var/log/ldapSync.log"
LOG_ACTIVE=0
LOG_TEMP=$(mktemp /tmp/ldapSync_log.XXXXX)
ERROR_CODE_MODIFY_1=0
ERROR_CODE_MODIFY_2=0
ERROR_CODE_MODIFY_3=0
ERROR_CODE_MODIFY_4=0
TMP_FOLDER=/tmp/ldapSync-$(uuidgen)
COMPLETE_USER_DN=${TMP_FOLDER}/all_user_dn
ACTUAL_USER_DN=${TMP_FOLDER}/actual_user_dn
ACTUAL_USER_WITH_ATTRIBUTE=${TMP_FOLDER}/actual_user_with_attribute
LDAP_ADD_LIST=${TMP_FOLDER}/ldap_add_list
LDAP_DELETE_LIST=${TMP_FOLDER}/ldap_delete_list
LDAP_ADD_MEMBEROF=${TMP_FOLDER}/ldap_add_memberof
LDAP_DELETE_MEMBEROF=${TMP_FOLDER}/ldap_delete_memberof
MEMBER_OF_GROUP=${TMP_FOLDER}/group_members
LDIF_ATTRIBUTE_MOD_FOLDER=${TMP_FOLDER}/attribute_modif
LDIF_ATTRIBUTE_MOD_LIST=${TMP_FOLDER}/attribute_modif_list_ldif
LDIF_ERROR_APPLY=${TMP_FOLDER}/error_apply_ldif
MEMBER_OF_GROUP=${TMP_FOLDER}/group_members
LDAP_MODIFY_ADD=${TMP_FOLDER}/modif_add.ldif
LDAP_MODIFY_DELETE=${TMP_FOLDER}/modif_delete.ldif
SYNC_MEMBEROF="no"
SYNC_MEMBEROF_ATTRIBUTE="memberOf"
ATTIBUTE_USED="no"
LDAP_ADD_DESCRIPTION=${TMP_FOLDER}/modif_description.ldif
LDAP_GROUP_DESCRIPTION_FILE=${TMP_FOLDER}/LDAP_GROUP_DESCRIPTION_FILE

help () {
	echo -e "\n**********************************************************************\n"
	echo -e "    $VERSION"
	echo -e "\n**********************************************************************\n"
	echo -e "This tool is make for sync LDAP GroupOfNames with the member list of an OD Group."
	echo -e "This tool need to be run on a computer bind to the OD."
	echo -e "\nDisclamer:"
	echo -e "This tool is provide without any support and guarantee."
	echo -e "\nSynopsis:"
	echo -e "\t./${SCRIPT_NAME} [-h] | -d <LDAP base namespace> -p <LDAP Admin password> -g <relative DN of LDAP GroupOfNames> -G <OD Groupname>"
	echo -e "\t           [-a <LDAP admin UID>] [-s <LDAP server>] [-u <relative DN of user banch>]"
	echo -e "\t           [-S <DSCL Search Path>]"
	echo -e "\t           [-e <email report option>] [-E <email address>] [-j <log file>]"
	echo -e "\n\t-h:                                      prints this help then exit"
	echo -e "\nMandatory options:"
	echo -e "\t-d <LDAP base namespace>:                the base DN for each LDAP entry (i.e.: 'dc=server,dc=office,dc=com')"
	echo -e "\t-p <LDAP admin password>:                the password of the LDAP administrator (asked if missing)"
	echo -e "\t-g <relative DN of LDAP GroupOfNames>:   the LDAP GroupOfNames to update"
	echo -e "\t-G <OD Groupname>:                       the OD group used as source"
	echo -e "\nOptional options:"
	echo -e "\t-a <LDAP admin UID>:                     UID of the LDAP administrator (i.e.: 'admin', default: '${LDAP_ADMIN_UID}')"
	echo -e "\t-s <LDAP Server>:                        the LDAP server URL (default: '${LDAP_SERVER}')"
	echo -e "\t-u <relative DN of user banch>:          the relative DN of the LDAP branch that contains the users (i.e.: 'cn=allusers', default: '${LDAP_DN_USER_BRANCH}')"
	echo -e "\t-S <DSCL Search Path>:                   the DSCL Search path for OD Group (default: '${DSCL_SEARCH_PATH}')"
	echo -e "\t-m <sync memberOf>:                      add syncing attribute of type 'memberOf' in each user LDAP entry (must be 'yes' or 'no', default: '${SYNC_MEMBEROF}'')"
	echo -e "\t-M <memberOf attribute>:                 attribute to use to add the distinguished name of the groups which the user is a member"
	echo -e "\t                                         (i.e.: 'resMemberOf', default: '${SYNC_MEMBEROF_ATTRIBUTE}'), use only if '-m' is used"           
	echo -e "\t-e <email report option>:                settings for sending a report by email, must be 'onerror', 'forcemail' or 'nomail' (default: '${EMAIL_REPORT}')"
	echo -e "\t-E <email address>:                      email address to send the report, must be filled if '-e forcemail' or '-e onerror' options is used"
	echo -e "\t-j <log file>:                           enables logging instead of standard output. Specify an argument for the full path to the log file"
	echo -e "\t                                         (i.e.: '${LOG}') or use 'default' (${LOG})"
	exit 0
}

error () {
	echo -e "\n*** Error ***"
	echo -e ${1}
	echo -e "\n"${VERSION}
	alldone 1
}

alldone () {
	[ $1 -ne 0 ] && echo -e "\n**** End of process with ERROR(s) ****\n" 
	[ $1 -eq 0 ] && echo -e "\n**** End of process OK : ${SCRIPT_NAME} ****\n"
	# Redirect standard outpout
	exec 1>&6 6>&-
	# Logging if needed 
	[ ${LOG_ACTIVE} -eq 1 ] && cat ${LOG_TEMP} >> ${LOG}
	# Print current log to standard outpout
	[ ${LOG_ACTIVE} -ne 1 ] && cat ${LOG_TEMP}
	[ ${EMAIL_LEVEL} -ne 0 ] && [ $1 -ne 0 ] && cat ${LOG_TEMP} | mail -s "[ERROR: ${SCRIPT_NAME}] OD Group $DSCL_GROUP_NAME to $LDAP_GROUP_DN,$LDAP_DN_BASE" $EMAIL_ADDRESS
	[ ${EMAIL_LEVEL} -eq 2 ] && [ $1 -eq 0 ] && cat ${LOG_TEMP} | mail -s "[OK: ${SCRIPT_NAME}] OD Group $DSCL_GROUP_NAME to $LDAP_GROUP_DN,$LDAP_DN_BASE" $EMAIL_ADDRESS
	# Remove temp files/folder
	rm -R ${LOG_TEMP}
	rm -R ${TMP_FOLDER}
	exit ${1}
}

OPTS_COUNT=0

while getopts "ha:p:g:G:s:S:d:e:E:j:u:m:M:" OPTION
do
	case "$OPTION" in
		h)	help="yes"
						;;
		a)	LDAP_ADMIN_UID=${OPTARG}
						;;
		p)	LDAP_ADMIN_PASS=${OPTARG}
                        ;;
		g)	LDAP_GROUP_DN=${OPTARG}
			let OPTS_COUNT=$OPTS_COUNT+1
                        ;;
        G)	DSCL_GROUP_NAME=${OPTARG}
			let OPTS_COUNT=$OPTS_COUNT+1
                        ;;
	    d) 	LDAP_DN_BASE=${OPTARG}
			let OPTS_COUNT=$OPTS_COUNT+1
						;;
		s)	LDAP_SERVER=${OPTARG}
                        ;;
        S)	DSCL_SEARCH_PATH=${OPTARG}
                        ;; 
        e)	EMAIL_REPORT=${OPTARG}
                        ;;                             
        E)	EMAIL_ADDRESS=${OPTARG}
                        ;;
        j)	[ $OPTARG != "default" ] && LOG=${OPTARG}
			LOG_ACTIVE=1
                        ;;
        u)	LDAP_DN_USER_BRANCH=${OPTARG}
                        ;;
		m)	SYNC_MEMBEROF=${OPTARG}
                        ;;
        M)	SYNC_MEMBEROF_ATTRIBUTE=${OPTARG}
			ATTIBUTE_USED="yes"
        				;;
	esac
done

if [[ ${OPTS_COUNT} != "3" ]]
then
        help
        alldone 1
fi

if [[ ${help} = "yes" ]]
then
	help
	alldone 0
fi

if [[ ${LDAP_ADMIN_PASS} = "" ]]
	then
	echo "Password for uid=$LDAP_ADMIN_UID,$LDAP_DN_USER_BRANCH,$LDAP_DN_BASE?" 
	read -s LDAP_ADMIN_PASS
fi

# Create tmp folder
mkdir -p ${TMP_FOLDER}

# Redirect standard outpout to temp file
exec 6>&1
exec >> ${LOG_TEMP}

# Begin temp log
echo -e "\n****************************** `date` ******************************\n\n$0 for $DSCL_GROUP_NAME to $LDAP_GROUP_DN,$LDAP_DN_BASE\n"

# Test of sending email parameter and check the consistency of the parameter email address
if [[ ${EMAIL_REPORT} = "forcemail" ]]
	then
	EMAIL_LEVEL=2
	if [[ -z ${EMAIL_ADDRESS} ]]
		then
		echo -e "You use option '-e ${EMAIL_REPORT}' but you have not entered any email info.\n\t-> We continue the process without sending email."
		EMAIL_LEVEL=0
	else
		echo "${EMAIL_ADDRESS}" | grep '^[a-zA-Z0-9._-]*@[a-zA-Z0-9._-]*\.[a-zA-Z0-9._-]*$' > /dev/null 2>&1
		if [ $? -ne 0 ]
			then
    		echo -e "This address '${EMAIL_REPORT}' does not seem valid.\n\t-> We continue the process without sending email."
    		EMAIL_LEVEL=0
    	fi
    fi
elif [[ ${EMAIL_REPORT} = "onerror" ]]
	then
	EMAIL_LEVEL=1
	if [[ -z ${EMAIL_ADDRESS} ]]
		then
		echo -e "You use option '-e ${EMAIL_REPORT}' but you have not entered any email info.\n\t-> We continue the process without sending email."
		EMAIL_LEVEL=0
	else
		echo "${EMAIL_ADDRESS}" | grep '^[a-zA-Z0-9._-]*@[a-zA-Z0-9._-]*\.[a-zA-Z0-9._-]*$' > /dev/null 2>&1
		if [ $? -ne 0 ]
			then	
    		echo -e "This address '${EMAIL_REPORT}' does not seem valid.\n\t-> We continue the process without sending email."
    		EMAIL_LEVEL=0
    	fi
    fi
elif [[ ${EMAIL_REPORT} != "nomail" ]]
	then
	echo -e "\nOption '-e ${EMAIL_REPORT}' is not valid (must be: 'onerror', 'forcemail' or 'nomail').\n\t-> We continue the process without sending email."
	EMAIL_LEVEL=0
elif [[ ${EMAIL_REPORT} = "nomail" ]]
	then
	EMAIL_LEVEL=0
fi

# Test ${SYNC_MEMBEROF} and ${ATTIBUTE_USED}
[[ ${SYNC_MEMBEROF} != "yes" ]] && [[ ${SYNC_MEMBEROF} != "no" ]] && error "Trying to use '-m' option but paramter '${SYNC_MEMBEROF}' is forbiden, must be 'yes' or 'no'."
[[ ${ATTIBUTE_USED} = "yes" ]] && [[ ${SYNC_MEMBEROF} = "no" ]] && error "Trying to use '-M' without '-m' option is forbiden."

echo -e "\n**********************************************************************"
echo -e "               Group OpenDirectory -> LDAP GroupOfNames"
echo -e "**********************************************************************\n"

# LDAP connection test...
echo -e "Test to bind LDAP ${LDAP_SERVER}:"
ldapsearch -LLL -x -b ${LDAP_GROUP_DN},${LDAP_DN_BASE} -H ${LDAP_SERVER} > /dev/null 2>&1
if [ $? -ne 0 ]
	then 
	error "Error while connecting with LDAP ${LDAP_SERVER} (${LDAP_GROUP_DN},${LDAP_DN_BASE}).\nPlease verify LDAP connection parameters, base DN and group relative DN."
else
	echo -e "-> OK"
fi

# Test if OD group exists and if OD group contains members
echo -e "\nCommand: '${DSCL_SEARCH_PATH} read /Groups/${DSCL_GROUP_NAME} GroupMembership'"
dscl ${DSCL_SEARCH_PATH} read /Groups/${DSCL_GROUP_NAME} GroupMembership > /dev/null 2>&1
if [ $? -ne 0 ] 
	then
		error "Command 'dscl ${DSCL_SEARCH_PATH} read /Groups/${DSCL_GROUP_NAME} GroupMembership' did not work properly.\nError $?."
	else
		dscl ${DSCL_SEARCH_PATH} read /Groups/${DSCL_GROUP_NAME} GroupMembership > ${MEMBER_OF_GROUP}
		[[ -z $(cat ${MEMBER_OF_GROUP}) ]] && error "Command 'dscl ${DSCL_SEARCH_PATH} read /Groups/${DSCL_GROUP_NAME} GroupMembership' does not find any user in the group."
fi

# Sync Group Description
[[ -e ${LDAP_GROUP_DESCRIPTION_FILE} ]] && rm ${LDAP_GROUP_DESCRIPTION_FILE}
touch ${LDAP_GROUP_DESCRIPTION_FILE}
if [[ $(dscl ${DSCL_SEARCH_PATH} read /Groups/${DSCL_GROUP_NAME} Description 2> /dev/null | wc -l) -eq 1 ]]; then
	for item in $(dscl ${DSCL_SEARCH_PATH} read /Groups/${DSCL_GROUP_NAME} Description 2> /dev/null | perl -p -e "s/dsAttrTypeNative:Description://g" | sed '/^$/d'| sed 's/^[ \t]*//')
	do
		echo "${item}" >> ${LDAP_GROUP_DESCRIPTION_FILE}
	done
elif [[ $(dscl ${DSCL_SEARCH_PATH} read /Groups/${DSCL_GROUP_NAME} Description 2> /dev/null | wc -l) -gt 1 ]]; then
	dscl ${DSCL_SEARCH_PATH} read /Groups/${DSCL_GROUP_NAME} Description 2> /dev/null | perl -p -e "s/dsAttrTypeNative:Description://g" | sed '/^$/d' | sed 's/^[ \t]*//' >> ${LDAP_GROUP_DESCRIPTION_FILE}
fi

LDAP_GROUP_DESCRIPTION=$(head -1 ${LDAP_GROUP_DESCRIPTION_FILE})

if [[ ! -z ${LDAP_GROUP_DESCRIPTION} ]]; then
	echo "dn: ${LDAP_GROUP_DN},${LDAP_DN_BASE}" > ${LDAP_GROUP_DESCRIPTION_FILE}
	echo "changetype: modify" >> ${LDAP_GROUP_DESCRIPTION_FILE}
	echo "replace: description" >> ${LDAP_GROUP_DESCRIPTION_FILE}
	echo "description: ${LDAP_GROUP_DESCRIPTION}" >> ${LDAP_GROUP_DESCRIPTION_FILE}
	echo -e "\n...Add description on ${LDAP_SERVER} on groupOfNames ${LDAP_GROUP_DN},${LDAP_DN_BASE}:"
	ldapmodify -D uid=${LDAP_ADMIN_UID},${LDAP_DN_USER_BRANCH},${LDAP_DN_BASE} -w ${LDAP_ADMIN_PASS} -H ${LDAP_SERVER} -x -f ${LDAP_GROUP_DESCRIPTION_FILE}
	[ $? -ne 0 ] && ERROR_CODE_MODIFY_4=1
	OLDIFS=$IFS
	IFS=$'\n'
	for MODIF_LINE in $(cat ${LDAP_GROUP_DESCRIPTION_FILE})
	do
		echo -e "\t${MODIF_LINE}"
	done
	IFS=$OLDIFS
fi

# Export actual members of OD group
echo -e "Actual members of OD group:"
dscl ${DSCL_SEARCH_PATH} read /Groups/${DSCL_GROUP_NAME} GroupMembership | sed 's/GroupMembership: //g' | tr ' ' '\n' | xargs -I @ echo @ | while read LINE_UID
do
	echo uid=${LINE_UID},${LDAP_DN_USER_BRANCH},${LDAP_DN_BASE} >> ${COMPLETE_USER_DN}
	echo -e "\t- ${LINE_UID} (uid=${LINE_UID},${LDAP_DN_USER_BRANCH},${LDAP_DN_BASE})"
done

# Export actual members of groupOfNames group
ldapsearch -LLL -x -b ${LDAP_GROUP_DN},${LDAP_DN_BASE} -H ${LDAP_SERVER} | grep member | awk '{print $2}' >> ${ACTUAL_USER_DN}

# Calculation of changes to be applied
grep -vf ${ACTUAL_USER_DN} ${COMPLETE_USER_DN} > ${LDAP_ADD_LIST}
grep -vf ${COMPLETE_USER_DN} ${ACTUAL_USER_DN} > ${LDAP_DELETE_LIST}

# Nothing to change
[[ -z $(cat ${LDAP_DELETE_LIST}) ]] &&  [[ -z $(cat ${LDAP_ADD_LIST}) ]] && echo -e "-> Nothing to sync at this step!"

## We add users first to avoid error with empty groupOfNames
# Add users to groupOfNames
if [[ ! -z $(cat ${LDAP_ADD_LIST}) ]]; then
	echo "dn: ${LDAP_GROUP_DN},${LDAP_DN_BASE}" > ${LDAP_MODIFY_ADD}
	echo "changetype: modify" >> ${LDAP_MODIFY_ADD}
	echo "add: member" >> ${LDAP_MODIFY_ADD}
	for MEMBER in $(cat ${LDAP_ADD_LIST})
	do 
		echo "${MEMBER}" | sed 's/uid=/member: uid=/' >> ${LDAP_MODIFY_ADD}
	done
	echo -e "\n...Add users on ${LDAP_SERVER} on groupOfNames ${LDAP_GROUP_DN},${LDAP_DN_BASE}:"
	ldapmodify -D uid=${LDAP_ADMIN_UID},${LDAP_DN_USER_BRANCH},${LDAP_DN_BASE} -w ${LDAP_ADMIN_PASS} -H ${LDAP_SERVER} -x -f ${LDAP_MODIFY_ADD}
	[ $? -ne 0 ] && ERROR_CODE_MODIFY_2=1
	OLDIFS=$IFS
	IFS=$'\n'
	for MODIF_LINE in $(cat ${LDAP_MODIFY_ADD})
	do
		echo -e "\t${MODIF_LINE}"
	done
	IFS=$OLDIFS
fi

# Delete users in groupOfNames
if [[ ! -z $(cat ${LDAP_DELETE_LIST}) ]]; then
	echo "dn: ${LDAP_GROUP_DN},${LDAP_DN_BASE}" > ${LDAP_MODIFY_DELETE}
	echo "changetype: modify" >> ${LDAP_MODIFY_DELETE}
	echo "delete: member" >> ${LDAP_MODIFY_DELETE}
	for MEMBER in $(cat ${LDAP_DELETE_LIST})
	do 
		echo "${MEMBER}" | sed 's/uid=/member: uid=/' >> ${LDAP_MODIFY_DELETE}
	done
	echo -e "\n...Delete users on ${LDAP_SERVER} on groupOfNames ${LDAP_GROUP_DN},${LDAP_DN_BASE}:"
	ldapmodify -D uid=${LDAP_ADMIN_UID},${LDAP_DN_USER_BRANCH},${LDAP_DN_BASE} -w ${LDAP_ADMIN_PASS} -H ${LDAP_SERVER} -x -f ${LDAP_MODIFY_DELETE}
	[ $? -ne 0 ] && ERROR_CODE_MODIFY_1=1
	OLDIFS=$IFS
	IFS=$'\n'
	for MODIF_LINE in $(cat ${LDAP_MODIFY_DELETE})
	do
		echo -e "\t${MODIF_LINE}"
	done
	IFS=$OLDIFS
fi

# Processing error code 
if [ ${ERROR_CODE_MODIFY_1} -ne 0 ] || [ ${ERROR_CODE_MODIFY_2} -ne 0 ] || [ ${ERROR_CODE_MODIFY_4} -ne 0 ]; then
	error "Error while applying LDAP groupOfNames modifications on ${LDAP_SERVER}.\nPlease verify LDAP connection parameters and result."
fi

# Modifications on user attribute
if [[ ${SYNC_MEMBEROF} = "yes" ]]
	then
	echo -e "\n**********************************************************************"
	echo -e "               Add or delete attribute ${SYNC_MEMBEROF_ATTRIBUTE}"
	echo -e "**********************************************************************\n"

	mkdir -p ${LDIF_ATTRIBUTE_MOD_FOLDER}

	# Export actual user with attribute ${SYNC_MEMBEROF_ATTRIBUTE}: ${LDAP_GROUP_DN},${LDAP_DN_BASE}
	ldapsearch -LLL -x -H ${LDAP_SERVER} -D uid=${LDAP_ADMIN_UID},${LDAP_DN_USER_BRANCH},${LDAP_DN_BASE} -w ${LDAP_ADMIN_PASS} -b ${LDAP_DN_USER_BRANCH},${LDAP_DN_BASE} ${SYNC_MEMBEROF_ATTRIBUTE}=${LDAP_GROUP_DN},${LDAP_DN_BASE} | grep uid= | sed 's/dn: //' >> ${ACTUAL_USER_WITH_ATTRIBUTE}
	
	# Calculation of changes to be applied on users
	grep -vf ${ACTUAL_USER_WITH_ATTRIBUTE} ${COMPLETE_USER_DN} > ${LDAP_ADD_MEMBEROF}
	grep -vf ${COMPLETE_USER_DN} ${ACTUAL_USER_WITH_ATTRIBUTE} > ${LDAP_DELETE_MEMBEROF}
	
	echo -e "Add or delete attribute '${SYNC_MEMBEROF_ATTRIBUTE}: ${LDAP_GROUP_DN},${LDAP_DN_BASE}' to users on ${LDAP_SERVER}"
	
	[[ -z $(cat ${LDAP_ADD_MEMBEROF}) ]] && [[ -z $(cat ${LDAP_DELETE_MEMBEROF}) ]] && echo -e "-> Nothing to sync at this step!" && alldone 0

	# Add attribute ${SYNC_MEMBEROF_ATTRIBUTE} to users
	if [[ ! -z $(cat ${LDAP_ADD_MEMBEROF}) ]]
		then
		for USER_DN in $(cat ${LDAP_ADD_MEMBEROF})
		do
			USER_UID=$(echo ${USER_DN} | awk -F'=' '{print $2}' | awk -F',' '{print $1}')
			# Opening LDIF file
			LDIF_FILE=${LDIF_ATTRIBUTE_MOD_FOLDER}/${USER_UID}.ldif
			echo "dn: ${USER_DN}" > ${LDIF_FILE}
			echo "changetype: modify" >> ${LDIF_FILE}
			echo "add: ${SYNC_MEMBEROF_ATTRIBUTE}" >> ${LDIF_FILE}
			echo "${SYNC_MEMBEROF_ATTRIBUTE}: ${LDAP_GROUP_DN},${LDAP_DN_BASE}" >> ${LDIF_FILE}
			echo -e "...Add: '${SYNC_MEMBEROF_ATTRIBUTE}: ${LDAP_GROUP_DN},${LDAP_DN_BASE}' to '${USER_DN}'"
		done
	fi

	# Delete attribute ${SYNC_MEMBEROF_ATTRIBUTE} to users
	if [[ ! -z $(cat ${LDAP_DELETE_MEMBEROF}) ]]
		then
		for USER_DN in $(cat ${LDAP_DELETE_MEMBEROF})
		do
			USER_UID=$(echo ${USER_DN} | awk -F'=' '{print $2}' | awk -F',' '{print $1}')
			# Opening LDIF file
			LDIF_FILE=${LDIF_ATTRIBUTE_MOD_FOLDER}/${USER_UID}.ldif
			echo "dn: ${USER_DN}" > ${LDIF_FILE}
			echo "changetype: modify" >> ${LDIF_FILE}
			echo "delete: ${SYNC_MEMBEROF_ATTRIBUTE}" >> ${LDIF_FILE}
			echo "${SYNC_MEMBEROF_ATTRIBUTE}: ${LDAP_GROUP_DN},${LDAP_DN_BASE}" >> ${LDIF_FILE}
			echo -e "...Delete: '${SYNC_MEMBEROF_ATTRIBUTE}: ${LDAP_GROUP_DN},${LDAP_DN_BASE}' to '${USER_DN}'"
		done
	fi

	# Processing 
	find ${LDIF_ATTRIBUTE_MOD_FOLDER} -type f -name "*.ldif" >> ${LDIF_ATTRIBUTE_MOD_LIST}
	if [[ ! -z $(cat ${LDIF_ATTRIBUTE_MOD_LIST}) ]]
		then
		for LDIF_MODIF_FILE in $(cat ${LDIF_ATTRIBUTE_MOD_LIST})
		do
			ldapmodify -D uid=${LDAP_ADMIN_UID},${LDAP_DN_USER_BRANCH},${LDAP_DN_BASE} -w ${LDAP_ADMIN_PASS} -H ${LDAP_SERVER} -x -f ${LDIF_MODIF_FILE}
			if [ $? -ne 0 ] 
				then
				ERROR_CODE_MODIFY_3=1
				echo $(basename ${LDIF_MODIF_FILE}) | sed 's/.ldif//' >> ${LDIF_ERROR_APPLY}
			fi
		done
	fi

	# Processing error code
	if [ ${ERROR_CODE_MODIFY_3} -ne 0 ]
		then
		error "Error while applying LDAP modifications on ${LDAP_SERVER} for users:\n$(cat ${LDIF_ERROR_APPLY} | perl -p -e 's/\n/ - /g' | awk 'sub( "...$", "" )')\nPlease verify LDAP connection parameters and result for these users."
	fi

	echo -e "-> OK"
fi

alldone 0

Vous pouvez l’installer sur votre serveur, une fois positionné dans le dossier de votre choix, au moyen d’un simple :

wget --no-check-certificate https://raw.github.com/yvangodard/ldapSync/master/ldapSync.sh ; 
sudo chmod +x ldapSync.sh

Comme l’expliquais Yoann Gini sur son blog, ce script ce va se servir du service d’annuaire local au Mac pour retrouver la liste des membres d’un groupe (ici equipe) puis il va générer les noms uniques LDAP de chaque utilisateur en considérant que tout utilisateur de l’OD à un DN de type uid=username,cn=users,dc=… . Une fois la liste d’utilisateur récupérée, le script s’occupe de la comparer avec ce qui existe déjà dans la liste de membres du groupe LDAP (ici dans notre exemple cn=workgroup,ou=lists…) et va générer un fichier ldif qui contiendra les entrées à ajouter et un fichier ldif qui contiendra les entrées à supprimer du groupe final.

La syntaxe pour l’utilisation simple de ce script est la suivante :

./ldapSync.sh [-h] | -d <LDAP base namespace> -p <LDAP Admin password> -g <relative DN of LDAP GroupOfNames> -G <OD Groupname>

… soit dans notre exemple :

./ldapSync.sh -d dc=serveur,dc=myoffice,dc=com -p password -g cn=equipe,ou=lists -G workgroup

Vous pouvez appeler les options suivantes selon votre cas de figure :

  • -a <UID de l'administrateur LDAP> : UID de l’utilisateur ayant les droits read/write sur le groupe (par défaut, si vous n’appelez pas cette option : ‘diradmin‘)
  • -s <URL du serveur LDAP&gt; : URL du serveur LDAP (par défaut, si vous n’appelez pas cette option : ‘ldap://127.0.0.1‘)
  • -u <DN relatif de la branche Users> : DN relatif de la branche contenant vos utilisateurs (par exemple : ‘cn=allusers‘, par défaut, si vous n’appelez pas cette option : ‘cn=users‘)
  • -S <répertoire de recherche DSCL> : répertoire de recherche DSCL pour le groupe OpenDirectory (par défaut, si vous n’appelez pas cette option : ‘/Search‘)
  • -e <option de rapport par email> : paramètre d’envoi de rapport d’exécution par email, soit ‘onerror‘, ‘forcemail‘ ou ‘nomail‘ (par défaut : ‘nomail‘)
  • -E <adresse email pour les rapports> : adresse email à laquelle sera envoyé le rapport. Cette option doit être utilisée si les options ‘-e forcemail‘ ou ‘-e onerror‘ sont utilisées
  • -j <fichier journal> : cette option active la journalisation à la place de la sortie standard. Spécifiez en argument le chemin du fichier de log (par exemple : ‘/var/log/ldapSync.log‘) ou utilisez ‘default‘ (/var/log/ldapSync.log)

Si vous souhaitez consulter l’aide associée au script :

./ldapSync.sh help

Je vous renvoie à l’article de Yoann Gini qui propose une méthode pour sécuriser un peu l’utilisation de ce script en donnant des droits particuliers à un utilisateur sur le groupe, pour éviter de devoir utiliser le compte administrateur de l’OpenDirectory dans vos entrées launchd.

Etape 4 : modifiez le schéma de votre LDAP en ajoutant un attribut alternatif au memberOf

Si, comme moi, vous souhaitez ajouter un attribut de type memberOf sur vos entrées ‘User‘ de votre LDAP pour marquer directement leur appartenance à un groupOfNames, il vous faudra préalablement modifier légèrement la structure de votre LDAP. En effet, l’attribut memberOf existe bien dans le schéma de l’OpenDirectory, mais il s’agit d’un attribut opérationnel dans OpenLDAP, ce qui signifie que nous ne pouvons pas le rendre accessible à une classe d’utilisateurs.

Ma méthode est donc ici un peu workaround, puisque j’ajoute au schéma de notre LDAP un nouveau type d’attribut (ici ‘resMemberOf‘) et une classe d’objet (ici ‘resPerson‘) qui autorise ce nouvel attribut. Cela implique donc d’étendre le schéma du LDAP fourni par l’OpenDirectory.

Pour cela, créer un fichier au format .ldif contenant les modifications à apporter (ici ce fichier sera  : /var/root/ldap_modif.ldif) :

dn: cn=reslr,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: reslr
olcAttributeTypes: ( 1.3.6.1.4.1.43016.2.3.1
 NAME 'resMemberOf' 
 DESC 'Distinguished name of a group of which the object is a member' 
 EQUALITY distinguishedNameMatch 
 SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
olcObjectClasses: ( 1.3.6.1.4.1.43016.2.4.1 
 NAME 'resPerson' DESC 'reseauenscene.fr person' 
 AUXILIARY MAY ( resMemberOf ) )

Pensez à personnaliser :

  • le CN de cette configuration (ici cn=reslr, en ligne 1 et 3)
  • le nom un nouveau type d’attribut (ici resMemberOf) et une classe d’objet (ici resPerson)
  • les OID (1.3.6.1.4.1.43016.2.3.1 et 1.3.6.1.4.1.43016.2.4.1), car ceux que j’utilise ici sont composés avec mon propre préfixe OID assigné par l’IANA. Vous devez remplacer 1.3.6.1.4.1.43016 avec votre propre préfixe OID. Si vous ne disposez pas de votre propre numéro, vous pouvez en obtenir un (cela prend à peine quelques jours) en vous enregistrant ici.

Pour appliquer l’ajout dans votre schéma, dans votre Terminal :

ldapadd -H ldap://urldevotreldap -D uid=diradmin,cn=users,dc=.... -w passwordldapadmin -x -f /var/root/ldap_modif.ldif

Vous pouvez ensuite utiliser le Gestionnaire de Groupe de Travail (WorkgroupManager) d’OS X Server, en y activant préalablement les options avancées pour pouvoir accéder à toutes les entrées, pour vérifier que la modification a été correctement apportée. Pour cela, rendez vous dans OLCSchemaConfig. Vous devriez y voir les modifications apportées comme ici :

OLCSchemaConfig

Etape 5 : utilisez le script ldapSync pour augmenter les entrées Users

Maintenant que nous disposons d’un attribut alternatif à memberOf (ici resMemberOf), il nous faut trouver une méthode pour que les entrées Users du LDAP fourni par notre OpenDirectory puissent être automatiquement augmentée de cet attribut.

Sur OpenLdap (en environnement GNU/Linux) on utilise traditionnellement le module overlay pour réaliser cela dynamiquement : à chaque fois qu’un utilisateur est ajouté à un groupe, le module overlay se charge (s’il est correctement configuré) de marquer l’appartenance à ce groupe dans l’entrée User avec un attribut de type memberOf.

Mais, malheureusement, je n’ai pas trouvé de méthode pour activer ce module sur Mac OS X Serveur. D’ailleurs si quelqu’un a une piste pour cela, les commentaires sont là pour ça !

Du coup, j’ai ajouté des fonctions dans le script ldapSync :

  • -m <sync memberOf> : cette option, ajoute, en plus de la synchronisation vers un groupOfNames l’ajout d’attributs de type memberOf pour marquer l’appartenance à ce groupe dans l’entrée User. Pour activer cette fonction utilisez ‘-m yes‘.
  • -M <memberOf attribute> : attribut à utiliser, par exemple ‘-M resMemberOf‘ (par défaut, si vous n’appelez pas cette option, l’attribut utilisé sera ‘memberOf‘).

Si vous souhaitez ainsi utiliser cette fonction supplémentaire, vous pouvez appelez :

./ldapSync.sh -d dc=serveur,dc=myoffice,dc=com -p password -g cn=equipe,ou=lists -G workgroup -m yes -M resMemberOf

Il ne vous reste plus qu’à programmer cette tâche avec une entrée launchd de manière périodique.

Posted in Apple et Macintosh, Unix et LinuxTags: