blob: 9d205f0f5f1bf8d0999e5c252c8ee8da4f3fbb64 [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.solr.security.hadoop;
import java.io.File;
import java.lang.invoke.MethodHandles;
import java.net.BindException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.apache.commons.io.file.PathUtils;
import org.apache.hadoop.minikdc.MiniKdc;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.solr.client.solrj.impl.Krb5HttpClientBuilder;
import org.apache.solr.client.solrj.util.Constants;
import org.apache.solr.common.util.EnvUtils;
import org.junit.Assume;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class KerberosTestServices {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private volatile MiniKdc kdc;
private final JaasConfiguration jaasConfiguration;
private final Configuration savedConfig;
private final boolean debug;
private final File workDir;
public static void assumeMiniKdcWorksWithDefaultLocale() throws Exception {
final String principal = "server";
Path kdcDir = LuceneTestCase.createTempDir().resolve("miniKdc");
File keytabFile = kdcDir.resolve("keytabs").toFile();
Properties conf = MiniKdc.createConf();
conf.setProperty("kdc.port", "0");
MiniKdc kdc = new MiniKdc(conf, kdcDir.toFile());
kdc.start();
kdc.createPrincipal(keytabFile, principal);
AppConfigurationEntry appConfigEntry =
new AppConfigurationEntry(
KerberosTestServices.krb5LoginModuleName,
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
Map.of(
"principal",
principal,
"storeKey",
"true",
"useKeyTab",
"true",
"useTicketCache",
"false",
"refreshKrb5Config",
"true",
"keyTab",
keytabFile.getAbsolutePath(),
"keytab",
keytabFile.getAbsolutePath()));
Configuration configuration =
new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
return new AppConfigurationEntry[] {appConfigEntry};
}
};
try {
new LoginContext("Server", null, null, configuration).login();
} catch (LoginException e) {
Assume.assumeNoException(Locale.getDefault() + " appears to be incompatible with MiniKdc", e);
} finally {
kdc.stop();
}
}
private KerberosTestServices(
File workDir, JaasConfiguration jaasConfiguration, Configuration savedConfig, boolean debug) {
this.jaasConfiguration = jaasConfiguration;
this.savedConfig = savedConfig;
this.workDir = workDir;
this.debug = debug;
}
public MiniKdc getKdc() {
return kdc;
}
public void start() throws Exception {
assumeMiniKdcWorksWithDefaultLocale();
// There is time lag between selecting a port and trying to bind with it. It's possible that
// another service captures the port in between which'll result in BindException.
boolean bindException;
int numTries = 0;
do {
try {
bindException = false;
kdc = getKdc(workDir, debug);
kdc.start();
} catch (BindException e) {
PathUtils.deleteDirectory(workDir.toPath()); // clean directory
numTries++;
if (numTries == 3) {
log.error("Failed setting up MiniKDC. Tried {} times.", numTries);
throw e;
}
log.error("BindException encountered when setting up MiniKdc. Trying again.");
bindException = true;
}
} while (bindException);
Configuration.setConfiguration(jaasConfiguration);
Krb5HttpClientBuilder.regenerateJaasConfiguration();
}
public void stop() {
if (kdc != null) kdc.stop();
Configuration.setConfiguration(savedConfig);
Krb5HttpClientBuilder.regenerateJaasConfiguration();
}
public static Builder builder() {
return new Builder();
}
/**
* Returns a MiniKdc that can be used for creating kerberos principals and keytabs. Caller is
* responsible for starting/stopping the kdc.
*/
private static MiniKdc getKdc(File workDir, boolean debug) throws Exception {
Properties conf = MiniKdc.createConf();
conf.setProperty("kdc.port", "0");
conf.setProperty("debug", String.valueOf(debug));
return new MiniKdc(conf, workDir);
}
/**
* Programmatic version of a jaas.conf file suitable for connecting to a SASL-configured
* zookeeper.
*/
private static class JaasConfiguration extends Configuration {
private static AppConfigurationEntry[] clientEntry;
private static AppConfigurationEntry[] serverEntry;
private String clientAppName = "Client", serverAppName = "Server";
/**
* Add an entry to the jaas configuration with the passed in name, principal, and keytab. The
* other necessary options will be set for you.
*
* @param clientPrincipal The principal of the client
* @param clientKeytab The location of the keytab with the clientPrincipal
* @param serverPrincipal The principal of the server
* @param serverKeytab The location of the keytab with the serverPrincipal
*/
public JaasConfiguration(
String clientPrincipal, File clientKeytab, String serverPrincipal, File serverKeytab) {
Map<String, String> clientOptions = new HashMap<>();
clientOptions.put("principal", clientPrincipal);
clientOptions.put("keyTab", clientKeytab.getAbsolutePath());
clientOptions.put("useKeyTab", "true");
clientOptions.put("storeKey", "true");
clientOptions.put("useTicketCache", "false");
clientOptions.put("refreshKrb5Config", "true");
String jaasProp = EnvUtils.getProperty("solr.jaas.debug");
if (jaasProp != null && "true".equalsIgnoreCase(jaasProp)) {
clientOptions.put("debug", "true");
}
clientEntry =
new AppConfigurationEntry[] {
new AppConfigurationEntry(
krb5LoginModuleName,
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
clientOptions)
};
if (serverPrincipal != null && serverKeytab != null) {
Map<String, String> serverOptions = new HashMap<>(clientOptions);
serverOptions.put("principal", serverPrincipal);
serverOptions.put("keytab", serverKeytab.getAbsolutePath());
serverEntry =
new AppConfigurationEntry[] {
new AppConfigurationEntry(
krb5LoginModuleName,
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
serverOptions)
};
}
}
/**
* Add an entry to the jaas configuration with the passed in principal and keytab, along with
* the app name.
*
* @param principal The principal
* @param keytab The keytab containing credentials for the principal
* @param appName The app name of the configuration
*/
public JaasConfiguration(String principal, File keytab, String appName) {
this(principal, keytab, null, null);
clientAppName = appName;
serverAppName = null;
}
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
if (name.equals(clientAppName)) {
return clientEntry;
} else if (name.equals(serverAppName)) {
return serverEntry;
}
return null;
}
}
public static final String krb5LoginModuleName =
Constants.IS_IBM_JAVA
? "com.ibm.security.auth.module.Krb5LoginModule"
: "com.sun.security.auth.module.Krb5LoginModule";
public static class Builder {
private File kdcWorkDir;
private String clientPrincipal;
private File clientKeytab;
private String serverPrincipal;
private File serverKeytab;
private String appName;
private boolean debug = false;
public Builder() {}
public Builder withKdc(File kdcWorkDir) {
this.kdcWorkDir = kdcWorkDir;
return this;
}
public Builder withDebug() {
this.debug = true;
return this;
}
public Builder withJaasConfiguration(
String clientPrincipal, File clientKeytab, String serverPrincipal, File serverKeytab) {
this.clientPrincipal = Objects.requireNonNull(clientPrincipal);
this.clientKeytab = Objects.requireNonNull(clientKeytab);
this.serverPrincipal = serverPrincipal;
this.serverKeytab = serverKeytab;
this.appName = null;
return this;
}
public Builder withJaasConfiguration(String principal, File keytab, String appName) {
this.clientPrincipal = Objects.requireNonNull(principal);
this.clientKeytab = Objects.requireNonNull(keytab);
this.serverPrincipal = null;
this.serverKeytab = null;
this.appName = appName;
return this;
}
public KerberosTestServices build() throws Exception {
final Configuration oldConfig =
clientPrincipal != null ? Configuration.getConfiguration() : null;
JaasConfiguration jaasConfiguration = null;
if (clientPrincipal != null) {
jaasConfiguration =
(appName == null)
? new JaasConfiguration(
clientPrincipal, clientKeytab, serverPrincipal, serverKeytab)
: new JaasConfiguration(clientPrincipal, clientKeytab, appName);
}
return new KerberosTestServices(kdcWorkDir, jaasConfiguration, oldConfig, debug);
}
}
}