You've successfully subscribed to Florin Loghiade
Great! Next, complete checkout for full access to Florin Loghiade
Welcome back! You've successfully signed in
Success! Your account is fully activated, you now have access to all content.

Migrating OpenLDAP to Active Directory

in

I’ve been working on a project that required a migration from an OpenLDAP Directory Server to Active Directory which was a very big challenge because there was no Samba server in order to do any work with ADMT or any other procedure. The options were to find a way to synchronize OpenLDAP with Active Directory and cut it off once everything was in order or to export the entire directory in a CSV file and then prepare it to be consumed by Active Directory.

We were considering both options at the time but the first one that tickled our fancy was to find the miracle tool that would help us the pain of constructing the CSV file. Lo and behold that we found the worthy open source candidate that could do exactly what we wanted. The tool is called “LDAP Synchronization Connector” and you can find out more about it here: LSC Project

If you stopped for a moment to check their website, you would have noticed that there is a guide for populating Active Directory from an OpenLDAP Directory server but the problem is that things changed and that tutorial can be treated as a guide line.

Parts of the tutorial helped me construct the XML file that’s required for the connector to actually do something but following it by the heart won’t get you anywhere and you will pull your hair out (like me) trying to get it working.

First of all; You will need to configure the Active Directory Domain Controller to use secure LDAP (LDAPS) otherwise this will never work. In this case you have two options, either you have PKI or you buy a third party SSL certificate. By using Active Directory Certificate Services, a Server Authentication certificate gets installed on the domain controllers which they use for enabling LDAPS.

Once you got that done, you will need to export the certificate that’s used for LDAPS without the private key. You need to do this task because you will have to import it on the machine where the LSC connector is installed.

The next step is to install the LSC tool, preferably on your OpenLDAP machine because it’s less hassle and you skip a few pain points.

#APT Install

nano /etc/apt/sources.list.d/lsc-project.list

#Paste the following lines:
deb     http://lsc-project.org/debian lsc main
deb-src http://lsc-project.org/debian lsc main
#

wget -O - http://ltb-project.org/wiki/lib/RPM-GPG-KEY-LTB-project | sudo apt-key add -
apt-get update
apt-get install lsc

#YUM Install

nano /etc/yum.repos.d/lsc-project.repo

#Paste the following lines:
[lsc-project]
name=LSC project packages
baseurl=http://lsc-project.org/rpm/noarch
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-LTB-project
#

rpm --import http://ltb-project.org/wiki/lib/RPM-GPG-KEY-LTB-project
yum update
yum install lsc

The guys at LSC-Project have YUM and APT repositories so we don’t need to manually install anything. Basically we run the lines posted above and we’re good to go.

The next step in getting everything set up is to import the certificate that you previously exported in the JVM Keystore. For this part you can use your favorite way of putting files on a Linux machine. Once that’s done, import the certificate in the JVM Keystore.

 cd $JAVA_HOME/jre/lib/security/
../../bin/keytool -import -file /path/to/ldapssl.cer -keystore jssecacerts

The next step is to the necessary directories on the server the connector is installed.

For this I used the example in the LSC tutorial.

mkdir /etc/lsc/openldap2ad/
cp /etc/lsc/logback.xml /etc/lsc/openldap2ad
touch /etc/lsc/openldap2ad/lsc.xml

When you get to this part, you’re done with the Linux and Windows servers. This is the part where you open your favorite xml editor and start working on the actual file that will tell the connector what to do. I used Visual Studio Code for this task.

Let’s start with the config.

<?xml version="1.0" ?>
<!-- 
	In the following file, comments are describing each node. Elements are
	referenced through XPath expression, whereas attributes are prefixed with
	'@'

	//lsc Root node of the XML configuration file
	@xmlns XML Schema validation is not ready yet (Reserved for futur use)
	@id optional, added by XML API
	@revision mandatory, used by the Web Administration Interface to version
				this file
	 -->
<lsc xmlns="http://lsc-project.org/XSD/lsc-core-2.1.xsd" revision="0">

<!--  ./connections Connections list node, must contain at least two connections -->
 
  <connections>
<!--  OpenLDAP connection node -->
  <ldapConnection>
      <name>openldap</name>
      <url>ldap://localhost:389/dc=domain,dc=com</url>
      <username>cn=adminuser,dc=domain,dc=com</username>
      <password>PASSWORD</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>
<!--  Active Directory connection node -->
    <ldapConnection>
      <name>AD</name>
      <url>ldaps://DCIP:636/dc=domain,dc=com</url>
      <username>CN=DomainAdmin,CN=Users,DC=domain,DC=com</username>
      <password>PASSWORD</password>
      <authentication>SIMPLE</authentication>
      <referral>IGNORE</referral>
      <derefAliases>NEVER</derefAliases>
      <version>VERSION_3</version>
      <pageSize>1000</pageSize>
      <factory>com.sun.jndi.ldap.LdapCtxFactory</factory>
      <tlsActivated>false</tlsActivated>
    </ldapConnection>

  </connections>

The first part of the configuration is pretty straight forward. You define the connection node for the OpenLDAP Server and Active Directory server. For this project I chose to create a domain admin that I delete after one complete sync so from my point of view that wasn’t a problem with having plain text credentials. The thing that you may notice is that I used ldap:// for the OpenLDAP server and ldaps:// for the domain controller. This is because I installed the connector directly on the OpenLDAP server.

Next up we have the tasks that the connector will actually perform.

<tasks>

    <task>
      <name>MigrateUsers</name>
      <bean>org.lsc.beans.SimpleBean</bean>
      <ldapSourceService>
        <name>openldap-source-service</name>
        <connection reference="openldap" />
        <baseDn>ou=People,dc=domain,dc=com</baseDn>
        <pivotAttributes>
          <string>uid</string>
        </pivotAttributes>
        <fetchedAttributes>
          <string>cn</string>
          <string>description</string>
          <string>givenName</string>
          <string>mail</string>
          <string>sn</string>
          <string>uid</string>

        </fetchedAttributes>
        <getAllFilter><![CDATA[(objectClass=inetOrgPerson)]]></getAllFilter>
        <getOneFilter><![CDATA[(&(objectClass=inetOrgPerson)(uid={uid}))]]></getOneFilter>
        <cleanFilter><![CDATA[(&(objectClass=inetOrgPerson)(uid={sAMAccountName}))]]></cleanFilter>
      </ldapSourceService>

      <ldapDestinationService>
        <name>ad-dst-service</name>
        <connection reference="AD" />
        <baseDn>OU=ORGNAME,dc=domain,dc=com</baseDn>
        <pivotAttributes>
          <string>sAMAccountName</string>
        </pivotAttributes>
        <fetchedAttributes>
          <string>cn</string>
          <string>description</string>
          <string>givenName</string>
          <string>mail</string>
          <string>objectclass</string>
          <string>pwdLastSet</string>
          <string>sAMAccountName</string>
          <string>sn</string>
          <string>unicodePwd</string>
          <string>userAccountControl</string>
          <string>userPrincipalName</string>
        </fetchedAttributes>
        <getAllFilter><![CDATA[(objectClass=user)]]></getAllFilter>
        <getOneFilter><![CDATA[(&(objectClass=user)(sAMAccountName={uid}))]]></getOneFilter>
      </ldapDestinationService>

      <propertiesBasedSyncOptions>
     <mainIdentifier>"cn=" + srcBean.getDatasetFirstValueById("cn") + ",OU=ORGNAME,dc=domain,dc=com"</mainIdentifier>
    <defaultDelimiter>;</defaultDelimiter>
    <defaultPolicy>FORCE</defaultPolicy>

    <conditions>
      <create>true</create>
      <update>true</update>
      <delete>false</delete>
      <changeId>true</changeId>
    </conditions>

    <dataset>
      <name>objectclass</name>
      <policy>KEEP</policy>
      <createValues>
        <string>"user"</string>
        <string>"organizationalPerson"</string>
        <string>"person"</string>
        <string>"top"</string>
      </createValues>
    </dataset>

    <dataset>
     <name>sAMAccountName</name>
     <policy>KEEP</policy>
     <createValues>
      <string>srcBean.getDatasetFirstValueById("uid")</string>
     </createValues>
    </dataset>

    <dataset>
     <!-- userPrincipalName = uid + "@domain.com" -->
     <name>userPrincipalName</name>
     <policy>FORCE</policy>
     <forceValues>
      <string>srcBean.getDatasetFirstValueById("uid") + "@domain.com"</string>
     </forceValues>
    </dataset>

    <dataset>
     <name>userAccountControl</name>
     <policy>KEEP</policy>
     <createValues>
      <string>AD.userAccountControlSet( "0", [AD.UAC_SET_NORMAL_ACCOUNT])</string>
     </createValues>
    </dataset>

    <dataset>
     <!-- pwdLastSet = 0 to force user to change password on next connection --> 
     <name>pwdLastSet</name>
     <policy>KEEP</policy>
     <createValues>
      <string>"0"</string>
     </createValues>
    </dataset>

    <dataset>
      <!-- unicodePwd = Type a strong password because AD requires a complex password to be filled. If you don't provide a complex password you will get very ambiguous errors   -->
     <name>unicodePwd</name>
     <policy>KEEP</policy>
     <createValues>
      <string>AD.getUnicodePwd("q76X2pccn4OS83p")</string>
     </createValues>
    </dataset>

   </propertiesBasedSyncOptions>
  </task>

This is the most complex and hard part of the XML. In the tasks phase, you need to define everything that you need to copy over to the domain controller. The snippet from above is just a basic example. The XML file that I created for the client had 40 fields that were required to be synchronized.

So let’s get it simple. We have a source service and a destination service.

In our case the source service is the OpenLDAP Server and the destination service is the AD Domain Controller:

 <ldapSourceService>
        <name>openldap-source-service</name>
        <connection reference="openldap" />
        <baseDn>ou=People,dc=domain,dc=com</baseDn>
        <pivotAttributes>
          <string>uid</string>
        </pivotAttributes>
        <fetchedAttributes>
          <string>cn</string>
          <string>description</string>
          <string>givenName</string>
          <string>mail</string>
          <string>sn</string>
          <string>uid</string>
 
        </fetchedAttributes>
        <getAllFilter><![CDATA[(objectClass=inetOrgPerson)]]></getAllFilter>
        <getOneFilter><![CDATA[(&(objectClass=inetOrgPerson)(uid={uid}))]]></getOneFilter>
        <cleanFilter><![CDATA[(&(objectClass=inetOrgPerson)(uid={sAMAccountName}))]]></cleanFilter>
      </ldapSourceService>
      <ldapDestinationService>
        <name>ad-dst-service</name>
        <connection reference="AD" />
        <baseDn>OU=ORGNAME,dc=domain,dc=com</baseDn>
        <pivotAttributes>
          <string>sAMAccountName</string>
        </pivotAttributes>
        <fetchedAttributes>
          <string>cn</string>
          <string>description</string>
          <string>givenName</string>
          <string>mail</string>
          <string>objectclass</string>
          <string>pwdLastSet</string>
          <string>sAMAccountName</string>
          <string>sn</string>
          <string>unicodePwd</string>
          <string>userAccountControl</string>
          <string>userPrincipalName</string>
        </fetchedAttributes>
        <getAllFilter><![CDATA[(objectClass=user)]]></getAllFilter>
        <getOneFilter><![CDATA[(&(objectClass=user)(sAMAccountName={uid}))]]></getOneFilter>
      </ldapDestinationService>

As you can see we’re defining a pivot attribute (unique attribute) and we’re defining attributes that need to be fetched. Those attributes can be as many as you want but my advice to you is to start small and get it right.

On both the nodes we’re defining a pivot and fetching some attributes because if we’re doing multiple syncs or something changes, the connector won’t need to recreate the user in the destination service, it will just update it.

The next part of the config is the get filters which basically runs a query in the source and destination directories and loos for something specific. In this case we’re querying for users.

 <propertiesBasedSyncOptions>
     <mainIdentifier>"cn=" + srcBean.getDatasetFirstValueById("cn") + ",OU=ORGNAME,dc=domain,dc=com"</mainIdentifier>
    <defaultDelimiter>;</defaultDelimiter>
    <defaultPolicy>FORCE</defaultPolicy>

The snippet above tells the connector how to build the destination object. In the example above we’re building the object by using the CN= attribute and we’re appending an Organization Unit and the domain. You can simply remove the OU= part and put something else like CN=Users

 <conditions>
      <create>true</create>
      <update>true</update>
      <delete>false</delete>
      <changeId>true</changeId>
    </conditions>

The conditions node tells the connector what to do:

Create = Creates the object
Update = Updates the object
Delete = Deletes the object if non-existent in source service
ChangeID = Modifies the object identifier

    <dataset>
      <name>objectclass</name>
      <policy>KEEP</policy>
      <createValues>
        <string>"user"</string>
        <string>"organizationalPerson"</string>
        <string>"person"</string>
        <string>"top"</string>
      </createValues>
    </dataset>
 
    <dataset>
     <name>sAMAccountName</name>
     <policy>KEEP</policy>
     <createValues>
      <string>srcBean.getDatasetFirstValueById("uid")</string>
     </createValues>
    </dataset>
 
    <dataset>
     <!-- userPrincipalName = uid + "@domain.com" -->
     <name>userPrincipalName</name>
     <policy>FORCE</policy>
     <forceValues>
      <string>srcBean.getDatasetFirstValueById("uid") + "@domain.com"</string>
     </forceValues>
    </dataset>
 
    <dataset>
     <name>userAccountControl</name>
     <policy>KEEP</policy>
     <createValues>
      <string>AD.userAccountControlSet( "0", [AD.UAC_SET_NORMAL_ACCOUNT])</string>
     </createValues>
    </dataset>
 
    <dataset>
     <!-- pwdLastSet = 0 to force user to change password on next connection --> 
     <name>pwdLastSet</name>
     <policy>KEEP</policy>
     <createValues>
      <string>"0"</string>
     </createValues>
    </dataset>
 
    <dataset>
      <!-- unicodePwd = Type a strong password because AD requires a complex password to be filled. If you don't provide a complex password you will get very ambiguous errors   -->
     <name>unicodePwd</name>
     <policy>KEEP</policy>
     <createValues>
      <string>AD.getUnicodePwd("q76X2pccn4OS83p")</string>
     </createValues>
    </dataset>

Datasets are synchronization rules where you can define how to do the synchronization. You can have mirror rules or transformation rules.

Mirror rules simply take the value in the source service and does a 1:1 copy in the destination service while the transformation rule takes the values from the source and modifies it to look different.

Transform rules look like this:

    <dataset>
     <name>sAMAccountName</name>
     <policy>KEEP</policy>
     <createValues>
      <string>srcBean.getDatasetFirstValueById("uid")</string>
     </createValues>
    </dataset>
 
    <dataset>
     <!-- userPrincipalName = uid + "@domain.com" -->
     <name>userPrincipalName</name>
     <policy>FORCE</policy>
     <forceValues>
      <string>srcBean.getDatasetFirstValueById("uid") + "@domain.com"</string>
     </forceValues>
    </dataset>

As you can see we have two types of transform rules. One has a KEEP Policy with a node and the other one has a FORCE Policy with a node.

The main difference being that one is used to create the value and the other one to force the value.

One very important thing to mention is that you cannot synchronize passwords from OpenLDAP to AD or vice-versa. Unless you have the passwords in plain text (which is a very bad idea), passwords won’t be synchronized. Two of the datasets from above perform two operations on the newly created object. The first one is to force the user to change his password when he logs in the first time and second one is to set a password.

These are the datasets that do the operations:

    <dataset>
     <!-- pwdLastSet = 0 to force user to change password on next connection --> 
     <name>pwdLastSet</name>
     <policy>KEEP</policy>
     <createValues>
      <string>"0"</string>
     </createValues>
    </dataset>
 
    <dataset>
      <!-- unicodePwd = Type a strong password because AD requires a complex password to be filled. If you don't provide a complex password you will get very ambiguous errors   -->
     <name>unicodePwd</name>
     <policy>KEEP</policy>
     <createValues>
      <string>AD.getUnicodePwd("q76X2pccn4OS83p")</string>
     </createValues>
    </dataset>

This pretty much covers almost everything when it comes to migrating users. You can however go even deeper in the rabbit hole and start writing .js code that can be referenced inside the configuration file.

Our next tasks job will be to create the groups that are in the OpenLDAP server on the Active Directory server and of course populate those groups.

To do this, we create a new task that will handle group migration.

    <task>
      <name>group</name>
      <bean>org.lsc.beans.SimpleBean</bean>
       <asyncLdapSourceService>
        <name>group-source-service</name>
        <connection reference="openldap" />
        <baseDn>ou=group,dc=domain,dc=com</baseDn>
        <pivotAttributes>
          <string>cn</string>
        </pivotAttributes>
        <fetchedAttributes>
          <string>cn</string>
          <string>description</string>
          <string>uniqueMember</string>
        </fetchedAttributes>
        <getAllFilter><![CDATA[(objectClass=groupOfUniqueNames)]]></getAllFilter>
        <getOneFilter><![CDATA[(&(objectClass=groupOfUniqueNames)(cn={cn}))]]></getOneFilter>
        <cleanFilter><![CDATA[(&(objectClass=groupOfUniqueNames)(cn={cn}))]]></cleanFilter>
        <serverType>OpenLDAP</serverType>
      </asyncLdapSourceService>
      <ldapDestinationService>
        <name>group-dst-service</name>
        <connection reference="AD" />
        <baseDn>OU=Group,OU=ORGNAME,DC=domain,DC=com</baseDn>
        <pivotAttributes>
          <string>cn</string>
        </pivotAttributes>
        <fetchedAttributes>
          <string>cn</string>
          <string>description</string>
          <string>member</string>
          <string>objectClass</string>
        </fetchedAttributes>
        <getAllFilter><![CDATA[(objectClass=group)]]></getAllFilter>
        <getOneFilter><![CDATA[(&(objectClass=group)(cn={cn}))]]></getOneFilter>
      </ldapDestinationService>
   <propertiesBasedSyncOptions>
           <mainIdentifier>js:"cn=" + javax.naming.ldap.Rdn.escapeValue(srcBean.getDatasetFirstValueById("cn")) + ",OU=Group,OU=ORGNAME,DC=domain,DC=com"</mainIdentifier>
        <defaultDelimiter>;</defaultDelimiter>
        <defaultPolicy>FORCE</defaultPolicy>
        <conditions>
          <create>true</create>
          <update>true</update>
          <delete>true</delete>
          <changeId>true</changeId>
        </conditions>
                <dataset>
          <name>objectclass</name>
          <policy>KEEP</policy>
          <createValues>
            <string>"group"</string>
            <string>"top"</string>
          </createValues>
        </dataset>
                <dataset>
         <name>member</name>
         <policy>KEEP</policy>
         <changeValues>
           <string>
           <![CDATA[rjs:
                var membersSrcDn = srcBean.getDatasetValuesById("uniqueMember");
                var membersDstDn = [];
                for  (var i=0; i<membersSrcDn.size(); i++) {
                        var memberSrcDn = membersSrcDn.get(i);
                        var uid = "";
                        try {
                                uid = srcLdap.attribute(memberSrcDn, "uid").get(0);
                        } catch(e) {
                                continue;
                        }
                        var destDn = ldap.search("ou=ORGNAME", "(sAMAccountName=" + uid + ")");
                        if (destDn.size() == 0 || destDn.size() > 1) {
                                continue;
                        }
                        var destMemberDn = destDn.get(0) + "," +  ldap.getContextDn();
                        membersDstDn.push(destMemberDn);
                }
                membersDstDn
           ]]>
           </string>
         </channgeValues>
      </dataset>
      </propertiesBasedSyncOptions>
      </task>
 </tasks>
</lsc>

Once you understand how to build the user migration task then the group migration task will not be so hard to do. The task above basically takes the groups it find in the base DN and creates them in the destination directory. For actually populating the groups with members, we need to have some JavaScript code that does a LDAP search and populates the groups with the results from the query.

The only problem here is that the LSC connector doesn’t support OU migrations so if you don’t have a flat directory then the solution would be to write some JavaScript code or do it like me and create a task for each OU. I’m not that savvy in JavaScript so doing multiple copy paste operations with subtle changes was worth it.

Now that we understand how the tool works. All we need to do is piece together everything, paste it in the lsc.xml file that was created on the Linux machine and execute the sync job and see what happens ?

Here’s the command for the sync job:

#Dry Run: Test if the code works

 /usr/bin/lsc -f /etc/lsc/openldap2ad -s all -c all -n

#Full sync run
 /usr/bin/lsc -f /etc/lsc/openldap2ad -s all -c all
<?xml version="1.0" ?>
<!-- 
	In the following file, comments are describing each node. Elements are
	referenced through XPath expression, whereas attributes are prefixed with
	'@'

	//lsc Root node of the XML configuration file
	@xmlns XML Schema validation is not ready yet (Reserved for futur use)
	@id optional, added by XML API
	@revision mandatory, used by the Web Administration Interface to version
				this file
	 -->
<lsc xmlns="http://lsc-project.org/XSD/lsc-core-2.1.xsd" revision="0">

<!--  ./connections Connections list node, must contain at least two connections -->
 
  <connections>
<!--  OpenLDAP connection node -->
  <ldapConnection>
      <name>openldap</name>
      <url>ldap://localhost:389/dc=domain,dc=com</url>
      <username>cn=adminuser,dc=domain,dc=com</username>
      <password>PASSWORD</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>
<!--  Active Directory connection node -->
    <ldapConnection>
      <name>AD</name>
      <url>ldaps://DCIP:636/dc=domain,dc=com</url>
      <username>CN=DomainAdmin,CN=Users,DC=domain,DC=com</username>
      <password>PASSWORD</password>
      <authentication>SIMPLE</authentication>
      <referral>IGNORE</referral>
      <derefAliases>NEVER</derefAliases>
      <version>VERSION_3</version>
      <pageSize>1000</pageSize>
      <factory>com.sun.jndi.ldap.LdapCtxFactory</factory>
      <tlsActivated>false</tlsActivated>
    </ldapConnection>

  </connections>
<tasks>

    <task>
      <name>MigrateUsers</name>
      <bean>org.lsc.beans.SimpleBean</bean>
      <ldapSourceService>
        <name>openldap-source-service</name>
        <connection reference="openldap" />
        <baseDn>ou=People,dc=domain,dc=com</baseDn>
        <pivotAttributes>
          <string>uid</string>
        </pivotAttributes>
        <fetchedAttributes>
          <string>cn</string>
          <string>description</string>
          <string>givenName</string>
          <string>mail</string>
          <string>sn</string>
          <string>uid</string>

        </fetchedAttributes>
        <getAllFilter><![CDATA[(objectClass=inetOrgPerson)]]></getAllFilter>
        <getOneFilter><![CDATA[(&(objectClass=inetOrgPerson)(uid={uid}))]]></getOneFilter>
        <cleanFilter><![CDATA[(&(objectClass=inetOrgPerson)(uid={sAMAccountName}))]]></cleanFilter>
      </ldapSourceService>

      <ldapDestinationService>
        <name>ad-dst-service</name>
        <connection reference="AD" />
        <baseDn>OU=ORGNAME,dc=domain,dc=com</baseDn>
        <pivotAttributes>
          <string>sAMAccountName</string>
        </pivotAttributes>
        <fetchedAttributes>
          <string>cn</string>
          <string>description</string>
          <string>givenName</string>
          <string>mail</string>
          <string>objectclass</string>
          <string>pwdLastSet</string>
          <string>sAMAccountName</string>
          <string>sn</string>
          <string>unicodePwd</string>
          <string>userAccountControl</string>
          <string>userPrincipalName</string>
        </fetchedAttributes>
        <getAllFilter><![CDATA[(objectClass=user)]]></getAllFilter>
        <getOneFilter><![CDATA[(&(objectClass=user)(sAMAccountName={uid}))]]></getOneFilter>
      </ldapDestinationService>

      <propertiesBasedSyncOptions>
     <mainIdentifier>"cn=" + srcBean.getDatasetFirstValueById("cn") + ",OU=ORGNAME,dc=domain,dc=com"</mainIdentifier>
    <defaultDelimiter>;</defaultDelimiter>
    <defaultPolicy>FORCE</defaultPolicy>

    <conditions>
      <create>true</create>
      <update>true</update>
      <delete>false</delete>
      <changeId>true</changeId>
    </conditions>

    <dataset>
      <name>objectclass</name>
      <policy>KEEP</policy>
      <createValues>
        <string>"user"</string>
        <string>"organizationalPerson"</string>
        <string>"person"</string>
        <string>"top"</string>
      </createValues>
    </dataset>

    <dataset>
     <name>sAMAccountName</name>
     <policy>KEEP</policy>
     <createValues>
      <string>srcBean.getDatasetFirstValueById("uid")</string>
     </createValues>
    </dataset>

    <dataset>
     <!-- userPrincipalName = uid + "@domain.com" -->
     <name>userPrincipalName</name>
     <policy>FORCE</policy>
     <forceValues>
      <string>srcBean.getDatasetFirstValueById("uid") + "@domain.com"</string>
     </forceValues>
    </dataset>

    <dataset>
     <name>userAccountControl</name>
     <policy>KEEP</policy>
     <createValues>
      <string>AD.userAccountControlSet( "0", [AD.UAC_SET_NORMAL_ACCOUNT])</string>
     </createValues>
    </dataset>

    <dataset>
     <!-- pwdLastSet = 0 to force user to change password on next connection --> 
     <name>pwdLastSet</name>
     <policy>KEEP</policy>
     <createValues>
      <string>"0"</string>
     </createValues>
    </dataset>

    <dataset>
      <!-- unicodePwd = Type a strong password because AD requires a complex password to be filled. If you don't provide a complex password you will get very ambiguous errors   -->
     <name>unicodePwd</name>
     <policy>KEEP</policy>
     <createValues>
      <string>AD.getUnicodePwd("q76X2pccn4OS83p")</string>
     </createValues>
    </dataset>

   </propertiesBasedSyncOptions>
  </task>   
 <task>
      <name>group</name>
      <bean>org.lsc.beans.SimpleBean</bean>
       <asyncLdapSourceService>
        <name>group-source-service</name>
        <connection reference="openldap" />
        <baseDn>ou=group,dc=domain,dc=com</baseDn>
        <pivotAttributes>
          <string>cn</string>
        </pivotAttributes>
        <fetchedAttributes>
          <string>cn</string>
          <string>description</string>
          <string>uniqueMember</string>
        </fetchedAttributes>
        <getAllFilter><![CDATA[(objectClass=groupOfUniqueNames)]]></getAllFilter>
        <getOneFilter><![CDATA[(&(objectClass=groupOfUniqueNames)(cn={cn}))]]></getOneFilter>
        <cleanFilter><![CDATA[(&(objectClass=groupOfUniqueNames)(cn={cn}))]]></cleanFilter>
        <serverType>OpenLDAP</serverType>
      </asyncLdapSourceService>
      <ldapDestinationService>
        <name>group-dst-service</name>
        <connection reference="AD" />
        <baseDn>OU=Group,OU=ORGNAME,DC=domain,DC=com</baseDn>
        <pivotAttributes>
          <string>cn</string>
        </pivotAttributes>
        <fetchedAttributes>
          <string>cn</string>
          <string>description</string>
          <string>member</string>
          <string>objectClass</string>
        </fetchedAttributes>
        <getAllFilter><![CDATA[(objectClass=group)]]></getAllFilter>
        <getOneFilter><![CDATA[(&(objectClass=group)(cn={cn}))]]></getOneFilter>
      </ldapDestinationService>
   <propertiesBasedSyncOptions>
           <mainIdentifier>js:"cn=" + javax.naming.ldap.Rdn.escapeValue(srcBean.getDatasetFirstValueById("cn")) + ",OU=Group,OU=ORGNAME,DC=domain,DC=com"</mainIdentifier>
        <defaultDelimiter>;</defaultDelimiter>
        <defaultPolicy>FORCE</defaultPolicy>
        <conditions>
          <create>true</create>
          <update>true</update>
          <delete>true</delete>
          <changeId>true</changeId>
        </conditions>
                <dataset>
          <name>objectclass</name>
          <policy>KEEP</policy>
          <createValues>
            <string>"group"</string>
            <string>"top"</string>
          </createValues>
        </dataset>
                <dataset>
         <name>member</name>
         <policy>KEEP</policy>
         <changeValues>
           <string>
           <![CDATA[rjs:
                var membersSrcDn = srcBean.getDatasetValuesById("uniqueMember");
                var membersDstDn = [];
                for  (var i=0; i<membersSrcDn.size(); i++) {
                        var memberSrcDn = membersSrcDn.get(i);
                        var uid = "";
                        try {
                                uid = srcLdap.attribute(memberSrcDn, "uid").get(0);
                        } catch(e) {
                                continue;
                        }
                        var destDn = ldap.search("ou=ORGNAME", "(sAMAccountName=" + uid + ")");
                        if (destDn.size() == 0 || destDn.size() > 1) {
                                continue;
                        }
                        var destMemberDn = destDn.get(0) + "," +  ldap.getContextDn();
                        membersDstDn.push(destMemberDn);
                }
                membersDstDn
           ]]>
           </string>
         </channgeValues>
      </dataset>
      </propertiesBasedSyncOptions>
      </task>
 </tasks>
</lsc>

In the end, the connector can do a lot of nice stuff and this is only one part of it. The problem I had was that they have a very poor documentation and you need to do a lot of digging to actually get stuff done.

Well that’s it folks!

Have a good one!