blob: 4e095a32b03bf6559f05c08e5b3af39bd8c97026 [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.pulsar.broker.authentication;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.apache.kerby.kerberos.kerb.KrbException;
import org.apache.kerby.kerberos.kerb.server.KdcConfigKey;
import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
import org.apache.kerby.util.IOUtil;
import org.apache.kerby.util.NetworkUtil;
/**
* Mini KDC based on Apache Directory Server that can be embedded in testcases
* or used from command line as a standalone KDC.
*
* <p><b>From within testcases:</b>
*
* <p>MiniKdc sets one System property when started and un-set when stopped:
* <ul>
* <li>sun.security.krb5.debug: set to the debug value provided in the
* configuration</li>
* </ul>
* Because of this, multiple MiniKdc instances cannot be started in parallel.
* For example, running testcases in parallel that start a KDC each. To
* accomplish this a single MiniKdc should be used for all testcases running
* in parallel.
*
* <p>MiniKdc default configuration values are:
* <ul>
* <li>org.name=EXAMPLE (used to create the REALM)</li>
* <li>org.domain=COM (used to create the REALM)</li>
* <li>kdc.bind.address=localhost</li>
* <li>kdc.port=0 (ephemeral port)</li>
* <li>instance=DefaultKrbServer</li>
* <li>max.ticket.lifetime=86400000 (1 day)</li>
* <li>max.renewable.lifetime=604800000 (7 days)</li>
* <li>transport=TCP</li>
* <li>debug=false</li>
* </ul>
* The generated krb5.conf forces TCP connections.
* This code is originally from HDFS, see the file name MiniKdc there
* in case of bug fixing, history, etc.
* https://github.com/apache/hadoop/blob/trunk/hadoop-common-project/hadoop-minikdc/src/main/java/org/apache/hadoop/minikdc/MiniKdc.java
*/
@Slf4j
public class MiniKdc {
public static final String JAVA_SECURITY_KRB5_CONF =
"java.security.krb5.conf";
public static final String SUN_SECURITY_KRB5_DEBUG =
"sun.security.krb5.debug";
public static final String ORG_NAME = "org.name";
public static final String ORG_DOMAIN = "org.domain";
public static final String KDC_BIND_ADDRESS = "kdc.bind.address";
public static final String KDC_PORT = "kdc.port";
public static final String INSTANCE = "instance";
public static final String MAX_TICKET_LIFETIME = "max.ticket.lifetime";
public static final String MAX_RENEWABLE_LIFETIME = "max.renewable.lifetime";
public static final String TRANSPORT = "transport";
public static final String DEBUG = "debug";
private static final Set<String> PROPERTIES = new HashSet<String>();
private static final Properties DEFAULT_CONFIG = new Properties();
static {
PROPERTIES.add(ORG_NAME);
PROPERTIES.add(ORG_DOMAIN);
PROPERTIES.add(KDC_BIND_ADDRESS);
PROPERTIES.add(KDC_BIND_ADDRESS);
PROPERTIES.add(KDC_PORT);
PROPERTIES.add(INSTANCE);
PROPERTIES.add(TRANSPORT);
PROPERTIES.add(MAX_TICKET_LIFETIME);
PROPERTIES.add(MAX_RENEWABLE_LIFETIME);
DEFAULT_CONFIG.setProperty(KDC_BIND_ADDRESS, "localhost");
DEFAULT_CONFIG.setProperty(KDC_PORT, "0");
DEFAULT_CONFIG.setProperty(INSTANCE, "DefaultKrbServer");
DEFAULT_CONFIG.setProperty(ORG_NAME, "EXAMPLE");
DEFAULT_CONFIG.setProperty(ORG_DOMAIN, "COM");
DEFAULT_CONFIG.setProperty(TRANSPORT, "TCP");
DEFAULT_CONFIG.setProperty(MAX_TICKET_LIFETIME, "86400000");
DEFAULT_CONFIG.setProperty(MAX_RENEWABLE_LIFETIME, "604800000");
DEFAULT_CONFIG.setProperty(DEBUG, "false");
}
/**
* Convenience method that returns MiniKdc default configuration.
*
* <p>The returned configuration is a copy, it can be customized before using
* it to create a MiniKdc.
* @return a MiniKdc default configuration.
*/
public static Properties createConf() {
return (Properties) DEFAULT_CONFIG.clone();
}
private Properties conf;
private SimpleKdcServer simpleKdc;
private int port;
private String realm;
private File workDir;
private File krb5conf;
private String transport;
private boolean krb5Debug;
public void setTransport(String transport) {
this.transport = transport;
}
/**
* Creates a MiniKdc.
*
* @param conf MiniKdc configuration.
* @param workDir working directory, it should be the build directory. Under
* this directory an ApacheDS working directory will be created, this
* directory will be deleted when the MiniKdc stops.
* @throws Exception thrown if the MiniKdc could not be created.
*/
public MiniKdc(Properties conf, File workDir) throws Exception {
if (!conf.keySet().containsAll(PROPERTIES)) {
Set<String> missingProperties = new HashSet<String>(PROPERTIES);
missingProperties.removeAll(conf.keySet());
throw new IllegalArgumentException("Missing configuration properties: "
+ missingProperties);
}
this.workDir = new File(workDir, Long.toString(System.currentTimeMillis()));
if (!this.workDir.exists()
&& !this.workDir.mkdirs()) {
throw new RuntimeException("Cannot create directory " + this.workDir);
}
log.info("Configuration:");
log.info("---------------------------------------------------------------");
for (Map.Entry<?, ?> entry : conf.entrySet()) {
log.info(" {}: {}", entry.getKey(), entry.getValue());
}
log.info("---------------------------------------------------------------");
this.conf = conf;
port = Integer.parseInt(conf.getProperty(KDC_PORT));
String orgName = conf.getProperty(ORG_NAME);
String orgDomain = conf.getProperty(ORG_DOMAIN);
realm = orgName.toUpperCase(Locale.ENGLISH) + "."
+ orgDomain.toUpperCase(Locale.ENGLISH);
}
/**
* Returns the port of the MiniKdc.
*
* @return the port of the MiniKdc.
*/
public int getPort() {
return port;
}
/**
* Returns the host of the MiniKdc.
*
* @return the host of the MiniKdc.
*/
public String getHost() {
return conf.getProperty(KDC_BIND_ADDRESS);
}
/**
* Returns the realm of the MiniKdc.
*
* @return the realm of the MiniKdc.
*/
public String getRealm() {
return realm;
}
public File getKrb5conf() {
krb5conf = new File(System.getProperty(JAVA_SECURITY_KRB5_CONF));
return krb5conf;
}
/**
* Starts the MiniKdc.
*
* @throws Exception thrown if the MiniKdc could not be started.
*/
public synchronized void start() throws Exception {
if (simpleKdc != null) {
throw new RuntimeException("Already started");
}
simpleKdc = new SimpleKdcServer();
prepareKdcServer();
simpleKdc.init();
resetDefaultRealm();
simpleKdc.start();
log.info("MiniKdc stated.");
}
private void resetDefaultRealm() throws IOException {
try (InputStream templateResource = new FileInputStream(
getKrb5conf().getAbsolutePath())) {
String content = IOUtil.readInput(templateResource);
content = content.replaceAll("default_realm = .*\n",
"default_realm = " + getRealm() + "\n");
IOUtil.writeFile(content, getKrb5conf());
}
}
private void prepareKdcServer() throws Exception {
// transport
simpleKdc.setWorkDir(workDir);
simpleKdc.setKdcHost(getHost());
simpleKdc.setKdcRealm(realm);
if (transport == null) {
transport = conf.getProperty(TRANSPORT);
}
if (port == 0) {
port = NetworkUtil.getServerPort();
}
if (transport != null) {
if (transport.trim().equals("TCP")) {
simpleKdc.setKdcTcpPort(port);
simpleKdc.setAllowUdp(false);
} else if (transport.trim().equals("UDP")) {
simpleKdc.setKdcUdpPort(port);
simpleKdc.setAllowTcp(false);
} else {
throw new IllegalArgumentException("Invalid transport: " + transport);
}
} else {
throw new IllegalArgumentException("Need to set transport!");
}
simpleKdc.getKdcConfig().setString(KdcConfigKey.KDC_SERVICE_NAME,
conf.getProperty(INSTANCE));
if (conf.getProperty(DEBUG) != null) {
krb5Debug = getAndSet(SUN_SECURITY_KRB5_DEBUG, conf.getProperty(DEBUG));
}
}
/**
* Stops the MiniKdc.
*/
public synchronized void stop() {
if (simpleKdc != null) {
try {
simpleKdc.stop();
} catch (KrbException e) {
e.printStackTrace();
} finally {
if (conf.getProperty(DEBUG) != null) {
System.setProperty(SUN_SECURITY_KRB5_DEBUG,
Boolean.toString(krb5Debug));
}
}
}
delete(workDir);
try {
// Will be fixed in next Kerby version.
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("MiniKdc stopped.");
}
private void delete(File f) {
if (f.isFile()) {
if (!f.delete()) {
log.warn("WARNING: cannot delete file " + f.getAbsolutePath());
}
} else {
for (File c: f.listFiles()) {
delete(c);
}
if (!f.delete()) {
log.warn("WARNING: cannot delete directory " + f.getAbsolutePath());
}
}
}
/**
* Creates a principal in the KDC with the specified user and password.
*
* @param principal principal name, do not include the domain.
* @param password password.
* @throws Exception thrown if the principal could not be created.
*/
public synchronized void createPrincipal(String principal, String password)
throws Exception {
simpleKdc.createPrincipal(principal, password);
}
/**
* Creates multiple principals in the KDC and adds them to a keytab file.
*
* @param keytabFile keytab file to add the created principals.
* @param principals principals to add to the KDC, do not include the domain.
* @throws Exception thrown if the principals or the keytab file could not be
* created.
*/
public synchronized void createPrincipal(File keytabFile,
String ... principals)
throws Exception {
simpleKdc.createPrincipals(principals);
if (keytabFile.exists() && !keytabFile.delete()) {
log.error("Failed to delete keytab file: " + keytabFile);
}
for (String principal : principals) {
simpleKdc.getKadmin().exportKeytab(keytabFile, principal);
}
}
/**
* Set the System property; return the old value for caching.
*
* @param sysprop property
* @param debug true or false
* @return the previous value
*/
private boolean getAndSet(String sysprop, String debug) {
boolean old = Boolean.getBoolean(sysprop);
System.setProperty(sysprop, debug);
return old;
}
}