/* * Copyright 2015 EMC Corporation * All Rights Reserved */ package com.emc.storageos.api.ldap.ldapserver; import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.schema.Schema; import com.unboundid.ldif.LDIFException; import com.unboundid.util.ssl.KeyStoreKeyManager; import com.unboundid.util.ssl.SSLUtil; import com.unboundid.util.ssl.TrustAllTrustManager; import org.apache.commons.io.IOUtils; import org.springframework.util.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.LoggerFactory; import org.slf4j.Logger; import java.io.*; import java.security.GeneralSecurityException; import java.util.*; import com.emc.storageos.api.ldap.exceptions.FileOperationFailedException; import com.emc.storageos.api.ldap.exceptions.DirectoryOrFileNotFoundException; /** * A class that implements the in memory ldap server using the unboundId * ldap sdk for java. It loads the given schema and configuration ldif files * inorder simulate the ldap server in memory. Unless specified in the api request * to start, the default schema and config files (stored as part of the class resources) * loaded into in memory ldap server. It supports both ldap and ldaps * types of connection. Unless specified in the api request to start, * the ldap connection uses default port 389 and ldaps connection uses the * default port 636. */ public class LDAPServer { private final Logger _log = LoggerFactory.getLogger(this.getClass()); // The default base and manager DN (Distinguished Name) and managerDN password. // This can be overwritten by providing the required value in the /ldap-service/start // api request. Make sure these values matches entries or objects in the // schema and config ldif files. This manager DN and password can be used to // bind the ldap or ldaps connection. private static final String DEFAULT_LDAP_BASE_DN = "dc=apitest,dc=com"; private static final String DEFAULT_LDAP_MANAGER_DN = "cn=manager,dc=apitest,dc=com"; private static final String DEFAULT_LDAP_MANAGER_DN_PASSWORD = "secret"; // The default schema and config ldif files given as a class resource. private static final String DEFAULT_LDAP_SCHEMA_EXPORT = "/Ldap_Schema_Export.ldif"; private static final String DEFAULT_LDAP_CONFIG_EXPORT = "/Ldap_Config_Export.ldif"; // While starting the in memory ldap server, all the required files (schema and config ldifs, // keystore file for the ldaps connection) will be read from the given respective source files // and a written into a a dummy temporary files under the build directory. This is make sure // we are not damaging any of source files. The next few constants specifies the default // location for the dummy files. private static final String DEFAULT_LDIF_FILES_DIRECTORY = "./build/%s/ldifs"; private static final String DEFAULT_LDIF_SCHEMA_FILES_DIRECTORY = DEFAULT_LDIF_FILES_DIRECTORY + "/schema"; private static final String DEFAULT_LDIF_CONFIG_FILES_DIRECTORY = DEFAULT_LDIF_FILES_DIRECTORY + "/config"; private static final String DEFAULT_KEYSTORE_FILE_PATH = "./build/%s/secure/keystore"; private static final String DEFAULT_KEYSTORE_FILE = "/ldapserverkeystore"; private static final String DEFAULT_LDAP_SERVER_PROPERTIES = "/ldapserver.properties"; // The default ldap and ldaps listening port. private static final int DEFAULT_LDAP_LISTEN_PORT = 389; private static final int DEFAULT_LDAPS_LISTEN_PORT = 636; private InMemoryDirectoryServerConfig _inMemoryDSConfig; private InMemoryDirectoryServer _inMemoryDS; private boolean _isRunning = false; private String _listenerName; private String _baseDN; private String _managerDN; private String _managerDNPassword; private List<String> _schemaLdifList; private List<String> _configLdifList; private int _ldapsListenPort; private int _ldapListenPort; /** * Returns the listener name. * * @return _listenerName. */ public String getListenerName() { return _listenerName; } /** * Sets the listener name. * * @param _listenerName a unique listener name. */ public void setListenerName(String _listenerName) { this._listenerName = _listenerName; } /** * Returns the base Distinguished Name. * * @return _baseDN. */ public String getBaseDN() { return _baseDN; } /** * Sets the base Distinguished Name. * * @param _baseDN base DN of the ldap DIT. */ public void setBaseDN(String _baseDN) { this._baseDN = _baseDN; } /** Returns the manager Distinguished Name. * * @return _managerDN. */ public String getManagerDN() { return _managerDN; } /** * Sets the manager Distinguished Name. * * @param _managerDN a DN used to used bind the ldap connections. */ public void setManagerDN(String _managerDN) { this._managerDN = _managerDN; } /** * Returns the password of the manager DN. * * @return _managerDNPassword. */ public String getManagerDNPassword() { return _managerDNPassword; } /** * Sets the password of manager DN. * * @param _managerDNPassword a manager DN password. */ public void setManagerDNPassword(String _managerDNPassword) { this._managerDNPassword = _managerDNPassword; } /** * Set the list of schema ldif files. A new copy is generated * from the source list. * * @param _schemaLdifList list of source schema ldif files path. */ public void setSchemaLdifList(List<String> _schemaLdifList) { if (CollectionUtils.isEmpty(_schemaLdifList)) { _log.debug("There are not schema ldifs given."); return; } if (this._schemaLdifList == null) { this._schemaLdifList = new ArrayList<String>(); } else { this._schemaLdifList.clear(); } this._schemaLdifList.addAll(_schemaLdifList); } /** * Adds a single schema ldif file to the exising list of schema * ldif files. * * @param schemaLdif a schema ldif file path. */ public void addSchemaLdif(String schemaLdif) { if (this._schemaLdifList == null) { this._schemaLdifList = new ArrayList<String>(); } _schemaLdifList.add(schemaLdif); } /** * Set the list of config ldif files. A new copy is generated * from the source list. * * @param _configLdifList list of source config ldif files path. */ public void setConfigLdifList(List<String> _configLdifList) { if (CollectionUtils.isEmpty(_configLdifList)) { _log.debug("There are not config ldifs given."); return; } if (this._configLdifList == null) { this._configLdifList = new ArrayList<String>(); } else { this._configLdifList.clear(); } this._configLdifList.addAll(_configLdifList); } /** * Adds a single config ldif file to the exising list of schema * ldif files. * * @param configLdif a config ldif file path. */ public void addConfigLdif(String configLdif) { if (this._configLdifList == null) { this._configLdifList = new ArrayList<String>(); } _configLdifList.add(configLdif); } /** * Returns the ldaps listen port. * * @return _ldapsListenPort */ public int getLdapsListenPort() { return _ldapsListenPort; } /** * Sets the ldaps listen port. * * @param _ldapsListenPort a ldaps listener port. */ public void setLdapsListenPort(int _ldapsListenPort) { this._ldapsListenPort = _ldapsListenPort; } /** * Returns the ldap listen port. * * @return _ldapListenPort */ public int getListenPort() { return _ldapListenPort; } /** * Sets the ldap listen port. * * @param _listenPort a ldap listener port. */ public void setListenPort(int _listenPort) { this._ldapListenPort = _listenPort; } /** * Stars the in memory ldap server by reading all the schema and config * ldif files. Once all the configurations are loaded to the in memory * ldap server, it starts listening for both ldap and ldaps connections * from clients. * * @return true if the in memory ldap server is started and successfully * listening for the client connections, false otherwise. * * @throws LDIFException * @throws LDAPException * @throws IOException * @throws FileOperationFailedException * @throws GeneralSecurityException * @throws DirectoryOrFileNotFoundException */ public boolean start() throws LDIFException, LDAPException, IOException, FileOperationFailedException, GeneralSecurityException, DirectoryOrFileNotFoundException { if (_isRunning) { _log.info("LDAP Service is already running."); return false; } _log.info("Starting LDAP Service."); addLDAPBindCredentials(); _log.info("Importing Schema Ldifs"); importLDAPSchemaLdifs(); List<InMemoryListenerConfig> listenerConfigs = getInMemoryListenerConfigs(); _inMemoryDSConfig.setListenerConfigs(listenerConfigs); _inMemoryDS = new InMemoryDirectoryServer(_inMemoryDSConfig); _log.info("Importing Config Ldifs"); importLDAPConfigLdifs(); _log.info("Star listening..."); _inMemoryDS.startListening(); _isRunning = true; return _isRunning; } /** * Stops the in memory ldap server. * * @return true if the in memory ldap server is stoppedsuccessfully * false otherwise. */ public boolean stop() { boolean isLDAPServerStopped = true; if (_isRunning) { _inMemoryDS.shutDown(true); _isRunning = false; _log.info("Stopping LDAP Service"); } else { _log.debug("LDAP Service is not running"); isLDAPServerStopped = false; } return isLDAPServerStopped; } /** * Returns the status of the in memory ldap server. * * @return true if it running, false otherwise. */ public boolean isRunning() { _log.debug("LDAP Service status : {}", _isRunning); return _isRunning; } /** * Adds the Base Distinguished Name and credentials of the * user to be used for binding the in memory ldap server. * * @throws LDAPException */ private void addLDAPBindCredentials() throws LDAPException { String baseDN = _baseDN; if(StringUtils.isEmpty(baseDN)) { baseDN = DEFAULT_LDAP_BASE_DN; } _log.debug("BaseDN {}", baseDN); _inMemoryDSConfig = new InMemoryDirectoryServerConfig(baseDN); String managerDN = _managerDN; if(StringUtils.isEmpty(managerDN)) { managerDN = DEFAULT_LDAP_MANAGER_DN; } String managerDNPassword = _managerDNPassword; if(StringUtils.isEmpty(managerDNPassword)) { managerDNPassword = DEFAULT_LDAP_MANAGER_DN_PASSWORD; } _log.debug("ManagerDN {} and ManagerDN password {}", managerDN, managerDNPassword); _inMemoryDSConfig.addAdditionalBindCredentials(managerDN, managerDNPassword); } /** * Creates the dummy schema ldif files under ./build directory. * These files are created based on the given schema_ldifs of * /ldap-service/start api payload. If there are no schema_ldfis * specified in the or the /ldap-service/start api payload, the * default file will be used. * * @throws FileOperationFailedException * @throws DirectoryOrFileNotFoundException * @throws IOException */ private void createLDAPSchemaFiles() throws FileOperationFailedException, DirectoryOrFileNotFoundException, IOException { // Creates a dummy ldif files directory under ./build. createLdifFilesDirectory(); if (CollectionUtils.isEmpty(_schemaLdifList)) { _log.info("Using default schema ldif files"); InputStream schemaExportStream = LDAPServer.class.getResourceAsStream(DEFAULT_LDAP_SCHEMA_EXPORT); BufferedReader schemaExportReader = new BufferedReader(new InputStreamReader(schemaExportStream)); String ldapSchemaExportFileName = "%s" + DEFAULT_LDAP_SCHEMA_EXPORT; ldapSchemaExportFileName = String.format(ldapSchemaExportFileName, getSchemaFilesDirectory()); _log.debug("Schema export file name {}", ldapSchemaExportFileName); createLdifFile(schemaExportReader, ldapSchemaExportFileName); } else { _log.info("Using configured schema ldif files"); for (String file : _schemaLdifList) { File fileObject = new File(file); if (!fileObject.exists()) { throw new DirectoryOrFileNotFoundException("File", file); } InputStream schemaExportStream = new FileInputStream(file); BufferedReader schemaExportReader = new BufferedReader(new InputStreamReader(schemaExportStream)); String ldapSchemaExportFileName = "%s/" + fileObject.getName(); ldapSchemaExportFileName = String.format(ldapSchemaExportFileName, getSchemaFilesDirectory()); _log.debug("Schema export file name {}", ldapSchemaExportFileName); createLdifFile(schemaExportReader, ldapSchemaExportFileName); } } } /** * Creates the dummy config ldif files under ./build directory. * These files are created based on the given config_ldifs of * /ldap-service/start api payload. If there are no config_ldfis * specified in the or the /ldap-service/start api payload, the * default file will be used. * * @throws FileOperationFailedException * @throws DirectoryOrFileNotFoundException * @throws IOException */ private void createLDAPConfigFiles() throws FileOperationFailedException, DirectoryOrFileNotFoundException, IOException { // Creates a dummy ldif files directory under ./build. createLdifFilesDirectory(); if (CollectionUtils.isEmpty(_configLdifList)) { _log.info("Using default config ldif files"); InputStream configExportStream = LDAPServer.class.getResourceAsStream(DEFAULT_LDAP_CONFIG_EXPORT); BufferedReader configExportReader = new BufferedReader(new InputStreamReader(configExportStream)); String ldapConfigExportFileName = "%s" + DEFAULT_LDAP_CONFIG_EXPORT; ldapConfigExportFileName = String.format(ldapConfigExportFileName, getConfigFilesDirectory()); _log.debug("Config export file name {}", ldapConfigExportFileName); createLdifFile(configExportReader, ldapConfigExportFileName); } else { _log.info("Using configured config ldif files"); for (String file : _configLdifList) { File fileObject = new File(file); if (!fileObject.exists()) { throw new DirectoryOrFileNotFoundException("File", file); } InputStream configExportStream = new FileInputStream(file); BufferedReader configExportReader = new BufferedReader(new InputStreamReader(configExportStream)); String ldapConfigExportFileName = "%s/" + fileObject.getName(); ldapConfigExportFileName = String.format(ldapConfigExportFileName, getConfigFilesDirectory()); _log.debug("Config export file name {}", ldapConfigExportFileName); createLdifFile(configExportReader, ldapConfigExportFileName); } } } /** * Creates a dummy ldif file under ./build directory from the * source file. * * @param schemaExportReader a source ldif stream reader. * @param ldapSchemaFileName a an Absolute path of destination ldif file. * * @throws IOException * @throws FileOperationFailedException */ private void createLdifFile(BufferedReader schemaExportReader, String ldapSchemaFileName) throws IOException, FileOperationFailedException { _log.info("Ldif file {}", ldapSchemaFileName); File ldapSchemaExportFile = new File(ldapSchemaFileName); if (ldapSchemaExportFile.exists()) { if (!ldapSchemaExportFile.delete()) { throw new FileOperationFailedException("delete", "file", ldapSchemaFileName); } } if (!ldapSchemaExportFile.createNewFile()) { throw new FileOperationFailedException("create", "file", ldapSchemaFileName); } if (!ldapSchemaExportFile.setWritable(true)) { throw new FileOperationFailedException("set writable", "file", ldapSchemaFileName); } ldapSchemaExportFile.deleteOnExit(); BufferedWriter writer = new BufferedWriter(new FileWriter(ldapSchemaExportFile)); String line; while ((line = schemaExportReader.readLine()) != null) { writer.write(line); writer.newLine(); } writer.close(); } /** * Creates the dummy directory for the ldif files. * * @throws FileOperationFailedException */ private void createLdifFilesDirectory() throws FileOperationFailedException { String schemaExportDirName = getSchemaFilesDirectory(); _log.info("Schema ldif files directory {}", schemaExportDirName); File schemaExportDir = new File(schemaExportDirName); if (!schemaExportDir.exists()) { if (!schemaExportDir.mkdirs()) { throw new FileOperationFailedException("create", "directory", schemaExportDirName); } } schemaExportDir.deleteOnExit(); String configExportDirName = getConfigFilesDirectory(); _log.info("Config ldif files directory {}", configExportDirName); File configExportDir = new File(configExportDirName); if (!configExportDir.exists()) { if (!configExportDir.mkdirs()) { throw new FileOperationFailedException("create", "directory", configExportDirName); } } configExportDir.deleteOnExit(); } /** * Creates they dummy keystore file from the source keystore file * for ldaps listener configuration. * * @return returns the absolute path of the dummy keystore file. * * @throws IOException * @throws FileOperationFailedException */ private String createKeystoreFile() throws IOException, FileOperationFailedException { InputStream keystoreFileStream = LDAPServer.class.getResourceAsStream(DEFAULT_KEYSTORE_FILE); byte [] keystoreData = IOUtils.toByteArray(keystoreFileStream); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write(keystoreData); String keyStoreDirName = getDefaultKeyStoreDirPath(); File keystoreDir = new File(keyStoreDirName); if (!keystoreDir.exists()) { if (!keystoreDir.mkdirs()) { throw new FileOperationFailedException("create", "directory", keyStoreDirName); } } keystoreDir.deleteOnExit(); _log.debug("Keystore file {}", keyStoreDirName + DEFAULT_KEYSTORE_FILE); File keystoreFile = new File(keyStoreDirName + DEFAULT_KEYSTORE_FILE); if (keystoreFile.exists()) { if (!keystoreFile.delete()) { throw new FileOperationFailedException("delete", "file", keystoreFile.getAbsolutePath()); } } if (!keystoreFile.createNewFile()) { throw new FileOperationFailedException("create", "file", keystoreFile.getAbsolutePath()); } keystoreFile.setWritable(true); keystoreFile.deleteOnExit(); FileOutputStream keyStoreFileOutStream = new FileOutputStream(keystoreFile); outputStream.writeTo(keyStoreFileOutStream); outputStream.close(); keyStoreFileOutStream.close(); return keystoreFile.getAbsolutePath(); } /** * Returns the relative path of the dummy config ldif files directory. * * @return relative path of the dummy config ldif files directory. */ private String getConfigFilesDirectory() { return String.format(DEFAULT_LDIF_CONFIG_FILES_DIRECTORY, _listenerName); } /** * Returns the relative path of the dummy schema ldif files directory. * * @return relative path of the dummy schema ldif files directory. */ private String getSchemaFilesDirectory() { return String.format(DEFAULT_LDIF_SCHEMA_FILES_DIRECTORY, _listenerName); } /** * Returns the relative path of the dummy keystore files directory. * * @return relative path of the dummy keystore files directory. */ private String getDefaultKeyStoreDirPath() { return String.format(DEFAULT_KEYSTORE_FILE_PATH, _listenerName); } /** * Returns the relative path of the dummy ldif files directory. * * @return relative path of the dummy ldif files directory. */ private String getDefaultLdifDirPath() { return String.format(DEFAULT_LDIF_FILES_DIRECTORY, _listenerName); } private List<InMemoryListenerConfig> getInMemoryListenerConfigs() throws LDAPException, IOException, GeneralSecurityException, FileOperationFailedException { // Creates the ldap configuration of the in memory ldap server. int ldapPort = this._ldapListenPort != 0 ? this._ldapListenPort : DEFAULT_LDAP_LISTEN_PORT; InMemoryListenerConfig ldapListenerConfig = InMemoryListenerConfig.createLDAPConfig(_listenerName, ldapPort); // Creates the ldaps configuration of the in memory ldap server. int ldapsPort = this._ldapsListenPort != 0 ? this._ldapsListenPort : DEFAULT_LDAPS_LISTEN_PORT; _log.debug("Ldap port {} and Ldaps port {}", ldapPort, ldapsPort); InputStream propFile = LDAPServer.class.getResourceAsStream(DEFAULT_LDAP_SERVER_PROPERTIES); Properties prop = new Properties(); prop.load(propFile); String keyStorePassword = prop.getProperty("keyStorePassword"); String keyStoreAlias = prop.getProperty("keyStoreAlias"); String keyStoreType = prop.getProperty("keyStoreType"); final SSLUtil serverSSLUtil = new SSLUtil(new KeyStoreKeyManager(createKeystoreFile(), keyStorePassword.toCharArray(), keyStoreType, keyStoreAlias), null); final SSLUtil clientSSLUtil = new SSLUtil(new TrustAllTrustManager()); String secureListenerName = "Secure_" + _listenerName; InMemoryListenerConfig ldapsListenerConfig = InMemoryListenerConfig.createLDAPSConfig(secureListenerName, null, ldapsPort, serverSSLUtil.createSSLServerSocketFactory(), clientSSLUtil.createSSLSocketFactory()); _log.info("Listener config {} and secure listener config {}", ldapListenerConfig.getListenerName(), ldapsListenerConfig.getListenerName()); // Adds both ldap and ldaps configuration to the list of listener configs of the // in memory ldap server. List<InMemoryListenerConfig> listenerConfigs = new ArrayList<InMemoryListenerConfig>(); listenerConfigs.add(ldapListenerConfig); listenerConfigs.add(ldapsListenerConfig); return listenerConfigs; } /** * Imports the schema ldif to the in memory ldap server. * * @throws FileOperationFailedException * @throws IOException * @throws LDIFException * @throws DirectoryOrFileNotFoundException */ private void importLDAPSchemaLdifs() throws FileOperationFailedException, IOException, LDIFException, DirectoryOrFileNotFoundException { createLDAPSchemaFiles(); File schemaFileDirectory = new File(getSchemaFilesDirectory()); if (!schemaFileDirectory.exists() || CollectionUtils.isEmpty(Arrays.asList(schemaFileDirectory.listFiles()))) { throw new DirectoryOrFileNotFoundException("Directory", getSchemaFilesDirectory()); } _inMemoryDSConfig.setSchema(Schema.getSchema(schemaFileDirectory.listFiles())); } /** * Imports the config ldif to the in memory ldap server. * * @throws FileOperationFailedException * @throws IOException * @throws LDIFException * @throws LDAPException * @throws DirectoryOrFileNotFoundException */ private void importLDAPConfigLdifs() throws FileOperationFailedException, IOException, LDIFException, LDAPException, DirectoryOrFileNotFoundException { createLDAPConfigFiles(); File configFileDirectory = new File(getConfigFilesDirectory()); if (!configFileDirectory.exists() || CollectionUtils.isEmpty(Arrays.asList(configFileDirectory.listFiles()))) { throw new DirectoryOrFileNotFoundException("Directory", getConfigFilesDirectory()); } for(File configFile : configFileDirectory.listFiles()) { _inMemoryDS.importFromLDIF(true, configFile.getPath()); } } }