/* * Copyright (c) 2010-2017 Evolveum * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.evolveum.midpoint.test.ldap; import static org.testng.AssertJUnit.assertEquals; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.Validate; import org.opends.messages.Message; import org.opends.messages.MessageBuilder; import org.opends.server.config.ConfigException; import org.opends.server.core.AddOperation; import org.opends.server.core.BindOperation; import org.opends.server.core.DeleteOperation; import org.opends.server.core.ModifyDNOperation; import org.opends.server.core.ModifyOperation; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.protocols.internal.InternalSearchOperation; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeValue; import org.opends.server.types.ByteString; import org.opends.server.types.DN; import org.opends.server.types.DereferencePolicy; import org.opends.server.types.DirectoryEnvironmentConfig; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.InitializationException; import org.opends.server.types.LDIFImportConfig; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchResultEntry; import org.opends.server.types.SearchScope; import org.opends.server.util.ChangeRecordEntry; import org.opends.server.util.EmbeddedUtils; import org.opends.server.util.LDIFException; import org.opends.server.util.LDIFReader; import org.opends.server.util.ModifyChangeRecordEntry; import org.opends.server.util.ModifyDNChangeRecordEntry; import org.testng.AssertJUnit; import com.evolveum.midpoint.test.util.MidPointAsserts; import com.evolveum.midpoint.util.MiscUtil; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; /** * This class controls embedded OpenDJ instance. * * It is used in Unit tests. It configures and starts and stops the instance. It * can even manage a "template" configuration of OpenDJ and copy it to working * instance configuration. * * @author Radovan Semancik */ public class OpenDJController extends AbstractResourceController { private String DATA_TEMPLATE_DIR = "test-data"; private String SERVER_ROOT = "target/test-data/opendj"; private String LDAP_SUFFIX = "dc=example,dc=com"; public static final String DEFAULT_TEMPLATE_NAME = "opendj.template"; public static final String RI_TEMPLATE_NAME = "opendj.template.ri"; public static final String OBJECT_CLASS_INETORGPERSON_NAME = "inetOrgPerson"; public static final String RESOURCE_OPENDJ_PRIMARY_IDENTIFIER_LOCAL_NAME = "entryUUID"; public static final String RESOURCE_OPENDJ_SECONDARY_IDENTIFIER_LOCAL_NAME = "dn"; protected File serverRoot = new File(SERVER_ROOT); protected File configFile = null; protected File templateRoot; private static final Trace LOGGER = TraceManager.getTrace(OpenDJController.class); protected InternalClientConnection internalConnection; public OpenDJController() { init(); } public OpenDJController(String serverRoot) { SERVER_ROOT = serverRoot; this.serverRoot = new File(serverRoot); init(); } /** * Initialize * */ private void init() { if (!serverRoot.exists()) { serverRoot.mkdirs(); } if (configFile == null) { configFile = new File(serverRoot, "config/config.ldif"); } } /** * Get the value of serverRoot. * * The top directory of working OpenDS installation. The OpenDS placed in * this directory will be used during the tests. * * @return the value of serverRoot */ public File getServerRoot() { return this.serverRoot; } /** * Set the value of serverRoot * * The top directory of working OpenDS installation. The OpenDS placed in * this directory will be used during the tests. * * @param serverRoot * new value of serverRoot */ public void setServerRoot(File serverRoot) { this.serverRoot = serverRoot; } /** * Get the value of configFile * * File name of primary OpenDS configuration file. Normally * <serverRoot>/config/config.ldif * * @return the value of configFile */ public File getConfigFile() { return configFile; } /** * Set the value of configFile * * File name of primary OpenDS configuration file. Normally * <serverRoot>/config/config.ldif * * @param configFile * new value of configFile */ public void setConfigFile(File configFile) { this.configFile = configFile; } /** * Get the value of templateServerRoot * * The top directory of template OpenDS installation. All the files from * this directory will be copied to the working OpenDS directory * (serverRoot). This usually happens before the tests. * * @return the value of templateServerRoot */ public File getTemplateServerRoot() { return templateRoot; } public String getSuffix() { return LDAP_SUFFIX; } public String getSuffixPeople() { return "ou=People,"+LDAP_SUFFIX; } public String getAccountDn(String username) { return "uid="+username+","+getSuffixPeople(); } /** * Get the value of internalConnection * * The connection to the OpenDS instance. It can be used to fetch and * manipulate the data. * * @return the value of internelConnection */ public InternalClientConnection getInternalConnection() { Validate.notNull(internalConnection, "Not connected"); return internalConnection; } /** * Refresh working OpenDS installation from the template. * * The existing working OpenDS installation (in serverRoot) will be * discarded and replaced by a fresh known-state setup (from * templateServerRoot). * * @throws IOException * @throws URISyntaxException */ public void refreshFromTemplate(String templateName) throws IOException, URISyntaxException { deleteDirectory(serverRoot); extractTemplate(serverRoot, templateName); } /** * Extract template from class */ private void extractTemplate(File dst, String templateName) throws IOException, URISyntaxException { LOGGER.info("Extracting OpenDJ template...."); if (!dst.exists()) { LOGGER.debug("Creating target dir {}", dst.getPath()); dst.mkdirs(); } templateRoot = new File(DATA_TEMPLATE_DIR, templateName); String templateRootPath = DATA_TEMPLATE_DIR + "/" + templateName; // templateRoot.getPath does not work on Windows, as it puts "\" into the path name (leading to problems with getSystemResource) // Determing if we need to extract from JAR or directory if (templateRoot.isDirectory()) { LOGGER.trace("Need to do directory copy."); MiscUtil.copyDirectory(templateRoot, dst); return; } LOGGER.debug("Try to localize OpenDJ Template in JARs as " + templateRootPath); URL srcUrl = ClassLoader.getSystemResource(templateRootPath); LOGGER.debug("srcUrl " + srcUrl); // sample: // file:/C:/.m2/repository/test-util/1.9-SNAPSHOT/test-util-1.9-SNAPSHOT.jar!/test-data/opendj.template // output: // /C:/.m2/repository/test-util/1.9-SNAPSHOT/test-util-1.9-SNAPSHOT.jar // // beware that in the URL there can be spaces encoded as %20, e.g. // file:/C:/Documents%20and%20Settings/user/.m2/repository/com/evolveum/midpoint/infra/test-util/2.1-SNAPSHOT/test-util-2.1-SNAPSHOT.jar!/test-data/opendj.template // if (srcUrl.getPath().contains("!/")) { URI srcFileUri = new URI(srcUrl.getPath().split("!/")[0]); // e.g. file:/C:/Documents%20and%20Settings/user/.m2/repository/com/evolveum/midpoint/infra/test-util/2.1-SNAPSHOT/test-util-2.1-SNAPSHOT.jar File srcFile = new File(srcFileUri); JarFile jar = new JarFile(srcFile); LOGGER.debug("Extracting OpenDJ from JAR file {} to {}", srcFile.getPath(), dst.getPath()); Enumeration<JarEntry> entries = jar.entries(); JarEntry e; byte buf[] = new byte[655360]; while (entries.hasMoreElements()) { e = entries.nextElement(); // skip other files if (!e.getName().contains(templateRootPath)) { continue; } // prepare destination file String filepath = e.getName().substring(templateRootPath.length()); File dstFile = new File(dst, filepath); // test if directory if (e.isDirectory()) { LOGGER.debug("Create directory: {}", dstFile.getAbsolutePath()); dstFile.mkdirs(); continue; } LOGGER.debug("Extract {} to {}", filepath, dstFile.getAbsolutePath()); // Find file on classpath InputStream is = ClassLoader.getSystemResourceAsStream(e.getName()); // InputStream is = jar.getInputStream(e); //old way // Copy content OutputStream out = new FileOutputStream(dstFile); int len; while ((len = is.read(buf)) > 0) { out.write(buf, 0, len); } out.close(); is.close(); } jar.close(); } else { try { File file = new File(srcUrl.toURI()); File[] files = file.listFiles(); for (File subFile : files) { if (subFile.isDirectory()) { MiscUtil.copyDirectory(subFile, new File(dst, subFile.getName())); } else { MiscUtil.copyFile(subFile, new File(dst, subFile.getName())); } } } catch (Exception ex) { throw new IOException(ex); } } LOGGER.debug("OpenDJ Extracted"); } /** * Start the embedded OpenDJ directory server using files copied from the default * template. */ public InternalClientConnection startCleanServer() throws IOException, URISyntaxException { return startCleanServer(DEFAULT_TEMPLATE_NAME); } /** * Start the embedded OpenDJ directory server using files copied from the * template with referential integrity plugin turned on. */ public InternalClientConnection startCleanServerRI() throws IOException, URISyntaxException { return startCleanServer(RI_TEMPLATE_NAME); } /** * Start the embedded OpenDJ directory server using files copied from the specified * template. */ public InternalClientConnection startCleanServer(String templateName) throws IOException, URISyntaxException { refreshFromTemplate(templateName); return start(); } /** * Start the embedded OpenDJ directory server. * * Configuration and databases from serverRoot location will be used. * * @return */ public InternalClientConnection start() { LOGGER.info("Starting OpenDJ server"); DirectoryEnvironmentConfig envConfig = new DirectoryEnvironmentConfig(); try { envConfig.setServerRoot(serverRoot); envConfig.setConfigFile(configFile); // envConfig.setDisableConnectionHandlers(true); } catch (InitializationException ex) { ex.printStackTrace(); throw new RuntimeException("OpenDJ initialization failed", ex); } // Check if the server is already running if (EmbeddedUtils.isRunning()) { throw new RuntimeException("Server already running"); } else { try { EmbeddedUtils.startServer(envConfig); } catch (ConfigException ex) { LOGGER.error("Possible OpenDJ misconfiguration: " + ex.getMessage(), ex); throw new RuntimeException("OpenDJ startup failed", ex); } catch (InitializationException ex) { LOGGER.error("OpenDJ startup failed", ex); throw new RuntimeException("OpenDJ startup failed", ex); } } internalConnection = InternalClientConnection.getRootConnection(); if (internalConnection == null) { LOGGER.error("OpenDJ cannot get internal connection (null)"); throw new RuntimeException("OpenDS cannot get internal connection (null)"); } LOGGER.info("OpenDJ server started"); return internalConnection; } /** * Stop the embedded OpenDS server. * */ public void stop() { if (EmbeddedUtils.isRunning()) { LOGGER.debug("Stopping OpenDJ server"); EmbeddedUtils.stopServer(this.getClass().getName(), Message.EMPTY); LOGGER.info("OpenDJ server is stopped"); } else { LOGGER.warn("Attempt to stop OpenDJ server that is already stopped."); } } public boolean isRunning() { return EmbeddedUtils.isRunning(); } public void assumeRunning() { if (!isRunning()) { start(); } } public void assumeStopped() { if (isRunning()) { stop(); } } /** * Delete a directory and its contents. * * @param dir * The name of the directory to delete. * @throws IOException * If the directory could not be deleted. */ public static void deleteDirectory(File dir) throws IOException { if (dir.isDirectory()) { // Recursively delete sub-directories and files. for (String child : dir.list()) { deleteDirectory(new File(dir, child)); } } dir.delete(); } public Set<String> asSet(List<Attribute> attributes) { // Just blindly get the fist one now. // There is most likely just one anyway. // TODO: improve that later // Attribute attr = attributes.get(0); Set<String> result = new HashSet<String>(); // TODO find newer OpenDS jar // Iterator<AttributeValue> iterator = attr.iterator(); // while (iterator.hasNext()) { // result.add(iterator.next().toString()); // } return result; } // Generic utility methods public Entry searchByEntryUuid(String entryUuid) throws DirectoryException { InternalSearchOperation op = getInternalConnection().processSearch( "dc=example,dc=com", SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 100, 100, false, "(entryUUID=" + entryUuid + ")", getSearchAttributes()); LinkedList<SearchResultEntry> searchEntries = op.getSearchEntries(); if (searchEntries == null || searchEntries.isEmpty()) { return null; } if (searchEntries.size() > 1) { AssertJUnit.fail("Multiple matches for Entry UUID "+entryUuid+": "+searchEntries); } return searchEntries.get(0); } public Entry searchAndAssertByEntryUuid(String entryUuid) throws DirectoryException { Entry entry = searchByEntryUuid(entryUuid); if (entry == null) { AssertJUnit.fail("Entry UUID "+entryUuid+" not found"); } return entry; } public Entry searchSingle(String filter) throws DirectoryException { InternalSearchOperation op = getInternalConnection().processSearch( getSuffix(), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 100, 100, false, filter, getSearchAttributes()); if (op.getEntriesSent() == 0) { return null; } else if (op.getEntriesSent() > 1) { AssertJUnit.fail("Found too many entries ("+op.getEntriesSent()+") for filter "+filter); } return op.getSearchEntries().get(0); } public Entry searchByUid(String string) throws DirectoryException { return searchSingle("(uid=" + string + ")"); } public Entry fetchEntry(String dn) throws DirectoryException { Validate.notNull(dn); InternalSearchOperation op = getInternalConnection().processSearch( dn, SearchScope.BASE_OBJECT, DereferencePolicy.NEVER_DEREF_ALIASES, 100, 100, false, "(objectclass=*)", getSearchAttributes()); if (op.getEntriesSent() == 0) { return null; } else if (op.getEntriesSent() > 1) { AssertJUnit.fail("Found too many entries ("+op.getEntriesSent()+") for dn "+dn); } return op.getSearchEntries().get(0); } public Entry fetchAndAssertEntry(String dn, String objectClass) throws DirectoryException { Entry entry = fetchEntry(dn); AssertJUnit.assertNotNull("No entry for DN "+dn, entry); assertDn(entry, dn); assertObjectClass(entry, objectClass); return entry; } private LinkedHashSet<String> getSearchAttributes() { LinkedHashSet<String> attrs = new LinkedHashSet<String>(); attrs.add("*"); attrs.add("ds-pwp-account-disabled"); return attrs; } public boolean isAccountEnabled(Entry ldapEntry) { String pwpAccountDisabled = getAttributeValue(ldapEntry, "ds-pwp-account-disabled"); if (pwpAccountDisabled != null && pwpAccountDisabled.equalsIgnoreCase("true")) { return false; } return true; } public static String getAttributeValue(Entry response, String name) { List<Attribute> attrs = response.getAttribute(name.toLowerCase()); if (attrs == null || attrs.size() == 0) { return null; } assertEquals("Too many attributes for name "+name+": ", 1, attrs.size()); Attribute attribute = attrs.get(0); return getAttributeValue(attribute); } public static String getAttributeValue(Attribute attribute) { return attribute.iterator().next().getValue().toString(); } public static byte[] getAttributeValueBinary(Entry response, String name) { List<Attribute> attrs = response.getAttribute(name.toLowerCase()); if (attrs == null || attrs.size() == 0) { return null; } assertEquals("Too many attributes for name "+name+": ", 1, attrs.size()); Attribute attribute = attrs.get(0); ByteString value = attribute.iterator().next().getValue(); return value.toByteArray(); } public static Collection<String> getAttributeValues(Entry response, String name) { List<Attribute> attrs = response.getAttribute(name.toLowerCase()); if (attrs == null || attrs.size() == 0) { return null; } assertEquals("Too many attributes for name "+name+": ", 1, attrs.size()); Attribute attribute = attrs.get(0); Collection<String> values = new ArrayList<String>(attribute.size()); Iterator<AttributeValue> iterator = attribute.iterator(); while (iterator.hasNext()) { AttributeValue attributeValue = iterator.next(); values.add(attributeValue.getValue().toString()); } return values; } public static String getDn(Entry response) { DN dn = response.getDN(); return dn.toString(); } public static void assertDn(Entry response, String expected) throws DirectoryException { DN actualDn = response.getDN(); if (actualDn.compareTo(DN.decode(expected)) != 0) { AssertJUnit.fail("Wrong DN, expected "+expected+" but was "+actualDn.toString()); } } public void assertNoEntry(String dn) throws DirectoryException { Entry entry = fetchEntry(dn); if (entry != null) { AssertJUnit.fail("Found entry for dn "+dn+" while not expecting it: "+entry); } } public static void assertObjectClass(Entry response, String expected) throws DirectoryException { Collection<String> objectClassValues = getAttributeValues(response, "objectClass"); AssertJUnit.assertTrue("Wrong objectclass for entry "+getDn(response)+", expected "+expected+" but got "+objectClassValues, objectClassValues.contains(expected)); } public static void assertNoObjectClass(Entry response, String unexpected) throws DirectoryException { Collection<String> objectClassValues = getAttributeValues(response, "objectClass"); AssertJUnit.assertFalse("Unexpected objectclass for entry "+getDn(response)+": "+unexpected+", got "+objectClassValues, objectClassValues.contains(unexpected)); } public void assertUniqueMember(Entry groupEntry, String accountDn) throws DirectoryException { Collection<String> members = getAttributeValues(groupEntry, "uniqueMember"); assertContainsDn("No member "+accountDn+" in group "+getDn(groupEntry), members, accountDn); } public static void assertContainsDn(String message, Collection<String> actualValues, String expectedValue) throws DirectoryException { AssertJUnit.assertNotNull(message+", expected "+expectedValue+", got null", actualValues); DN expectedDn = DN.decode(expectedValue); for (String actualValue: actualValues) { DN actualDn = DN.decode(actualValue); if (actualDn.compareTo(expectedDn)==0) { return; } } AssertJUnit.fail(message+", expected "+expectedValue+", got "+actualValues); } public void assertUniqueMember(String groupDn, String accountDn) throws DirectoryException { Entry groupEntry = fetchEntry(groupDn); assertUniqueMember(groupEntry, accountDn); } public void assertNoUniqueMember(String groupDn, String accountDn) throws DirectoryException { Entry groupEntry = fetchEntry(groupDn); assertNoUniqueMember(groupEntry, accountDn); } public void assertNoUniqueMember(Entry groupEntry, String accountDn) { Collection<String> members = getAttributeValues(groupEntry, "uniqueMember"); MidPointAsserts.assertNotContainsCaseIgnore("Member "+accountDn+" in group "+getDn(groupEntry), members, accountDn); } public static void assertAttribute(Entry response, String name, String... values) { List<Attribute> attrs = response.getAttribute(name.toLowerCase()); if (attrs == null || attrs.size() == 0) { if (values.length == 0) { return; } else { AssertJUnit.fail("Attribute "+name+" does not have any value"); } } assertEquals("Too many \"attributes\" for "+name+": ", 1, attrs.size()); Attribute attribute = response.getAttribute(name.toLowerCase()).get(0); if (values.length != attribute.size()) { AssertJUnit.fail("Wrong number of values for attribute "+name+", expected "+values.length+" values but got "+attribute.size()+" values: "+attribute); } for (String value: values) { boolean found = false; Iterator<AttributeValue> iterator = attribute.iterator(); List<String> attrVals = new ArrayList<String>(); while (iterator.hasNext()) { AttributeValue attributeValue = iterator.next(); String attrVal = attributeValue.toString(); attrVals.add(attrVal); if (attrVal.equals(value)) { found = true; } } if (!found) { AssertJUnit.fail("Attribute "+name+" does not contain value "+value+", it has values: "+attrVals); } } } public static void assertNoAttribute(Entry response, String name) { List<Attribute> attrs = response.getAttribute(name.toLowerCase()); if (attrs == null || attrs.size() == 0) { return; } assertEquals("Too many \"attributes\" for "+name+": ", 1, attrs.size()); Attribute attribute = response.getAttribute(name.toLowerCase()).get(0); if (attribute.size() == 0) { return; } if (attribute.isEmpty()) { return; } AssertJUnit.fail("Attribute "+name+" exists while not expecting it: "+attribute); } public void assertActive(Entry response, boolean active) { assertEquals("Unexpected activation of entry "+response, active, isAccountEnabled(response)); } public Entry addEntryFromLdifFile(File file) throws IOException, LDIFException { return addEntryFromLdifFile(file.getPath()); } public Entry addEntryFromLdifFile(String filename) throws IOException, LDIFException { LDIFImportConfig importConfig = new LDIFImportConfig(filename); LDIFReader ldifReader = new LDIFReader(importConfig); Entry ldifEntry = ldifReader.readEntry(); addEntry(ldifEntry); return ldifEntry; } public List<Entry> addEntriesFromLdifFile(String filename) throws IOException, LDIFException { List<Entry> retval = new ArrayList<>(); LDIFImportConfig importConfig = new LDIFImportConfig(filename); LDIFReader ldifReader = new LDIFReader(importConfig); for (;;) { Entry ldifEntry = ldifReader.readEntry(); if (ldifEntry == null) { break; } addEntry(ldifEntry); retval.add(ldifEntry); } return retval; } public void addEntry(Entry ldapEntry) { AddOperation addOperation = getInternalConnection().processAdd(ldapEntry); if (ResultCode.SUCCESS != addOperation.getResultCode()) { throw new RuntimeException("LDAP operation error: "+addOperation.getResultCode()+": "+addOperation.getErrorMessage()); } } public void addEntry(String ldif) throws IOException, LDIFException { LDIFImportConfig importConfig = new LDIFImportConfig(IOUtils.toInputStream(ldif, "utf-8")); LDIFReader ldifReader = new LDIFReader(importConfig); Entry ldifEntry = ldifReader.readEntry(); addEntry(ldifEntry); } public ChangeRecordEntry executeRenameChange(String filename) throws LDIFException, IOException{ LDIFImportConfig importConfig = new LDIFImportConfig(filename); LDIFReader ldifReader = new LDIFReader(importConfig); ChangeRecordEntry entry = ldifReader.readChangeRecord(false); if (!(entry instanceof ModifyDNChangeRecordEntry)){ throw new LDIFException(new MessageBuilder("Could not execute rename..Bad change").toMessage()); } ModifyDNOperation modifyOperation = getInternalConnection().processModifyDN((ModifyDNChangeRecordEntry)entry); if (ResultCode.SUCCESS != modifyOperation.getResultCode()) { throw new RuntimeException("LDAP operation error: "+modifyOperation.getResultCode()+": "+modifyOperation.getErrorMessage()); } return entry; } public ChangeRecordEntry executeLdifChange(File file) throws IOException, LDIFException { LDIFImportConfig importConfig = new LDIFImportConfig(file.getPath()); LDIFReader ldifReader = new LDIFReader(importConfig); ChangeRecordEntry entry = ldifReader.readChangeRecord(false); ModifyOperation modifyOperation = getInternalConnection() .processModify((ModifyChangeRecordEntry) entry); if (ResultCode.SUCCESS != modifyOperation.getResultCode()) { throw new RuntimeException("LDAP operation error: "+modifyOperation.getResultCode()+": "+modifyOperation.getErrorMessage()); } return entry; } public ChangeRecordEntry executeLdifChange(String ldif) throws IOException, LDIFException { InputStream ldifInputStream = IOUtils.toInputStream(ldif, "UTF-8"); LDIFImportConfig importConfig = new LDIFImportConfig(ldifInputStream); LDIFReader ldifReader = new LDIFReader(importConfig); ChangeRecordEntry entry = ldifReader.readChangeRecord(false); ModifyOperation modifyOperation = getInternalConnection() .processModify((ModifyChangeRecordEntry) entry); if (ResultCode.SUCCESS != modifyOperation.getResultCode()) { throw new RuntimeException("LDAP operation error: "+modifyOperation.getResultCode()+": "+modifyOperation.getErrorMessage()); } return entry; } public ChangeRecordEntry modifyReplace(String entryDn, String attributeName, String value) throws IOException, LDIFException { String ldif = "dn: " + entryDn + "\nchangetype: modify\nreplace: "+attributeName+"\n"+attributeName+": " + value; return executeLdifChange(ldif); } public ChangeRecordEntry modifyAdd(String entryDn, String attributeName, String value) throws IOException, LDIFException { String ldif = "dn: " + entryDn + "\nchangetype: modify\nadd: "+attributeName+"\n"+attributeName+": " + value; return executeLdifChange(ldif); } public ChangeRecordEntry modifyDelete(String entryDn, String attributeName, String value) throws IOException, LDIFException { String ldif = "dn: " + entryDn + "\nchangetype: modify\ndelete: "+attributeName+"\n"+attributeName+": " + value; return executeLdifChange(ldif); } public void delete(String entryDn) { DeleteOperation deleteOperation = getInternalConnection().processDelete(entryDn); if (ResultCode.SUCCESS != deleteOperation.getResultCode()) { throw new RuntimeException("LDAP operation error: "+deleteOperation.getResultCode()+": "+deleteOperation.getErrorMessage()); } } public String dumpEntries() throws DirectoryException { InternalSearchOperation op = getInternalConnection().processSearch( LDAP_SUFFIX, SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 100, 100, false, "(objectclass=*)", getSearchAttributes()); StringBuilder sb = new StringBuilder(); for (SearchResultEntry searchEntry: op.getSearchEntries()) { sb.append(toHumanReadableLdifoid(searchEntry)); sb.append("\n"); } return sb.toString(); } public String dumpTree() throws DirectoryException { StringBuilder sb = new StringBuilder(); sb.append(LDAP_SUFFIX).append("\n"); dumpTreeLevel(sb, LDAP_SUFFIX, 1); return sb.toString(); } private void dumpTreeLevel(StringBuilder sb, String dn, int indent) throws DirectoryException { InternalSearchOperation op = getInternalConnection().processSearch( dn, SearchScope.SINGLE_LEVEL, DereferencePolicy.NEVER_DEREF_ALIASES, 100, 100, false, "(objectclass=*)", getSearchAttributes()); for (SearchResultEntry searchEntry: op.getSearchEntries()) { ident(sb, indent); sb.append(searchEntry.getDN().getRDN()); sb.append("\n"); dumpTreeLevel(sb, searchEntry.getDN().toString(), indent + 1); } } private void ident(StringBuilder sb, int indent) { for(int i=0; i < indent; i++) { sb.append(" "); } } public String toHumanReadableLdifoid(Entry entry) { StringBuilder sb = new StringBuilder(); sb.append("dn: ").append(entry.getDN()).append("\n"); for (Attribute attribute: entry.getAttributes()) { for (AttributeValue val: attribute) { sb.append(attribute.getName()); sb.append(": "); sb.append(val); sb.append("\n"); } } return sb.toString(); } public Collection<String> getGroupUniqueMembers(String groupDn) throws DirectoryException { Entry groupEntry = fetchEntry(groupDn); if (groupEntry == null) { throw new IllegalArgumentException(groupDn + " was not found"); } return getAttributeValues(groupEntry, "uniqueMember"); } /* dn: <group> changetype: modify delete: uniqueMember uniqueMember: <member> */ public ChangeRecordEntry removeGroupUniqueMember(String groupDn, String memberDn) throws IOException, LDIFException { String ldif = "dn: " + groupDn + "\nchangetype: modify\ndelete: uniqueMember\nuniqueMember: " + memberDn; return executeLdifChange(ldif); } public ChangeRecordEntry addGroupUniqueMember(String groupDn, String memberDn) throws IOException, LDIFException { String ldif = "dn: " + groupDn + "\nchangetype: modify\nadd: uniqueMember\nuniqueMember: " + memberDn; return executeLdifChange(ldif); } public ChangeRecordEntry addGroupUniqueMembers(String groupDn, List<String> memberDns) throws IOException, LDIFException { if (memberDns.isEmpty()) { return null; // garbage in garbage out, sorry } StringBuilder sb = new StringBuilder(); sb.append("dn: ").append(groupDn).append("\nchangetype: modify\nadd: uniqueMember"); for (String memberDn : memberDns) { sb.append("\nuniqueMember: ").append(memberDn); } return executeLdifChange(sb.toString()); } public boolean checkPassword(String entryDn, String password) throws DirectoryException { InternalClientConnection conn = new InternalClientConnection(DN.decode(entryDn)); BindOperation op = conn.processSimpleBind(entryDn, password); if (op.getResultCode() == ResultCode.SUCCESS) { return true; } else { LOGGER.error("Bind error: {} ({})", op.getAuthFailureReason(), op.getResultCode()); return false; } } public void assertPassword(String entryDn, String password) throws DirectoryException { if (!checkPassword(entryDn, password)) { AssertJUnit.fail("Expected that entry "+entryDn+" will have password '"+password+"'. But the check failed."); } } }