/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * 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 org.keycloak.util.ldap; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.text.StrSubstitutor; import org.apache.directory.api.ldap.model.entry.DefaultEntry; import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException; import org.apache.directory.api.ldap.model.ldif.LdifEntry; import org.apache.directory.api.ldap.model.ldif.LdifReader; import org.apache.directory.api.ldap.model.schema.SchemaManager; import org.apache.directory.server.core.api.DirectoryService; import org.apache.directory.server.core.api.partition.Partition; import org.apache.directory.server.core.factory.DirectoryServiceFactory; import org.apache.directory.server.core.factory.PartitionFactory; import org.apache.directory.server.ldap.LdapServer; import org.apache.directory.server.protocol.shared.transport.TcpTransport; import org.apache.directory.server.protocol.shared.transport.Transport; import org.jboss.logging.Logger; import org.keycloak.common.util.FindFile; import org.keycloak.common.util.StreamUtil; import java.io.File; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public class LDAPEmbeddedServer { private static final Logger log = Logger.getLogger(LDAPEmbeddedServer.class); public static final String PROPERTY_BASE_DN = "ldap.baseDN"; public static final String PROPERTY_BIND_HOST = "ldap.host"; public static final String PROPERTY_BIND_PORT = "ldap.port"; public static final String PROPERTY_LDIF_FILE = "ldap.ldif"; public static final String PROPERTY_SASL_PRINCIPAL = "ldap.saslPrincipal"; public static final String PROPERTY_DSF = "ldap.dsf"; private static final String DEFAULT_BASE_DN = "dc=keycloak,dc=org"; private static final String DEFAULT_BIND_HOST = "localhost"; private static final String DEFAULT_BIND_PORT = "10389"; private static final String DEFAULT_LDIF_FILE = "classpath:ldap/default-users.ldif"; private static final String PROPERTY_ENABLE_SSL = "enableSSL"; private static final String PROPERTY_KEYSTORE_FILE = "keystoreFile"; private static final String PROPERTY_CERTIFICATE_PASSWORD = "certificatePassword"; public static final String DSF_INMEMORY = "mem"; public static final String DSF_FILE = "file"; public static final String DEFAULT_DSF = DSF_FILE; protected Properties defaultProperties; protected String baseDN; protected String bindHost; protected int bindPort; protected String ldifFile; protected String ldapSaslPrincipal; protected String directoryServiceFactory; protected boolean enableSSL = false; protected String keystoreFile; protected String certPassword; protected DirectoryService directoryService; protected LdapServer ldapServer; public static void main(String[] args) throws Exception { Properties defaultProperties = new Properties(); defaultProperties.put(PROPERTY_DSF, DSF_FILE); execute(args, defaultProperties); } public static void execute(String[] args, Properties defaultProperties) throws Exception { final LDAPEmbeddedServer ldapEmbeddedServer = new LDAPEmbeddedServer(defaultProperties); ldapEmbeddedServer.init(); ldapEmbeddedServer.start(); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { try { ldapEmbeddedServer.stop(); } catch (Exception e) { e.printStackTrace(); } } }); } public LDAPEmbeddedServer(Properties defaultProperties) { this.defaultProperties = defaultProperties; this.baseDN = readProperty(PROPERTY_BASE_DN, DEFAULT_BASE_DN); this.bindHost = readProperty(PROPERTY_BIND_HOST, DEFAULT_BIND_HOST); String bindPort = readProperty(PROPERTY_BIND_PORT, DEFAULT_BIND_PORT); this.bindPort = Integer.parseInt(bindPort); this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_LDIF_FILE); this.ldapSaslPrincipal = readProperty(PROPERTY_SASL_PRINCIPAL, null); this.directoryServiceFactory = readProperty(PROPERTY_DSF, DEFAULT_DSF); this.enableSSL = Boolean.valueOf(readProperty(PROPERTY_ENABLE_SSL, "false")); this.keystoreFile = readProperty(PROPERTY_KEYSTORE_FILE, null); this.certPassword = readProperty(PROPERTY_CERTIFICATE_PASSWORD, null); } protected String readProperty(String propertyName, String defaultValue) { String value = System.getProperty(propertyName); if (value == null || value.isEmpty()) { value = (String) this.defaultProperties.get(propertyName); } if (value == null || value.isEmpty()) { value = defaultValue; } return value; } public void init() throws Exception { log.info("Creating LDAP Directory Service. Config: baseDN=" + baseDN + ", bindHost=" + bindHost + ", bindPort=" + bindPort + ", ldapSaslPrincipal=" + ldapSaslPrincipal + ", directoryServiceFactory=" + directoryServiceFactory + ", ldif=" + ldifFile); this.directoryService = createDirectoryService(); log.info("Importing LDIF: " + ldifFile); importLdif(); log.info("Creating LDAP Server"); this.ldapServer = createLdapServer(); } public void start() throws Exception { log.info("Starting LDAP Server"); ldapServer.start(); log.info("LDAP Server started"); } protected DirectoryService createDirectoryService() throws Exception { // Parse "keycloak" from "dc=keycloak,dc=org" String dcName = baseDN.split(",")[0]; dcName = dcName.substring(dcName.indexOf("=") + 1); DirectoryServiceFactory dsf; if (this.directoryServiceFactory.equals(DSF_INMEMORY)) { dsf = new InMemoryDirectoryServiceFactory(); } else if (this.directoryServiceFactory.equals(DSF_FILE)) { dsf = new FileDirectoryServiceFactory(); } else { throw new IllegalStateException("Unknown value of directoryServiceFactory: " + this.directoryServiceFactory); } DirectoryService service = dsf.getDirectoryService(); service.setAccessControlEnabled(false); service.setAllowAnonymousAccess(false); service.getChangeLog().setEnabled(false); dsf.init(dcName + "DS"); SchemaManager schemaManager = service.getSchemaManager(); PartitionFactory partitionFactory = dsf.getPartitionFactory(); Partition partition = partitionFactory.createPartition( schemaManager, service.getDnFactory(), dcName, this.baseDN, 1000, new File(service.getInstanceLayout().getPartitionsDirectory(), dcName)); partition.setCacheService( service.getCacheService() ); partition.initialize(); partition.setSchemaManager( schemaManager ); // Inject the partition into the DirectoryService service.addPartition( partition ); // Last, process the context entry String entryLdif = "dn: " + baseDN + "\n" + "dc: " + dcName + "\n" + "objectClass: top\n" + "objectClass: domain\n\n"; importLdifContent(service, entryLdif); return service; } protected LdapServer createLdapServer() { LdapServer ldapServer = new LdapServer(); ldapServer.setServiceName("DefaultLdapServer"); ldapServer.setSearchBaseDn(this.baseDN); // Read the transports Transport ldaps = new TcpTransport(this.bindHost, this.bindPort, 3, 50); if (enableSSL) { ldaps.setEnableSSL(true); ldapServer.setKeystoreFile(keystoreFile); ldapServer.setCertificatePassword(certPassword); Transport ldap = new TcpTransport(this.bindHost, 10389, 3, 50); ldapServer.addTransports( ldap ); } ldapServer.addTransports( ldaps ); // Associate the DS to this LdapServer ldapServer.setDirectoryService( directoryService ); // Propagate the anonymous flag to the DS directoryService.setAllowAnonymousAccess(false); return ldapServer; } private void importLdif() throws Exception { Map<String, String> map = new HashMap<String, String>(); map.put("hostname", this.bindHost); if (this.ldapSaslPrincipal != null) { map.put("ldapSaslPrincipal", this.ldapSaslPrincipal); } // Find LDIF file on filesystem or classpath ( if it's like classpath:ldap/users.ldif ) InputStream is = FindFile.findFile(ldifFile); if (is == null) { throw new IllegalStateException("LDIF file not found on classpath or on file system. Location was: " + ldifFile); } final String ldifContent = StrSubstitutor.replace(StreamUtil.readString(is), map); log.info("Content of LDIF: " + ldifContent); final SchemaManager schemaManager = directoryService.getSchemaManager(); importLdifContent(directoryService, ldifContent); } private static void importLdifContent(DirectoryService directoryService, String ldifContent) throws Exception { LdifReader ldifReader = new LdifReader(IOUtils.toInputStream(ldifContent)); try { for (LdifEntry ldifEntry : ldifReader) { try { directoryService.getAdminSession().add(new DefaultEntry(directoryService.getSchemaManager(), ldifEntry.getEntry())); } catch (LdapEntryAlreadyExistsException ignore) { log.info("Entry " + ldifEntry.getDn() + " already exists. Ignoring"); } } } finally { ldifReader.close(); } } public void stop() throws Exception { stopLdapServer(); shutdownDirectoryService(); } protected void stopLdapServer() { log.info("Stopping LDAP server."); ldapServer.stop(); } protected void shutdownDirectoryService() throws Exception { log.info("Stopping Directory service."); directoryService.shutdown(); // Delete workfiles just for 'inmemory' implementation used in tests. Normally we want LDAP data to persist File instanceDir = directoryService.getInstanceLayout().getInstanceDirectory(); if (this.directoryServiceFactory.equals(DSF_INMEMORY)) { log.infof("Removing Directory service workfiles: %s", instanceDir.getAbsolutePath()); FileUtils.deleteDirectory(instanceDir); } else { log.info("Working LDAP directory not deleted. Delete it manually if you want to start with fresh LDAP data. Directory location: " + instanceDir.getAbsolutePath()); } } }