/*
* This file is part of ReadonlyREST.
*
* ReadonlyREST is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ReadonlyREST is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ReadonlyREST. If not, see http://www.gnu.org/licenses/
*/
package org.elasticsearch.plugin.readonlyrest.utils.containers;
import com.google.common.collect.Lists;
import com.unboundid.ldap.sdk.AddRequest;
import com.unboundid.ldap.sdk.BindResult;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldif.LDIFReader;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.plugin.readonlyrest.acl.definitions.ldaps.unboundid.SearchingUserConfig;
import org.elasticsearch.plugin.readonlyrest.utils.containers.exceptions.ContainerCreationException;
import org.elasticsearch.plugin.readonlyrest.utils.esdependent.MockedESContext;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.WaitStrategy;
import org.testcontainers.images.builder.ImageFromDockerfile;
import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import static org.elasticsearch.plugin.readonlyrest.utils.containers.ContainerUtils.checkTimeout;
public class LdapContainer extends GenericContainer<LdapContainer> {
private static Logger logger = MockedESContext.INSTANCE.logger(LdapContainer.class);
private static int LDAP_PORT = 389;
private static Duration LDAP_CONNECT_TIMEOUT = Duration.ofSeconds(5);
private static Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(240);
private static String LDAP_DOMAIN = "example.com";
private static String LDAP_ORGANISATION = "Example";
private static String LDAP_ADMIN = "admin";
private static String LDAP_ADMIN_PASSWORD = "password";
private static Duration WAIT_BETWEEN_RETRIES = Duration.ofSeconds(1);
private LdapContainer(ImageFromDockerfile imageFromDockerfile) {
super(imageFromDockerfile);
}
public static LdapContainer create(String ldapInitScript) {
File ldapInitScriptFile = ContainerUtils.getResourceFile(ldapInitScript);
logger.info("Creating LDAP container ...");
LdapContainer container = new LdapContainer(
new ImageFromDockerfile()
.withDockerfileFromBuilder(builder -> builder
.from("osixia/openldap:1.1.7")
.env("LDAP_ORGANISATION", LDAP_ORGANISATION)
.env("LDAP_DOMAIN", LDAP_DOMAIN)
.env("LDAP_ADMIN_PASSWORD", LDAP_ADMIN_PASSWORD)
.build()));
return container
.withExposedPorts(LDAP_PORT)
.waitingFor(
container.ldapWaitStrategy(ldapInitScriptFile)
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)
);
}
public String getLdapHost() {
return this.getContainerIpAddress();
}
public Integer getLdapPort() {
return this.getMappedPort(LDAP_PORT);
}
public SearchingUserConfig getSearchingUserConfig() {
List<String> dnParts = Lists.newArrayList(LDAP_DOMAIN.split("\\."));
if (dnParts.isEmpty()) throw new IllegalArgumentException("Wrong domain defined " + LDAP_DOMAIN);
String dnString = dnParts.stream().map(part -> "dc=" + part).reduce("", (s, s2) -> s + "," + s2);
String bindDN = String.format("cn=%s%s", LDAP_ADMIN, dnString);
return new SearchingUserConfig(bindDN, LDAP_ADMIN_PASSWORD);
}
private WaitStrategy ldapWaitStrategy(File initialDataLdif) {
return new GenericContainer.AbstractWaitStrategy() {
@Override
protected void waitUntilReady() {
logger.info("Waiting for LDAP container ...");
Optional<LDAPConnection> connection = tryConnect(getLdapHost(), getLdapPort());
if (connection.isPresent()) {
try {
initLdap(connection.get(), initialDataLdif);
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
connection.get().close();
}
}
else {
throw new IllegalStateException("Cannot connect");
}
logger.info("LDAP container stated");
}
private LDAPConnection createConnection() {
LDAPConnectionOptions options = new LDAPConnectionOptions();
options.setConnectTimeoutMillis((int) LDAP_CONNECT_TIMEOUT.toMillis());
return new LDAPConnection(options);
}
private Optional<LDAPConnection> tryConnect(String address, Integer port) {
final Instant startTime = Instant.now();
final LDAPConnection connection = createConnection();
do {
try {
connection.connect(address, port);
Thread.sleep(WAIT_BETWEEN_RETRIES.toMillis());
} catch (Exception ignored) {
}
} while (!connection.isConnected() && !checkTimeout(startTime, startupTimeout));
return Optional.of(connection);
}
private void initLdap(LDAPConnection connection, File initialDataLdif) throws Exception {
LDAPConnection bindedConnection = bind(connection);
LDIFReader r = new LDIFReader(initialDataLdif.getAbsoluteFile());
Entry readEntry;
while ((readEntry = r.readEntry()) != null) {
bindedConnection.add(new AddRequest(readEntry.toLDIF()));
}
}
private LDAPConnection bind(LDAPConnection connection) throws Exception {
SearchingUserConfig bindDNAndPassword = getSearchingUserConfig();
BindResult bindResult = connection.bind(bindDNAndPassword.getDn(), bindDNAndPassword.getPassword());
if (!ResultCode.SUCCESS.equals(bindResult.getResultCode())) {
throw new ContainerCreationException("Cannot init LDAP due to bind problem");
}
return connection;
}
};
}
}