From e8247eec1103e387e02bbb1e8859b4d468688f48 Mon Sep 17 00:00:00 2001 From: Rakesh Radhakrishnan Date: Mon, 13 Feb 2017 23:41:31 +0530 Subject: [PATCH] ZOOKEEPER-2689: Fix Kerberos Authentication related test cases Presently 'MiniKdc.java' uses Apache Kerby which has a build requirement of jdk1.7+, http://directory.apache.org/kerby/developer-guide.html]. Since branch-3.4.x support Java 1.6 or higher, Apache Kerby binding is causing trouble. I've tried an attempt to rewrite MiniKdc.java test using old way of Kerberos implementation provided by Apache Directory Server project, org.apache.directory.* packages. Please refer MiniKdc implementation of Hadoop, trunk branch git hash revision 42e3a805117ff7cb054c2442f7b0e0cc54be63ad Author: Rakesh Radhakrishnan Reviewers: Mohammad Arshad Closes #170 from rakeshadr/ZK-2689 --- ivy.xml | 56 +-- .../zookeeper/server/quorum/auth/MiniKdc.java | 344 +++++++++++++----- .../server/quorum/auth/MiniKdcTest.java | 18 +- 3 files changed, 291 insertions(+), 127 deletions(-) diff --git a/ivy.xml b/ivy.xml index 437b86be04d..4c113846f38 100644 --- a/ivy.xml +++ b/ivy.xml @@ -78,30 +78,38 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/java/test/org/apache/zookeeper/server/quorum/auth/MiniKdc.java b/src/java/test/org/apache/zookeeper/server/quorum/auth/MiniKdc.java index 4afef41eadc..8e3cb1bed04 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/auth/MiniKdc.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/auth/MiniKdc.java @@ -17,26 +17,67 @@ */ package org.apache.zookeeper.server.quorum.auth; + import org.apache.commons.io.Charsets; -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.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.text.StrSubstitutor; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.api.ldap.schemaextractor.SchemaLdifExtractor; +import org.apache.directory.api.ldap.schemaextractor.impl.DefaultSchemaLdifExtractor; +import org.apache.directory.api.ldap.schemaloader.LdifSchemaLoader; +import org.apache.directory.api.ldap.schemamanager.impl.DefaultSchemaManager; +import org.apache.directory.server.constants.ServerDNConstants; +import org.apache.directory.server.core.DefaultDirectoryService; +import org.apache.directory.server.core.api.CacheService; +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.kerberos.KeyDerivationInterceptor; +import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmIndex; +import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition; +import org.apache.directory.server.core.partition.ldif.LdifPartition; +import org.apache.directory.server.kerberos.KerberosConfig; +import org.apache.directory.server.kerberos.kdc.KdcServer; +import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory; +import org.apache.directory.server.kerberos.shared.keytab.Keytab; +import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry; +import org.apache.directory.server.protocol.shared.transport.TcpTransport; +import org.apache.directory.server.protocol.shared.transport.UdpTransport; +import org.apache.directory.server.xdbm.Index; +import org.apache.directory.shared.kerberos.KerberosTime; +import org.apache.directory.shared.kerberos.codec.types.EncryptionType; +import org.apache.directory.shared.kerberos.components.EncryptionKey; +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.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.registries.SchemaLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.UUID; /** * Mini KDC based on Apache Directory Server that can be embedded in testcases @@ -44,10 +85,11 @@ *

* From within testcases: *

- * MiniKdc sets one System property when started and un-set when stopped: + * MiniKdc sets 2 System properties when started and un-sets them when stopped: *

    - *
  • sun.security.krb5.debug: set to the debug value provided in the - * configuration
  • + *
  • java.security.krb5.conf: set to the MiniKDC real/host/port
  • + *
  • sun.security.krb5.debug: set to the debug value provided in the + * configuration
  • *
* Because of this, multiple MiniKdc instances cannot be started in parallel. * For example, running testcases in parallel that start a KDC each. To @@ -73,7 +115,7 @@ * in case of bug fixing, history, etc. * * Branch : trunk - * Github Revision: 916140604ffef59466ba30832478311d3e6249bd + * Github Revision: 42e3a805117ff7cb054c2442f7b0e0cc54be63ad */ public class MiniKdc { @@ -81,6 +123,8 @@ public class MiniKdc { "java.security.krb5.conf"; public static final String SUN_SECURITY_KRB5_DEBUG = "sun.security.krb5.debug"; + private static final File testData = new File( + System.getProperty("test.data.dir", "build/test/data")); public static void main(String[] args) throws Exception { if (args.length < 4) { @@ -195,17 +239,13 @@ public static Properties createConf() { } private Properties conf; - private SimpleKdcServer simpleKdc; + private DirectoryService ds; + private KdcServer kdc; 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. * @@ -235,7 +275,12 @@ public MiniKdc(Properties conf, File workDir) throws Exception { LOG.info("---------------------------------------------------------------"); this.conf = conf; port = Integer.parseInt(conf.getProperty(KDC_PORT)); - String orgName= conf.getProperty(ORG_NAME); + if (port == 0) { + ServerSocket ss = new ServerSocket(0, 1, InetAddress.getByName(conf.getProperty(KDC_BIND_ADDRESS))); + port = ss.getLocalPort(); + ss.close(); + } + String orgName = conf.getProperty(ORG_NAME); String orgDomain = conf.getProperty(ORG_DOMAIN); realm = orgName.toUpperCase(Locale.ENGLISH) + "." + orgDomain.toUpperCase(Locale.ENGLISH); @@ -269,7 +314,6 @@ public String getRealm() { } public File getKrb5conf() { - krb5conf = new File(System.getProperty(JAVA_SECURITY_KRB5_CONF)); return krb5conf; } @@ -279,81 +323,186 @@ public File getKrb5conf() { * @throws Exception thrown if the MiniKdc could not be started. */ public synchronized void start() throws Exception { - if (simpleKdc != null) { + if (kdc != null) { throw new RuntimeException("Already started"); } - simpleKdc = new SimpleKdcServer(); - prepareKdcServer(); - simpleKdc.init(); - resetDefaultRealm(); - simpleKdc.start(); - LOG.info("MiniKdc stated."); + initDirectoryService(); + initKDCServer(); } - 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 initDirectoryService() throws Exception { + ds = new DefaultDirectoryService(); + ds.setInstanceLayout(new InstanceLayout(workDir)); + + CacheService cacheService = new CacheService(); + ds.setCacheService(cacheService); + + // first load the schema + InstanceLayout instanceLayout = ds.getInstanceLayout(); + File schemaPartitionDirectory = new File(instanceLayout.getPartitionsDirectory(), "schema"); + SchemaLdifExtractor extractor = new DefaultSchemaLdifExtractor(instanceLayout.getPartitionsDirectory()); + extractor.extractOrCopy(); + + SchemaLoader loader = new LdifSchemaLoader(schemaPartitionDirectory); + SchemaManager schemaManager = new DefaultSchemaManager(loader); + schemaManager.loadAllEnabled(); + ds.setSchemaManager(schemaManager); + // Init the LdifPartition with schema + LdifPartition schemaLdifPartition = new LdifPartition(schemaManager); + schemaLdifPartition.setPartitionPath(schemaPartitionDirectory.toURI()); + + // The schema partition + SchemaPartition schemaPartition = new SchemaPartition(schemaManager); + schemaPartition.setWrappedPartition(schemaLdifPartition); + ds.setSchemaPartition(schemaPartition); + + JdbmPartition systemPartition = new JdbmPartition(ds.getSchemaManager()); + systemPartition.setId("system"); + systemPartition.setPartitionPath( + new File(ds.getInstanceLayout().getPartitionsDirectory(), systemPartition.getId()).toURI()); + systemPartition.setSuffixDn(new Dn(ServerDNConstants.SYSTEM_DN)); + systemPartition.setSchemaManager(ds.getSchemaManager()); + ds.setSystemPartition(systemPartition); + + ds.getChangeLog().setEnabled(false); + ds.setDenormalizeOpAttrsEnabled(true); + ds.addLast(new KeyDerivationInterceptor()); + + // create one partition + String orgName = conf.getProperty(ORG_NAME).toLowerCase(Locale.ENGLISH); + String orgDomain = conf.getProperty(ORG_DOMAIN).toLowerCase(Locale.ENGLISH); + + JdbmPartition partition = new JdbmPartition(ds.getSchemaManager()); + partition.setId(orgName); + partition.setPartitionPath(new File(ds.getInstanceLayout().getPartitionsDirectory(), orgName).toURI()); + partition.setSuffixDn(new Dn("dc=" + orgName + ",dc=" + orgDomain)); + ds.addPartition(partition); + // indexes + Set> indexedAttributes = new HashSet>(); + indexedAttributes.add(new JdbmIndex("objectClass", false)); + indexedAttributes.add(new JdbmIndex("dc", false)); + indexedAttributes.add(new JdbmIndex("ou", false)); + partition.setIndexedAttributes(indexedAttributes); + + // And start the ds + ds.setInstanceId(conf.getProperty(INSTANCE)); + ds.startup(); + // context entry, after ds.startup() + Dn dn = new Dn("dc=" + orgName + ",dc=" + orgDomain); + Entry entry = ds.newEntry(dn); + entry.add("objectClass", "top", "domain"); + entry.add("dc", orgName); + ds.getAdminSession().add(entry); } - private void prepareKdcServer() throws Exception { - // transport - simpleKdc.setWorkDir(workDir); - simpleKdc.setKdcHost(getHost()); - simpleKdc.setKdcRealm(realm); - if (transport == null) { - transport = conf.getProperty(TRANSPORT); + private void initKDCServer() throws Exception { + String orgName = conf.getProperty(ORG_NAME); + String orgDomain = conf.getProperty(ORG_DOMAIN); + String bindAddress = conf.getProperty(KDC_BIND_ADDRESS); + final Map map = new HashMap(); + map.put("0", orgName.toLowerCase(Locale.ENGLISH)); + map.put("1", orgDomain.toLowerCase(Locale.ENGLISH)); + map.put("2", orgName.toUpperCase(Locale.ENGLISH)); + map.put("3", orgDomain.toUpperCase(Locale.ENGLISH)); + map.put("4", bindAddress); + + InputStream is1 = getMinikdcResourceAsStream("minikdc.ldiff"); + + SchemaManager schemaManager = ds.getSchemaManager(); + LdifReader reader = null; + + try { + final String content = StrSubstitutor.replace(IOUtils.toString(is1), map); + reader = new LdifReader(new StringReader(content)); + + for (LdifEntry ldifEntry : reader) { + ds.getAdminSession().add(new DefaultEntry(schemaManager, ldifEntry.getEntry())); + } + } finally { + IOUtils.closeQuietly(reader); + IOUtils.closeQuietly(is1); } - if (port == 0) { - port = NetworkUtil.getServerPort(); + + KerberosConfig kerberosConfig = new KerberosConfig(); + kerberosConfig.setMaximumRenewableLifetime(Long.parseLong(conf.getProperty(MAX_RENEWABLE_LIFETIME))); + kerberosConfig.setMaximumTicketLifetime(Long.parseLong(conf.getProperty(MAX_TICKET_LIFETIME))); + kerberosConfig.setSearchBaseDn(String.format("dc=%s,dc=%s", orgName, orgDomain)); + kerberosConfig.setPaEncTimestampRequired(false); + kdc = new KdcServer(kerberosConfig); + kdc.setDirectoryService(ds); + + // transport + String transport = conf.getProperty(TRANSPORT); + if (transport.trim().equals("TCP")) { + kdc.addTransports(new TcpTransport(bindAddress, port, 3, 50)); + } else if (transport.trim().equals("UDP")) { + kdc.addTransports(new UdpTransport(port)); + } else { + throw new IllegalArgumentException("Invalid transport: " + transport); } - 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); + kdc.setServiceName(conf.getProperty(INSTANCE)); + kdc.start(); + + StringBuilder sb = new StringBuilder(); + InputStream is2 = getMinikdcResourceAsStream("minikdc-krb5.conf"); + + BufferedReader r = null; + + try { + r = new BufferedReader(new InputStreamReader(is2, Charsets.UTF_8)); + String line = r.readLine(); + + while (line != null) { + sb.append(line).append("{3}"); + line = r.readLine(); } - } else { - throw new IllegalArgumentException("Need to set transport!"); + } finally { + IOUtils.closeQuietly(r); + IOUtils.closeQuietly(is2); } - simpleKdc.getKdcConfig().setString(KdcConfigKey.KDC_SERVICE_NAME, - conf.getProperty(INSTANCE)); - if (conf.getProperty(DEBUG) != null) { - krb5Debug = getAndSet(SUN_SECURITY_KRB5_DEBUG, conf.getProperty(DEBUG)); + + krb5conf = new File(workDir, "krb5.conf").getAbsoluteFile(); + FileUtils.writeStringToFile(krb5conf, MessageFormat.format(sb.toString(), getRealm(), getHost(), + Integer.toString(getPort()), System.getProperty("line.separator"))); + System.setProperty(JAVA_SECURITY_KRB5_CONF, krb5conf.getAbsolutePath()); + + System.setProperty(SUN_SECURITY_KRB5_DEBUG, conf.getProperty(DEBUG, "false")); + + // refresh the config + Class classRef; + if (System.getProperty("java.vendor").contains("IBM")) { + classRef = Class.forName("com.ibm.security.krb5.internal.Config"); + } else { + classRef = Class.forName("sun.security.krb5.Config"); } + Method refreshMethod = classRef.getMethod("refresh", new Class[0]); + refreshMethod.invoke(classRef, new Object[0]); + + LOG.info("MiniKdc listening at port: {}", getPort()); + LOG.info("MiniKdc setting JVM krb5.conf to: {}", krb5conf.getAbsolutePath()); + } + + private InputStream getMinikdcResourceAsStream(String resourceName) + throws FileNotFoundException { + File kdcResourceFile = new File(testData, "/kerberos/" + resourceName); + return new FileInputStream(kdcResourceFile); } /** * Stops the MiniKdc */ public synchronized void stop() { - if (simpleKdc != null) { + if (kdc != null) { + System.getProperties().remove(JAVA_SECURITY_KRB5_CONF); + System.getProperties().remove(SUN_SECURITY_KRB5_DEBUG); + kdc.stop(); try { - simpleKdc.stop(); - } catch (KrbException e) { - e.printStackTrace(); - } finally { - if(conf.getProperty(DEBUG) != null) { - System.setProperty(SUN_SECURITY_KRB5_DEBUG, - Boolean.toString(krb5Debug)); - } + ds.shutdown(); + } catch (Exception ex) { + LOG.error("Could not shutdown ApacheDS properly: {}", ex.toString(), ex); } } 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) { @@ -378,9 +527,20 @@ private void delete(File f) { * @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); + public synchronized void createPrincipal(String principal, String password) throws Exception { + String orgName = conf.getProperty(ORG_NAME); + String orgDomain = conf.getProperty(ORG_DOMAIN); + String baseDn = "ou=users,dc=" + orgName.toLowerCase(Locale.ENGLISH) + ",dc=" + + orgDomain.toLowerCase(Locale.ENGLISH); + String content = "dn: uid=" + principal + "," + baseDn + "\n" + "objectClass: top\n" + "objectClass: person\n" + + "objectClass: inetOrgPerson\n" + "objectClass: krb5principal\n" + "objectClass: krb5kdcentry\n" + + "cn: " + principal + "\n" + "sn: " + principal + "\n" + "uid: " + principal + "\n" + "userPassword: " + + password + "\n" + "krb5PrincipalName: " + principal + "@" + getRealm() + "\n" + + "krb5KeyVersionNumber: 0"; + + for (LdifEntry ldifEntry : new LdifReader(new StringReader(content))) { + ds.getAdminSession().add(new DefaultEntry(ds.getSchemaManager(), ldifEntry.getEntry())); + } } /** @@ -394,25 +554,21 @@ public synchronized void createPrincipal(String principal, String password) 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); - } + String generatedPassword = UUID.randomUUID().toString(); + Keytab keytab = new Keytab(); + List entries = new ArrayList(); for (String principal : principals) { - simpleKdc.getKadmin().exportKeytab(keytabFile, principal); + createPrincipal(principal, generatedPassword); + principal = principal + "@" + getRealm(); + KerberosTime timestamp = new KerberosTime(); + for (Map.Entry entry : KerberosKeyFactory + .getKerberosKeys(principal, generatedPassword).entrySet()) { + EncryptionKey ekey = entry.getValue(); + byte keyVersion = (byte) ekey.getKeyVersion(); + entries.add(new KeytabEntry(principal, 1L, timestamp, keyVersion, ekey)); + } } + keytab.setEntries(entries); + keytab.write(keytabFile); } - - /** - * 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; - } -} \ No newline at end of file +} diff --git a/src/java/test/org/apache/zookeeper/server/quorum/auth/MiniKdcTest.java b/src/java/test/org/apache/zookeeper/server/quorum/auth/MiniKdcTest.java index a7bbf7feb93..196d8bec776 100644 --- a/src/java/test/org/apache/zookeeper/server/quorum/auth/MiniKdcTest.java +++ b/src/java/test/org/apache/zookeeper/server/quorum/auth/MiniKdcTest.java @@ -18,8 +18,8 @@ package org.apache.zookeeper.server.quorum.auth; -import org.apache.kerby.kerberos.kerb.keytab.Keytab; -import org.apache.kerby.kerberos.kerb.type.base.PrincipalName; +import org.apache.directory.server.kerberos.shared.keytab.Keytab; +import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry; import org.junit.Assert; import org.junit.Test; @@ -30,7 +30,6 @@ import javax.security.auth.login.LoginContext; import java.io.File; import java.security.Principal; -import java.util.List; import java.util.Set; import java.util.Map; import java.util.HashSet; @@ -60,16 +59,17 @@ public void testKeytabGen() throws Exception { File workDir = getWorkDir(); kdc.createPrincipal(new File(workDir, "keytab"), "foo/bar", "bar/foo"); - List principalNameList = - Keytab.loadKeytab(new File(workDir, "keytab")).getPrincipals(); + Keytab kt = Keytab.read(new File(workDir, "keytab")); Set principals = new HashSet(); - for (PrincipalName principalName : principalNameList) { - principals.add(principalName.getName()); + for (KeytabEntry entry : kt.getEntries()) { + principals.add(entry.getPrincipalName()); } - + //here principals use \ instead of / + //because org.apache.directory.server.kerberos.shared.keytab.KeytabDecoder + // .getPrincipalName(IoBuffer buffer) use \\ when generates principal Assert.assertEquals(new HashSet(Arrays.asList( - "foo/bar@" + kdc.getRealm(), "bar/foo@" + kdc.getRealm())), + "foo\\bar@" + kdc.getRealm(), "bar\\foo@" + kdc.getRealm())), principals); }