blob: 31ac0d8a39d2b2240b2d781e8d0ee5eb76d73bee [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.jackrabbit.oak.security.authentication.ldap;
import static org.junit.Assume.assumeFalse;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.BindException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import org.apache.commons.io.FileUtils;
import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapConfigurationException;
import org.apache.directory.api.ldap.model.exception.LdapException;
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.name.Dn;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.ldap.schema.manager.impl.DefaultSchemaManager;
import org.apache.directory.server.constants.ServerDNConstants;
import org.apache.directory.server.constants.SystemSchemaConstants;
import org.apache.directory.server.core.DefaultDirectoryService;
import org.apache.directory.server.core.api.CacheService;
import org.apache.directory.server.core.api.CoreSession;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.InstanceLayout;
import org.apache.directory.server.core.api.schema.SchemaPartition;
import org.apache.directory.server.core.jndi.CoreContextFactory;
import org.apache.directory.server.core.partition.impl.avl.AvlPartition;
import org.apache.directory.server.core.shared.DefaultDnFactory;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.ldap.handlers.extended.StartTlsHandler;
import org.apache.directory.server.ldap.handlers.extended.StoredProcedureExtendedOperationHandler;
import org.apache.directory.server.ldap.handlers.sasl.MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
import org.apache.directory.server.protocol.shared.transport.TcpTransport;
import org.apache.mina.util.AvailablePortFinder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A simple ldap test server
*/
public abstract class AbstractServer {
public static final String EXAMPLE_DN = "dc=example,dc=com";
private static final Logger LOG = LoggerFactory.getLogger(AbstractServer.class);
private static final List<LdifEntry> EMPTY_LIST = Collections.unmodifiableList(new ArrayList<LdifEntry>(0));
private static final String CTX_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
/**
* the context root for the rootDSE
*/
protected CoreSession rootDSE;
/**
* flag whether to delete database files for each test or not
*/
protected boolean doDelete = true;
protected int port = -1;
protected CacheService cacheService;
protected DirectoryService directoryService;
protected LdapServer ldapServer;
/**
* Loads an LDIF from an input stream and adds the entries it contains to
* the server. It appears as though the administrator added these entries
* to the server.
*
* @param in the input stream containing the LDIF entries to load
* @return a list of entries added to the server in the order they were added
* @throws NamingException of the load fails
*/
protected List<LdifEntry> loadLdif(InputStream in) throws Exception {
if (in == null) {
return EMPTY_LIST;
}
LdifReader ldifReader = new LdifReader(in);
return loadLdif(ldifReader);
}
protected List<LdifEntry> loadLdif(LdifReader ldifReader) throws Exception {
List<LdifEntry> entries = new ArrayList<LdifEntry>();
for (LdifEntry ldifEntry : ldifReader) {
Dn dn = ldifEntry.getDn();
if (ldifEntry.isEntry()) {
org.apache.directory.api.ldap.model.entry.Entry items = ldifEntry.getEntry();
rootDSE.add(new DefaultEntry(directoryService.getSchemaManager(), items));
LOG.info("Added entry {}", dn);
entries.add(ldifEntry);
}
}
return entries;
}
/**
* Inject an ldif String into the server. DN must be relative to the
* root.
*
* @param ldif the entries to inject
* @throws NamingException if the entries cannot be added
*/
protected void addEntry(String ldif) throws Exception {
ByteArrayInputStream in = new ByteArrayInputStream(ldif.getBytes("utf-8"));
LdifReader reader = new LdifReader(in);
loadLdif(reader);
}
/**
* Common code to get an initial context via a simple bind to the
* server over the wire using the SUN JNDI LDAP provider. Do not use
* this method until after the setUp() method is called to start the
* server otherwise it will fail.
*
* @return an LDAP context as the the administrator to the rootDSE
* @throws NamingException if the server cannot be contacted
*/
protected LdapContext getWiredContext() throws Exception {
return getWiredContext(ServerDNConstants.ADMIN_SYSTEM_DN, "secret");
}
/**
* Common code to get an initial context via a simple bind to the
* server over the wire using the SUN JNDI LDAP provider. Do not use
* this method until after the setUp() method is called to start the
* server otherwise it will fail.
*
* @param bindPrincipalDn the DN of the principal to bind as
* @param password the password of the bind principal
* @return an LDAP context as the the administrator to the rootDSE
* @throws NamingException if the server cannot be contacted
*/
protected LdapContext getWiredContext(String bindPrincipalDn, String password) throws Exception {
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, CTX_FACTORY);
env.put(Context.PROVIDER_URL, "ldap://localhost:" + port);
env.put(Context.SECURITY_PRINCIPAL, bindPrincipalDn);
env.put(Context.SECURITY_CREDENTIALS, password);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
return new InitialLdapContext(env, null);
}
/**
* Get's the initial context factory for the provider's ou=system context
* root.
*/
protected void setUp() throws Exception {
File cwd = new File("target", "apacheds");
doDelete(cwd);
// setup directory service
directoryService = new DefaultDirectoryService();
directoryService.setShutdownHookEnabled(false);
directoryService.setInstanceLayout(new InstanceLayout(cwd));
cacheService = new CacheService();
cacheService.initialize(directoryService.getInstanceLayout());
SchemaManager schemaManager = new DefaultSchemaManager();
directoryService.setSchemaManager(schemaManager);
directoryService.setDnFactory(new DefaultDnFactory(directoryService.getSchemaManager(), cacheService.getCache("dnCache")));
AvlPartition schLdifPart = new AvlPartition(directoryService.getSchemaManager(), directoryService.getDnFactory());
schLdifPart.setId("schema");
schLdifPart.setSuffixDn(directoryService.getDnFactory().create(ServerDNConstants.CN_SCHEMA_DN));
SchemaPartition schPart = new SchemaPartition(directoryService.getSchemaManager());
schPart.setWrappedPartition(schLdifPart);
directoryService.setSchemaPartition(schPart);
AvlPartition sysPart = new AvlPartition(directoryService.getSchemaManager(), directoryService.getDnFactory());
sysPart.setId(SystemSchemaConstants.SCHEMA_NAME);
sysPart.setSuffixDn(directoryService.getDnFactory().create(ServerDNConstants.SYSTEM_DN));
directoryService.setSystemPartition(sysPart);
AvlPartition examplePart = new AvlPartition(directoryService.getSchemaManager(), directoryService.getDnFactory());
examplePart.setId("example");
examplePart.setSuffixDn(directoryService.getDnFactory().create(EXAMPLE_DN));
examplePart.setCacheService(cacheService);
directoryService.addPartition(examplePart);
// setup ldap server
port = AvailablePortFinder.getNextAvailable(1024);
ldapServer = new LdapServer();
setupLdapServer();
setupSaslMechanisms();
directoryService.startup();
setupExamplePartition();
startLdapServer();
setContexts(ServerDNConstants.ADMIN_SYSTEM_DN, "secret");
}
/**
* Start the LDAP server assuming we can bind to the previously reserved port.
* Given that there is a small race between when the port was reserved and when the
* socket is actually bound this can still fail. For now we are ignoring this rare
* case and skip the test. See OAK-5542.
* TODO: OAK-5832: Make the LDAP server used in testing resilient against ports already in use
* @throws Exception
*/
private void startLdapServer() throws Exception {
try {
ldapServer.start();
} catch (LdapConfigurationException e) {
Throwable cause = e.getCause();
assumeFalse("Ignoring this test as the server port is already in use (OAK-5542): " + cause,
cause instanceof BindException);
throw e;
}
}
protected void setupLdapServer() throws Exception {
ldapServer.setTransports(new TcpTransport(port));
ldapServer.setDirectoryService(directoryService);
ldapServer.addExtendedOperationHandler(new StartTlsHandler());
ldapServer.addExtendedOperationHandler(new StoredProcedureExtendedOperationHandler());
}
protected void setupExamplePartition() throws Exception {
// setup acl to allow read all users
// Inject the context entry for dc=example,dc=com partition if it does not already exist
try {
directoryService.getAdminSession().lookup(new Dn(EXAMPLE_DN));
} catch (LdapException lnnfe) {
Entry entry = directoryService.newEntry(new Dn(EXAMPLE_DN));
entry.add("objectClass", "top", "domain", "extensibleObject");
entry.add("dc", "example");
entry.add("administrativeRole", "accessControlSpecificArea");
directoryService.getAdminSession().add(entry);
entry = directoryService.newEntry(new Dn("cn=enableSearchForAllUsers," + EXAMPLE_DN));
entry.add("objectClass", "top", "subentry", "accessControlSubentry");
entry.add("cn", "enableSearchForAllUsers");
entry.add("subtreeSpecification", "{}");
entry.add("prescriptiveACI",
"{ \n" +
" identificationTag \"enableSearchForAllUsers\",\n" +
" precedence 14,\n" +
" authenticationLevel simple,\n" +
" itemOrUserFirst userFirst: \n" +
" { \n" +
" userClasses { allUsers }, \n" +
" userPermissions \n" +
" { \n" +
" {\n" +
" protectedItems {entry, allUserAttributeTypesAndValues}, \n" +
" grantsAndDenials { grantRead, grantReturnDN, grantBrowse } \n" +
" }\n" +
" } \n" +
" } \n" +
"}");
directoryService.getAdminSession().add(entry);
directoryService.sync();
}
}
public void setMaxSizeLimit(long maxSizeLimit) {
ldapServer.setMaxSizeLimit(maxSizeLimit);
}
private void setupSaslMechanisms() {
Map<String, MechanismHandler> mechanismHandlerMap = new HashMap<String, MechanismHandler>();
mechanismHandlerMap.put(SupportedSaslMechanisms.PLAIN, new PlainMechanismHandler());
CramMd5MechanismHandler cramMd5MechanismHandler = new CramMd5MechanismHandler();
mechanismHandlerMap.put(SupportedSaslMechanisms.CRAM_MD5, cramMd5MechanismHandler);
DigestMd5MechanismHandler digestMd5MechanismHandler = new DigestMd5MechanismHandler();
mechanismHandlerMap.put(SupportedSaslMechanisms.DIGEST_MD5, digestMd5MechanismHandler);
GssapiMechanismHandler gssapiMechanismHandler = new GssapiMechanismHandler();
mechanismHandlerMap.put(SupportedSaslMechanisms.GSSAPI, gssapiMechanismHandler);
NtlmMechanismHandler ntlmMechanismHandler = new NtlmMechanismHandler();
// TODO - set some sort of default NtlmProvider implementation here
// ntlmMechanismHandler.setNtlmProvider( provider );
// TODO - or set FQCN of some sort of default NtlmProvider implementation here
// ntlmMechanismHandler.setNtlmProviderFqcn( "com.foo.BarNtlmProvider" );
mechanismHandlerMap.put(SupportedSaslMechanisms.NTLM, ntlmMechanismHandler);
mechanismHandlerMap.put(SupportedSaslMechanisms.GSS_SPNEGO, ntlmMechanismHandler);
ldapServer.setSaslMechanismHandlers(mechanismHandlerMap);
}
/**
* Deletes the Eve working directory.
*
* @param wkdir the directory to delete
* @throws IOException if the directory cannot be deleted
*/
protected void doDelete(File wkdir) throws IOException {
if (doDelete) {
if (wkdir.exists()) {
FileUtils.deleteDirectory(wkdir);
}
if (wkdir.exists()) {
throw new IOException("Failed to delete: " + wkdir);
}
}
}
/**
* Sets the contexts for this base class. Values of user and password used to
* set the respective JNDI properties. These values can be overriden by the
* overrides properties.
*
* @param user the username for authenticating as this user
* @param passwd the password of the user
* @throws NamingException if there is a failure of any kind
*/
protected void setContexts(String user, String passwd) throws Exception {
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(DirectoryService.JNDI_KEY, directoryService);
env.put(Context.SECURITY_PRINCIPAL, user);
env.put(Context.SECURITY_CREDENTIALS, passwd);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.INITIAL_CONTEXT_FACTORY, CoreContextFactory.class.getName());
setContexts(env);
}
/**
* Sets the contexts of this class taking into account the extras and overrides
* properties.
*
* @param env an environment to use while setting up the system root.
* @throws NamingException if there is a failure of any kind
*/
protected void setContexts(Hashtable<String, Object> env) throws Exception {
Hashtable<String, Object> envFinal = new Hashtable<String, Object>(env);
envFinal.put(Context.PROVIDER_URL, "");
rootDSE = directoryService.getAdminSession();
}
/**
* Sets the system context root to null.
*/
protected void tearDown() throws Exception {
if (ldapServer != null) {
ldapServer.stop();
}
try {
directoryService.shutdown();
} catch (Exception e) {
// ignore
}
if (cacheService != null) {
cacheService.destroy();
}
}
}