commit 8c4852b13bab5eb0c8dc09aa7c270052c5059dcb Author: Christian Pape Date: Thu Sep 9 14:59:44 2021 +0200 Working version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e59311 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.iws +*.iml +*.ipr +target/ +.DS_Store +.idea/ diff --git a/ldap-sync.sh b/ldap-sync.sh new file mode 100755 index 0000000..a43c278 --- /dev/null +++ b/ldap-sync.sh @@ -0,0 +1,2 @@ +#!/bin/bash +java -jar target/ldap-sync-1.0-SNAPSHOT-jar-with-dependencies.jar \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..deb3af3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,119 @@ + + + + 4.0.0 + + de.hsfulda.informatik + ldap-sync + 1.0-SNAPSHOT + + ldap-sync + + http://www.example.com + + + UTF-8 + 1.7 + 1.7 + + + + + com.unboundid + unboundid-ldapsdk + 6.0.1 + + + com.google.guava + guava + 30.1.1-jre + + + junit + junit + 4.11 + test + + + + + + + maven-assembly-plugin + + + foobar + package + + single + + + + + + jar-with-dependencies + + + + de.hsfulda.informatik.LdapSync + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + + diff --git a/src/main/java/de/hsfulda/informatik/AccountSource.java b/src/main/java/de/hsfulda/informatik/AccountSource.java new file mode 100644 index 0000000..b35eb14 --- /dev/null +++ b/src/main/java/de/hsfulda/informatik/AccountSource.java @@ -0,0 +1,101 @@ +package de.hsfulda.informatik; + +import com.unboundid.ldap.sdk.Entry; +import com.unboundid.ldap.sdk.ExtendedResult; +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.LDAPResult; +import com.unboundid.ldap.sdk.ResultCode; +import com.unboundid.ldap.sdk.SearchResult; +import com.unboundid.ldap.sdk.SearchResultEntry; +import com.unboundid.ldap.sdk.SearchScope; +import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest; +import com.unboundid.util.ssl.SSLUtil; +import com.unboundid.util.ssl.TrustAllTrustManager; + +import java.security.GeneralSecurityException; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class AccountSource { + final LDAPConnection connection; + final List searchResultEntryList; + final Set users; + final String baseDN; + + AccountSource(final String baseDN, final List searchResultEntryList) { + this.baseDN = baseDN; + this.searchResultEntryList = searchResultEntryList; + this.users = getEntries().stream() + .map(e -> e.getAttribute("cn").getValue().toLowerCase()) + .filter(e -> e.startsWith("fd")) + .collect(Collectors.toSet()); + connection = null; + } + + public AccountSource(final String host, final int port, final String bindDN, final String password, final String baseDN, final String filter, final String... attributes) throws LDAPException, GeneralSecurityException { + this.baseDN = baseDN; + final SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); + this.connection = new LDAPConnection(host, port); + final ExtendedResult extendedResult = connection.processExtendedOperation(new StartTLSExtendedRequest(sslUtil.createSSLContext())); + this.connection.bind(bindDN, password); + + if (extendedResult.getResultCode() != ResultCode.SUCCESS) { + throw new LDAPException(extendedResult.getResultCode()); + } + + final SearchResult searchResult = connection.search(baseDN, SearchScope.SUB, filter, attributes); + this.searchResultEntryList = searchResult.getSearchEntries(); + this.users = getEntries().stream() + .map(e -> e.getAttribute("cn").getValue().toLowerCase()) + .filter(e -> e.startsWith("fd")) + .collect(Collectors.toSet()); + } + + public List getEntries() { + return Collections.unmodifiableList(this.searchResultEntryList); + } + + public Set getUsers() { + return Collections.unmodifiableSet(this.users); + } + + public void add(final List usersToBeAdded) { + for (final Entry entry : usersToBeAdded) { + System.out.print("-> Füge Benutzer " + entry.getDN() + "..."); + final LDAPResult ldapResult; + try { + ldapResult = connection.add(entry); + + if (ldapResult.getResultCode().intValue() == 0) { + System.out.print("Ok\n"); + } else { + System.out.print("Failed (" + ldapResult.getResultCode() + ")\n"); + } + } catch (LDAPException e) { + System.out.print("Failed (" + e.getMessage() + ")\n"); + } + } + } + + public void del(final Set usersToBeDeleted) { + for (final String user : usersToBeDeleted) { + System.out.print("-> Lösche Benutzer cn=" + user + "," + this.baseDN + "..."); + final LDAPResult ldapResult; + + try { + ldapResult = connection.delete("cn=" + user + "," + this.baseDN); + + if (ldapResult.getResultCode().intValue() == 0) { + System.out.print("Ok\n"); + } else { + System.out.print("Failed (" + ldapResult.getResultCode() + ")\n"); + } + } catch (LDAPException e) { + System.out.print("Failed (" + e.getMessage() + ")\n"); + } + } + } +} diff --git a/src/main/java/de/hsfulda/informatik/LdapSync.java b/src/main/java/de/hsfulda/informatik/LdapSync.java new file mode 100644 index 0000000..f4a4cc8 --- /dev/null +++ b/src/main/java/de/hsfulda/informatik/LdapSync.java @@ -0,0 +1,135 @@ +package de.hsfulda.informatik; + +import com.unboundid.ldap.sdk.Attribute; +import com.unboundid.ldap.sdk.Entry; +import com.unboundid.ldap.sdk.LDAPException; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Ldap sync utility + */ +public class LdapSync { + final Properties properties = new Properties(); + final static Pattern pattern = Pattern.compile("^fd([a-z][a-z])?([0-9]*?)$"); + + public LdapSync() throws IOException, LDAPException, GeneralSecurityException { + // lade Konfiguration + properties.load(new FileReader("ldap-sync.properties")); + System.out.print("Abfrage der Benutzer im eDirectory..."); + + // lade Daten des Remote-Systems + final AccountSource remote = new AccountSource( + properties.getProperty("sync.src.host"), + Integer.parseInt(properties.getProperty("sync.src.port")), + properties.getProperty("sync.src.binddn"), + properties.getProperty("sync.src.bindpw"), + properties.getProperty("sync.src.basedn"), + properties.getProperty("sync.src.filter"), + new String[]{"cn", "givenname", "sn", "uid"} + ); + + System.out.print("Ok\nAbfrage der Benutzer im OpenLDAP..."); + + // lade Daten des lokalen Systems + final AccountSource local = new AccountSource( + properties.getProperty("sync.dst.host"), + Integer.parseInt(properties.getProperty("sync.dst.port")), + properties.getProperty("sync.dst.binddn"), + properties.getProperty("sync.dst.bindpw"), + properties.getProperty("sync.dst.basedn"), + properties.getProperty("sync.dst.filter"), + new String[]{} + ); + + System.out.print("Ok\n"); + + sync(remote, local); + } + + LdapSync(final AccountSource remote, final AccountSource local) throws IOException, LDAPException, GeneralSecurityException { + sync(remote, local); + } + + private void sync(final AccountSource remote, final AccountSource local) throws LDAPException { + Set usersToBeAdded = new TreeSet<>(remote.getUsers()); + usersToBeAdded.removeAll(local.getUsers()); + Set usersToBeDeleted = new TreeSet<>(local.getUsers()); + usersToBeDeleted.removeAll(remote.getUsers()); + + final List entriesToBeAdded = remote.getEntries().stream() + .filter(s -> usersToBeAdded.contains(s.getAttributeValue("cn").toLowerCase())) + .map(s -> { + final String cn = s.getAttributeValue("cn").toLowerCase(); + final String dn = "cn=" + cn + "," + local.baseDN; + final Entry e = new Entry(dn); + e.addAttribute(new Attribute("objectClass", "inetOrgPerson")); + e.addAttribute(new Attribute("objectClass", "shadowAccount")); + e.addAttribute(new Attribute("objectClass", "top")); + e.addAttribute(new Attribute("objectClass", "posixAccount")); + e.addAttribute(new Attribute("cn", cn)); + e.addAttribute(new Attribute("sn", s.getAttributeValue("sn"))); + e.addAttribute(new Attribute("givenname", s.getAttributeValue("givenName"))); + e.addAttribute(new Attribute("uid", cn)); + e.addAttribute(new Attribute("uidNumber", String.valueOf(computeUid(cn)))); + e.addAttribute(new Attribute("gidNumber", "20")); + e.addAttribute(new Attribute("loginShell", "/bin/zsh")); + e.addAttribute(new Attribute("homeDirectory", "/Users/" + cn)); + e.addAttribute(new Attribute("userPassword", "{SASL}" + cn)); + + return e; + }).collect(Collectors.toList()); + + System.out.print("Zu erzeugende Benutzer: " + entriesToBeAdded.size() + ", Zu löschende Benutzer: " + usersToBeDeleted.size() + "\n"); + + local.del(usersToBeDeleted); + local.add(entriesToBeAdded); + } + + static Integer computeUid(final String cn) { + final Matcher m = pattern.matcher(cn); + + if (m.find()) { + int a = 0; + int b = 0; + + if (m.group(1) != null) { + a = m.group(1).charAt(0) - 96; + b = m.group(1).charAt(1) - 96; + } + + final int s = m.group(2).length(); + final int z = Integer.parseInt(m.group(2)); + + final int uid = (a * 1000 + b * 10 + s) * 10000 + z; + + return uid; + } + + return null; + } + + public static void main(String[] args) { + try { + final LdapSync ldapSync = new LdapSync(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (LDAPException e) { + e.printStackTrace(); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/de/hsfulda/informatik/LdapSyncTest.java b/src/test/java/de/hsfulda/informatik/LdapSyncTest.java new file mode 100644 index 0000000..437df3b --- /dev/null +++ b/src/test/java/de/hsfulda/informatik/LdapSyncTest.java @@ -0,0 +1,111 @@ +package de.hsfulda.informatik; + +import com.unboundid.ldap.sdk.Attribute; +import com.unboundid.ldap.sdk.Entry; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.SearchResultEntry; +import org.junit.Test; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class LdapSyncTest { + private List addList; + private Set delSet; + + @Test + public void testSync() throws LDAPException, GeneralSecurityException, IOException { + final List remoteEntries = new ArrayList<>(); + remoteEntries.add(searchResultEntry("dn=Fd1234,dc=remote,dc=de", "fD1234", "Mustermann", "Mustermann")); + remoteEntries.add(searchResultEntry("dn=fDai1235,dc=remote,dc=de", "FDai1235", "Schuster", "Schuster")); + remoteEntries.add(searchResultEntry("dn=fdaI1236,dc=remote,dc=de", "fdAI1236", "Bunsen", "Bunsen")); + remoteEntries.add(searchResultEntry("dn=fDeT123,dc=remote,dc=de", "FDET123", "Bauer", "Bauer")); + remoteEntries.add(searchResultEntry("dn=fdXx9999,dc=remote,dc=de", "fdxX9999", "Schmidt", "Schmidt")); + final AccountSource remote = new AccountSource("dc=remote,dc=de", remoteEntries); + + final List localEntries = new ArrayList<>(); + localEntries.add(searchResultEntry("dn=Fdai1236,dc=local,dc=de", "fDai1236", "Bunsen", "Bunsen")); + localEntries.add(searchResultEntry("dn=fDai1237,dc=local,dc=de", "fdAi1237", "Beaker", "Beaker")); + localEntries.add(searchResultEntry("dn=fdAi1238,dc=local,dc=de", "fdaI1238", "Hopper", "Hopper")); + final AccountSource local = new AccountSource("dc=local,dc=de", localEntries) { + @Override + public void add(List usersToBeAdded) { + addList = usersToBeAdded; + } + + @Override + public void del(Set usersToBeDeleted) { + delSet = usersToBeDeleted; + } + }; + + final LdapSync ldapSync = new LdapSync(remote, local); + + assertEquals(delSet.size(), 2); + assertTrue(delSet.contains("fdai1237")); + assertTrue(delSet.contains("fdai1238")); + + assertEquals(addList.size(), 4); + + for (final Entry entry : addList) { + assertTrue(entry.getDN().endsWith(",dc=local,dc=de")); + + final List objectClasses = Arrays.asList(entry.getObjectClassValues()); + assertTrue(objectClasses.contains("shadowAccount")); + assertTrue(objectClasses.contains("posixAccount")); + assertTrue(objectClasses.contains("top")); + assertTrue(objectClasses.contains("inetOrgPerson")); + + final List attributes = entry.getAttributes().stream().map(a -> a.getName()).collect(Collectors.toList()); + assertTrue(attributes.contains("uid")); + assertTrue(attributes.contains("cn")); + assertTrue(attributes.contains("givenname")); + assertTrue(attributes.contains("sn")); + assertTrue(attributes.contains("uidNumber")); + assertTrue(attributes.contains("gidNumber")); + assertTrue(attributes.contains("homeDirectory")); + assertTrue(attributes.contains("loginShell")); + + assertEquals(entry.getAttributeValue("cn").toLowerCase(), entry.getAttributeValue("cn")); + assertEquals(entry.getAttributeValue("uid").toLowerCase(), entry.getAttributeValue("uid")); + assertEquals(entry.getDN().toLowerCase(), entry.getDN()); + + System.out.println(); + + for (final String string : entry.toLDIF()) { + System.out.println(string); + } + } + } + + @Test + public void testUidNumber() { + assertEquals(41000, (int) LdapSync.computeUid("fd1000")); + assertEquals(49999, (int) LdapSync.computeUid("fd9999")); + assertEquals(10141000, (int) LdapSync.computeUid("fdaa1000")); + assertEquals(10149999, (int) LdapSync.computeUid("fdaa9999")); + assertEquals(262641000, (int) LdapSync.computeUid("fdzz1000")); + assertEquals(262649999, (int) LdapSync.computeUid("fdzz9999")); + } + + private SearchResultEntry searchResultEntry(final String dn, final String cn, final String sn, final String givenname) { + final Attribute[] attributes = { + new Attribute("cn", cn), + new Attribute("uid", cn), + new Attribute("sn", sn), + new Attribute("givenname", givenname), + new Attribute("objectClass", "inetOrgPerson"), + new Attribute("objectClass", "top"), + }; + + return new SearchResultEntry(dn, attributes); + } +}