package org.slc.sli.ldap.inmemory; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.sdk.*; import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedRequest; import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedResult; import com.unboundid.ldap.sdk.schema.Schema; import com.unboundid.ldif.LDIFChangeRecord; import com.unboundid.ldif.LDIFReader; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slc.sli.ldap.inmemory.domain.Ldif; import org.slc.sli.ldap.inmemory.domain.Listener; import org.slc.sli.ldap.inmemory.domain.PasswordRewriteStrategy; import org.slc.sli.ldap.inmemory.domain.Server; import org.slc.sli.ldap.inmemory.loghandlers.AccessLogHandler; import org.slc.sli.ldap.inmemory.loghandlers.LDAPDebugLogHandler; import org.slc.sli.ldap.inmemory.utils.ConfigurationLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileNotFoundException; import java.io.InputStream; import java.net.BindException; import java.net.InetAddress; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; /** * LDAP Server implementation using UnboundedId Java SDK to expose an in-memory LDAP server. * The LDIF file was generated by using Apache Directory studio to export an LDIF file from * the hosted LDAP server. Then, the LdifAdjustor was run against that export file to add * missing changetype attributes -- which are used to import the LDIF file into the in-memory * LDAP provider. * Also, be aware, the corresponding LDAP schema is also specified to be able to properly load * the LDIF export. * TODO Add an Mbean to expose server controls (generate snapshots/LDIF exports, etc), or could use REST endpoints but that would couple to servlet deployment. * TODO incorporate Spring for DI once implementation settles. */ public class LdapServerImpl { private final static Logger LOG = LoggerFactory.getLogger(LdapServerImpl.class); /** * Lock timeout value. If needed, move this value into config XML. */ private static final int LOCK_TIMEOUT = 60; /** * Lock timeout value Time Unit. If needed, move this value into config XML. */ private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS; public static final String CONFIG_FILE = "ldap-inmemory-config.xml"; public static final String CONFIG_SCHEMA_FILE = "ldap-inmemory-config.xsd"; public static final String LDIF_SCHEMA_FILE = "ldif" + File.separator + "ldap-schema.ldif"; private static final String LOCK_TIMEOUT_MSG = "Unable to obtain lock due to timeout after " + LOCK_TIMEOUT + " " + TIME_UNIT.toString(); private static final String SERVER_NOT_STARTED = "The LDAP server is not started."; private static final String SERVER_ALREADY_STARTED = "The LDAP server is already started."; //should wrap this with atomicreference private InMemoryDirectoryServer server; private PasswordRewriteStrategy passwordStrategy = PasswordRewriteStrategy.getInstance(); /** * Either synchronize access to start/stop methods, or protect isStarted with a lock. */ private final AtomicBoolean isStarted = new AtomicBoolean(Boolean.FALSE); /** * Lock used to ensure threadsafe server state changes (e.g. startup, shutdown, etc). */ private final ReentrantLock serverStateLock = new ReentrantLock(); //should wrap this with atomicreference /** * Encapsulates configuration entries read from properties file. */ private Server configuration; /** * Default Constructor. */ public LdapServerImpl() { } /** * Helper method used to indentify if the server in memory LDAP server has been initialized, loaded and is listening for messages. * * @return boolean True if started. */ public boolean isStarted() { return this.isStarted.get(); } /** * Returns (or better yet, leaks) the instance of the InMemoryDirectoryServer. Since this class is for development, it's a convenience for testing. * * @return InMemoryDirectoryServer */ public InMemoryDirectoryServer getInMemoryDirectoryServer() { boolean hasLock = false; InMemoryDirectoryServer inMemoryDirectoryServer = null; try { hasLock = serverStateLock.tryLock(LdapServerImpl.LOCK_TIMEOUT, LdapServerImpl.TIME_UNIT); if (hasLock) { inMemoryDirectoryServer = server; } else { throw new IllegalStateException(LdapServerImpl.LOCK_TIMEOUT_MSG); } } catch (InterruptedException ioe) { //lock interrupted LOG.error(ioe.getMessage(), ioe); } finally { if (hasLock) { serverStateLock.unlock(); } } return inMemoryDirectoryServer; } /** * Starts an instance of the in memory LDAP server. * * @throws Exception */ public void start() throws Exception { boolean hasLock = false; try { hasLock = serverStateLock.tryLock(LdapServerImpl.LOCK_TIMEOUT, LdapServerImpl.TIME_UNIT); if (hasLock) { String configFile = LdapServerImpl.CONFIG_FILE; String schemaFile = LdapServerImpl.CONFIG_SCHEMA_FILE; LOG.info(" configFile: {}", configFile); LOG.info(" schemaFile: {}", schemaFile); configuration = ConfigurationLoader.load(configFile, schemaFile); doStart(); this.isStarted.set(Boolean.TRUE); } else { throw new IllegalStateException(LdapServerImpl.LOCK_TIMEOUT_MSG); } } catch (InterruptedException ioe) { //lock interrupted LOG.error(ioe.getMessage(), ioe); } finally { if (hasLock) { serverStateLock.unlock(); } } } private void doStart() throws Exception { if (isStarted.get()) { throw new IllegalStateException(LdapServerImpl.SERVER_ALREADY_STARTED); } LOG.info("Starting up in-Memory Ldap Server."); configureAndStartServer(); } /** * Returns the Listener Configs to register with he InMemory LDAP server. * * @return The server object loaded from XML file by JAXB. * @throws Exception */ public Collection<InMemoryListenerConfig> getInMemoryListenerConfigs() throws Exception { Collection<InMemoryListenerConfig> listenerConfigs = new ArrayList<InMemoryListenerConfig>(); if (configuration.getListeners() != null) { for (Listener listener : configuration.getListeners()) { InetAddress listenAddress = null; //according to javadoc, will listen for all connections on all addresses on all interfaces. if (!StringUtils.isEmpty(listener.getAddress())) { listenAddress = InetAddress.getByName(listener.getAddress()); } int listenPort = Integer.parseInt(listener.getPort()); javax.net.ServerSocketFactory serverSocketFactory = null; //if null uses the JVM default socket factory. javax.net.SocketFactory clientSocketFactory = null; //if null uses the JVM default socket factory. javax.net.ssl.SSLSocketFactory startTLSSocketFactory = null; //StartTLS is not supported on this listener. InMemoryListenerConfig listenerConfig = new InMemoryListenerConfig(listener.getName(), listenAddress, listenPort, serverSocketFactory, clientSocketFactory, startTLSSocketFactory); listenerConfigs.add(listenerConfig); } } return listenerConfigs; } /** * Configures and starts the local LDAP server. This method is invoked by start(). * * @throws Exception */ protected synchronized void configureAndStartServer() throws Exception { LOG.info(">>>LdapServerImpl.configureServer()"); Collection<InMemoryListenerConfig> listenerConfigs = getInMemoryListenerConfigs(); Schema schema = null; if (configuration.getSchema() == null || StringUtils.isEmpty(configuration.getSchema().getName())) { schema = Schema.getDefaultStandardSchema(); } else { final String schemaName = configuration.getSchema().getName(); URL schemaUrl = this.getClass().getClassLoader().getResource(schemaName); File schemaFile = new File(schemaUrl.toURI()); schema = Schema.getSchema(schemaFile); } final String rootObjectDN = configuration.getRoot().getObjectDn(); InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(new DN(rootObjectDN)); //LOG.debug(System.getProperty("java.class.path")); config.setSchema(schema); //schema can be set on the rootDN too, per javadoc. config.setListenerConfigs(listenerConfigs); LOG.info(config.getSchema().toString()); /* Add handlers for debug and acccess log events. These classes can be extended or dependency injected in a future version for more robust behavior. */ config.setLDAPDebugLogHandler(new LDAPDebugLogHandler()); config.setAccessLogHandler(new AccessLogHandler()); config.addAdditionalBindCredentials(configuration.getBindDn(), configuration.getPassword()); server = new InMemoryDirectoryServer(config); try { /* Clear entries from server. */ server.clear(); server.startListening(); loadRootEntry(); loadEntries(); loadLdifFiles(); resetPersonPasswords(); LOG.info(" Total entry count: {}", server.countEntries()); LOG.info("<<<LdapServerImpl.configureServer()"); } catch (LDAPException ldape) { if (ldape.getMessage().contains("java.net.BindException")) { throw new BindException(ldape.getMessage()); } throw ldape; } } /** * Stops the instance of the in-memory LDAP server. */ public void stop() { boolean hasLock = false; try { hasLock = serverStateLock.tryLock(LdapServerImpl.LOCK_TIMEOUT, LdapServerImpl.TIME_UNIT); if (hasLock) { if (!isStarted.get()) { throw new IllegalStateException(LdapServerImpl.SERVER_NOT_STARTED); } LOG.info("Shutting down in-Memory Ldap Server."); server.shutDown(true); } else { throw new IllegalStateException(LdapServerImpl.LOCK_TIMEOUT_MSG); } } catch (InterruptedException ioe) { //lock interrupted LOG.debug(ExceptionUtils.getStackTrace(ioe)); } finally { if (hasLock) { serverStateLock.unlock(); } } } /** * Loads the root entry into the LDAP server, as specified in the configuration file. * * @throws Exception */ protected void loadRootEntry() throws Exception { LOG.info(">>>LdapServerImpl.loadRootEntry()"); SearchResultEntry entry = this.server.getEntry(configuration.getRoot().getObjectDn()); if (entry == null) { LOG.info(" Root entry not found, create it."); final String attributeName = "objectClass"; final Collection<String> attributeValues = new ArrayList<String>(); for (String name : configuration.getRoot().getObjectClasses()) { attributeValues.add(name); } Entry rootEntry = new Entry(new DN(configuration.getRoot().getObjectDn())); rootEntry.addAttribute(attributeName, attributeValues); this.server.add(rootEntry); } entry = this.server.getEntry(configuration.getRoot().getObjectDn()); LOG.info(" Added root entry: {}", ToStringBuilder.reflectionToString(entry, ToStringStyle.MULTI_LINE_STYLE)); } /** * Loads entries (non-attribute entries) as specified in the configuration file. * * @throws Exception */ protected void loadEntries() throws Exception { LOG.info(">>>LdapServerImpl.loadEntries()"); if (configuration.getEntries() != null && configuration.getEntries().size() > 0) { for (org.slc.sli.ldap.inmemory.domain.Entry configEntry : configuration.getEntries()) { LOG.info(" creating entry."); final String attributeName = "objectClass"; final Collection<String> attributeValues = new ArrayList<String>(); for (String name : configEntry.getObjectClasses()) { attributeValues.add(name); } Entry en = new Entry(new DN(configEntry.getObjectDn())); en.addAttribute(attributeName, attributeValues); this.server.add(en); LOG.info(" Added entry: {}", ToStringBuilder.reflectionToString(en, ToStringStyle.MULTI_LINE_STYLE)); } } LOG.info("<<<LdapServerImpl.loadEntries()"); } /** * Loads LDIF files as specified in the configuration file. * * @throws Exception */ protected void loadLdifFiles() throws Exception { LOG.info(">>>loadLdifFiles()"); LOG.debug("{}", System.getProperty("java.class.path")); LOG.info(" ...loading LDIF resources: "); int ldifLoadCount = 0; for (Ldif ldif : configuration.getLdifs()) { LOG.info("----------------------------------------------------------"); LOG.info(" loading LDIF: {}", ldif.getName()); ldifLoadCount++; InputStream resourceAsStream = null; try { LOG.info(" file: '{}'", ldif.getName()); resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(ldif.getName()); if (resourceAsStream == null) { throw new FileNotFoundException("Should be able to load: " + ldif.getName()); } LDIFReader r = new LDIFReader(resourceAsStream); LDIFChangeRecord readEntry = null; int entryCount = 0; while ((readEntry = r.readChangeRecord()) != null) { LOG.debug(" ...readEntry"); LOG.debug("{}", ToStringBuilder.reflectionToString(readEntry, ToStringStyle.MULTI_LINE_STYLE)); entryCount++; readEntry.processChange(server); } LOG.info(" # of entries loaded: {}", entryCount); } finally { if (resourceAsStream != null) { resourceAsStream.close(); } } } LOG.info("----------------------------------------------------------"); LOG.info(" # of LDIF files loaded: {}", ldifLoadCount); LOG.info(" Post LDIF load server entry count: {}", server.countEntries()); LOG.info("<<<loadLdifFiles()"); } /** * Resets all passwords for all entries in the LDAP server, to be plain text, using the * scheme "username" + "1234". This is currently not configurable behavior * because the UnboundedId SDK only provides support for PlainText passwords (via its * SASL implementatioN). In the future, a specific implementation could be added * to implement GSSE, etc. */ public void resetPersonPasswords() { LOG.info("<<<LdapServerImpl.changePersonPasswords()"); LDAPConnection connection = null; Filter filter = Filter.createPresenceFilter("userPassword"); SearchRequest searchRequest = new SearchRequest("dc=slidev,dc=org", SearchScope.SUB, filter); SearchResult searchResult; try { connection = LdapServer.getInstance().getInMemoryDirectoryServer().getConnection(); searchResult = connection.search(searchRequest); int resultCount = searchResult.getEntryCount(); for (SearchResultEntry entry : searchResult.getSearchEntries()) { LOG.debug(" entry: " + ToStringBuilder.reflectionToString(entry, ToStringStyle.DEFAULT_STYLE)); // Attribute(name=userPassword, values={'{MD5}LUOaIWq99K/a23tT6zJWDg=='}) final String dn = entry.getDN(); //ou=people,... final String uid = entry.getAttributeValue("uid"); String newPwd = passwordStrategy.generatePassword(uid); LOG.debug(" uid: " + uid); LOG.debug(" dn:" + dn); int i = StringUtils.indexOf(dn, "ou=people,", 0); final String suffix = StringUtils.substring(dn, i); final String user = "uid=" + uid + "," + suffix; LOG.debug(" " + user); PasswordModifyExtendedRequest passwordModifyRequest = new PasswordModifyExtendedRequest(dn, // The user to update entry.getAttributeValue("userPassword"), // The current password for the user. newPwd); // The new password. null = server will generate PasswordModifyExtendedResult passwordModifyResult; try { passwordModifyResult = (PasswordModifyExtendedResult) connection.processExtendedOperation(passwordModifyRequest); ResultCode resultCode = passwordModifyResult.getResultCode(); if (passwordModifyResult != null && resultCode != ResultCode.SUCCESS) { LOG.debug(" " + resultCode); LOG.debug(" " + passwordModifyResult.getDiagnosticMessage()); } // This doesn't necessarily mean that the operation was successful, since // some kinds of extended operations return non-success results under // normal conditions. } catch (LDAPException le) { LOG.error(le.getMessage(), le); // For an extended operation, this generally means that a problem was // encountered while trying to send the request or read the result. passwordModifyResult = new PasswordModifyExtendedResult(new ExtendedResult(le.toLDAPResult())); if (passwordModifyResult != null) { ResultCode resultCode = passwordModifyResult.getResultCode(); LOG.error(" " + resultCode); LOG.error(" " + passwordModifyResult.getDiagnosticMessage()); } } } LOG.info(" changePersonPasswords searchResult entryCount: " + resultCount); } catch (LDAPSearchException lse) { // The search failed for some reason. searchResult = lse.getSearchResult(); ResultCode resultCode = lse.getResultCode(); String errorMessageFromServer = lse.getDiagnosticMessage(); LOG.error(ToStringBuilder.reflectionToString(searchResult, ToStringStyle.SIMPLE_STYLE)); LOG.error(ToStringBuilder.reflectionToString(resultCode, ToStringStyle.SIMPLE_STYLE)); LOG.error(errorMessageFromServer); LOG.error(lse.getMessage(), lse); } catch (Exception e) { LOG.error(e.getMessage(), e); } finally { if (connection != null && connection.isConnected()) { connection.close(); } } LOG.info("<<<LdapServerImpl.changePersonPasswords()"); } }