| /** |
| * 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.hadoop.minikdc; |
| 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; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| |
| /** |
| * 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. |
| */ |
| 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 void main(String[] args) throws Exception { |
| if (args.length < 4) { |
| System.out.println("Arguments: <WORKDIR> <MINIKDCPROPERTIES> " + |
| "<KEYTABFILE> [<PRINCIPALS>]+"); |
| System.exit(1); |
| } |
| File workDir = new File(args[0]); |
| if (!workDir.exists()) { |
| throw new RuntimeException("Specified work directory does not exists: " |
| + workDir.getAbsolutePath()); |
| } |
| Properties conf = createConf(); |
| File file = new File(args[1]); |
| if (!file.exists()) { |
| throw new RuntimeException("Specified configuration does not exists: " |
| + file.getAbsolutePath()); |
| } |
| Properties userConf = new Properties(); |
| InputStreamReader r = null; |
| try { |
| r = new InputStreamReader(new FileInputStream(file), |
| StandardCharsets.UTF_8); |
| userConf.load(r); |
| } finally { |
| if (r != null) { |
| r.close(); |
| } |
| } |
| for (Map.Entry<?, ?> entry : userConf.entrySet()) { |
| conf.put(entry.getKey(), entry.getValue()); |
| } |
| final MiniKdc miniKdc = new MiniKdc(conf, workDir); |
| miniKdc.start(); |
| File krb5conf = new File(workDir, "krb5.conf"); |
| if (miniKdc.getKrb5conf().renameTo(krb5conf)) { |
| File keytabFile = new File(args[2]).getAbsoluteFile(); |
| String[] principals = new String[args.length - 3]; |
| System.arraycopy(args, 3, principals, 0, args.length - 3); |
| miniKdc.createPrincipal(keytabFile, principals); |
| System.out.println(); |
| System.out.println("Standalone MiniKdc Running"); |
| System.out.println("---------------------------------------------------"); |
| System.out.println(" Realm : " + miniKdc.getRealm()); |
| System.out.println(" Running at : " + miniKdc.getHost() + ":" + |
| miniKdc.getHost()); |
| System.out.println(" krb5conf : " + krb5conf); |
| System.out.println(); |
| System.out.println(" created keytab : " + keytabFile); |
| System.out.println(" with principals : " + Arrays.asList(principals)); |
| System.out.println(); |
| System.out.println(" Do <CTRL-C> or kill <PID> to stop it"); |
| System.out.println("---------------------------------------------------"); |
| System.out.println(); |
| Runtime.getRuntime().addShutdownHook(new Thread() { |
| @Override |
| public void run() { |
| miniKdc.stop(); |
| } |
| }); |
| } else { |
| throw new RuntimeException("Cannot rename KDC's krb5conf to " |
| + krb5conf.getAbsolutePath()); |
| } |
| } |
| |
| private static final Logger LOG = LoggerFactory.getLogger(MiniKdc.class); |
| |
| 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 MIN_TICKET_LIFETIME = "min.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 started."); |
| } |
| |
| private void resetDefaultRealm() throws IOException { |
| 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)); |
| } |
| if (conf.getProperty(MIN_TICKET_LIFETIME) != null) { |
| simpleKdc.getKdcConfig().setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME, |
| Long.parseLong(conf.getProperty(MIN_TICKET_LIFETIME))); |
| } |
| if (conf.getProperty(MAX_TICKET_LIFETIME) != null) { |
| simpleKdc.getKdcConfig().setLong(KdcConfigKey.MAXIMUM_TICKET_LIFETIME, |
| Long.parseLong(conf.getProperty(MiniKdc.MAX_TICKET_LIFETIME))); |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| } |