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()");
}
}