Skip to content

Instantly share code, notes, and snippets.

@beargiles
Last active May 14, 2018 17:48
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save beargiles/3753104737c874ffe6bd7ccff931b674 to your computer and use it in GitHub Desktop.
JAAS with Kerberos; Unit Test using Apache Hadoop Mini-KDC.
import java.io.File;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.apache.log4j.Logger;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
/**
* Basic KDC tests - these just show that the test environment is properly
* configured.
*
* IMPORTANT: The UserGroupInformation.loginUserFromKeytabAndReturnUGI() method
* does not currently work with the KDC junit Rule. We must use the Subject-based
* method when testing HDFS + Kerberos.
*/
public class BasicKdcTest {
@SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(BasicKdcTest.class);
@ClassRule
public static final TemporaryFolder tmpDir = new TemporaryFolder();
@ClassRule
public static final EmbeddedKdcResource kdc = new EmbeddedKdcResource();
private static KerberosPrincipal alice;
private static KerberosPrincipal bob;
private static File keytabFile;
private KerberosUtilities utils = new KerberosUtilities();
@BeforeClass
public static void createKeytabs() throws Exception {
// create Kerberos principal and keytab filename.
alice = new KerberosPrincipal("alice@" + kdc.getRealm());
bob = new KerberosPrincipal("bob@" + kdc.getRealm());
keytabFile = tmpDir.newFile("users.keytab");
// create keytab file containing key for Alice but not Bob.
kdc.createKeytabFile(keytabFile, "alice");
}
/**
* Test LoginContext login with keytab file (success).
*
* @throws LoginException
*/
@Test
public void testLoginWithKeytabSuccess() throws LoginException {
final LoginContext lc = utils.getKerberosLoginContext(alice, keytabFile);
lc.login();
assertThat("subject does not contain expected principal", lc.getSubject().getPrincipals(),
contains(alice));
lc.logout();
}
/**
* Test LoginContext login with keytab file(unknown user). This only
* tests for missing keytab entry, not a valid keytab file with an unknown user.
*
* @throws LoginException
*/
@Test(expected = LoginException.class)
public void testLoginWithKeytabUnknownUser() throws LoginException {
@SuppressWarnings("unused")
final LoginContext lc = utils.getKerberosLoginContext(bob, keytabFile);
}
/**
* Test getKeyTab() method (success)
*/
@Test
public void testGetKeyTabSuccess() throws LoginException {
assertThat("failed to see key", utils.getKeyTab(alice, keytabFile), notNullValue());
}
/**
* Test getKeyTab() method (unknown user)
*/
@Test(expected = LoginException.class)
public void testGetKeyTabUnknownUser() throws LoginException {
assertThat("failed to see key", utils.getKeyTab(bob, keytabFile), notNullValue());
}
}
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Properties;
import org.apache.hadoop.minikdc.MiniKdc;
import org.junit.rules.ExternalResource;
import org.apache.log4j.Logger;
/**
* A JUnit 4 wrapper around Hadoop MiniKDC server.
*
* IMPORTANT: this works with JAAS LoginContext but does not work with Hadoop
* UserGroupInformationloginUserFromKeytabAndReturnUGI(). It is probably missing
* setting a system property.
*/
public class EmbeddedKdcResource extends ExternalResource {
@SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(EmbeddedKdcResource.class);
private final File baseDir;
private MiniKdc kdc;
public EmbeddedKdcResource() {
try {
baseDir = Files.createTempDirectory("mini-kdc_").toFile();
} catch (IOException e) {
// throw AssertionError so we don't have to deal with handling declared
// exceptions when creating a @ClassRule object.
throw new AssertionError("unable to create temporary directory: " + e.getMessage());
}
}
/***
* Start KDC.
*/
@Override
public void before() throws Exception {
final Properties kdcConf = MiniKdc.createConf();
kdcConf.setProperty(MiniKdc.INSTANCE, "DefaultKrbServer");
kdcConf.setProperty(MiniKdc.ORG_NAME, "EMBEDDED");
kdcConf.setProperty(MiniKdc.ORG_DOMAIN, "INVARIANTPROPERTIES.COM");
// several sources say to use extremely short lifetimes in test environment.
// however setting these values results in errors.
//kdcConf.setProperty(MiniKdc.MAX_TICKET_LIFETIME, "15_000");
//kdcConf.setProperty(MiniKdc.MAX_RENEWABLE_LIFETIME, "30_000");
kdc = new MiniKdc(kdcConf, baseDir);
kdc.start();
// this is the standard way to set the default location of the JAAS config file.
// we don't need to do this since we handle it programmatically.
//System.setProperty("java.security.krb5.conf", kdc.getKrb5conf().getAbsolutePath());
}
/**
* Shut down KDC, delete temporary directory.
*/
@Override
public void after() {
if (kdc != null) {
kdc.stop();
}
}
/**
* Get realm.
*/
public String getRealm() {
return kdc.getRealm();
}
/**
* Create a keytab file with entries for specified user(s).
*
* @param keytabFile
* @param names
* @throws Exception
*/
public void createKeytabFile(File keytabFile, String... names) throws Exception {
kdc.createPrincipal(keytabFile, names);
}
}
import java.io.File;
import java.security.Principal;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.TextOutputCallback;
import javax.security.auth.kerberos.KerberosKey;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KeyTab;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.apache.log4j.Logger;
import static java.lang.Boolean.TRUE;
/**
* Kerberos utilities.
*/
public class KerberosUtilities {
private static final Logger LOG = Logger.getLogger(KerberosUtilities.class);
private static final String SECURITY_AUTH_MODULE_KRB5_LOGIN_MODULE =
"com.sun.security.auth.module.Krb5LoginModule";
/**
* Get login context.
*
* @param principal
* @param keytabFile
* @param getTgt
* @param ticketCacheFile
* @return
*/
public LoginContext getKerberosLoginContext(KerberosPrincipal principal, File keytabFile)
throws LoginException {
final KeyTab keytab = getKeyTab(principal, keytabFile);
// Krb5LoginModule doesn't seem to accept the keytab file on input.
final Set<Principal> principals = Collections.<Principal> singleton(principal);
final Set<?> pubCredentials = Collections.emptySet();
final Set<?> privCredentials = Collections.<Object> singleton(keytab);
final Subject subject = new Subject(false, principals, pubCredentials, privCredentials);
final String serviceName = "krb5";
final LoginContext lc = new LoginContext(serviceName, subject, new CallbackHandler() {
public void handle(Callback[] callbacks) {
for (Callback callback : callbacks) {
if (callback instanceof TextOutputCallback) {
LOG.error(((TextOutputCallback) callback).getMessage());
}
}
}
}, new Krb5WithKeytabLoginConfiguration(serviceName, principal, keytabFile, getTgt,
ticketCacheFile));
return lc;
}
/**
* Get login context using ticket cache.
*
* @param principal
* @param ticketCacheFile
* @return
*/
public LoginContext getKerberosLoginContextUsingTicketCache(KerberosPrincipal principal,
File ticketCacheFile) throws LoginException {
final Set<Principal> principals = Collections.<Principal> singleton(principal);
final Set<?> pubCredentials = Collections.emptySet();
final Set<?> privCredentials = Collections.emptySet();
final Subject subject = new Subject(false, principals, pubCredentials, privCredentials);
final String serviceName = "krb5";
final LoginContext lc = new LoginContext(serviceName, subject, new CallbackHandler() {
public void handle(Callback[] callbacks) {
for (Callback callback : callbacks) {
if (callback instanceof TextOutputCallback) {
LOG.error(((TextOutputCallback) callback).getMessage());
}
}
}
}, new Krb5WithTicketCacheLoginConfiguration(serviceName, principal, ticketCacheFile));
return lc;
}
/**
* Load KeyTab. getJaasDataSource() will store this value in the Subject's private credentials.
* getHadoopDataSource() only uses this method to verify that an appropriate keytab file was
* specified.
*
* @param principal
* @param keytabFile
* @return
* @throws LoginException
*/
KeyTab getKeyTab(KerberosPrincipal principal, File keytabFile) throws LoginException {
if (!keytabFile.exists() || !keytabFile.canRead()) {
throw new LoginException("specified file does not exist or is not readable.");
}
// verify keytab file exists
final KeyTab keytab = KeyTab.getInstance(principal, keytabFile);
if (!keytab.exists()) {
throw new LoginException("specified file is not a keytab file.");
}
// verify keytab file actually contains at least one key for this principal.
final KerberosKey[] keys = keytab.getKeys(principal);
if (keys.length == 0) {
throw new LoginException(
"specified file does not contain at least one key for this principal.");
}
// destroy keys since we don't need them, we just need to make sure they exist.
for (KerberosKey key : keys) {
try {
key.destroy();
} catch (DestroyFailedException e) {
LOG.debug("unable to destroy key");
}
}
return keytab;
}
/**
* Class that allows us to pull JAAS configuration values from a Map instead of an external
* .conf file.
*/
static class CustomLoginConfiguration extends javax.security.auth.login.Configuration {
private final Map<String, AppConfigurationEntry> entries = new HashMap<>();
public CustomLoginConfiguration(Map<String, Map<String, String>> params) {
for (Map.Entry<String, Map<String, String>> entry : params.entrySet()) {
entries.put(entry.getKey(),
new AppConfigurationEntry(SECURITY_AUTH_MODULE_KRB5_LOGIN_MODULE,
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
entry.getValue()));
}
}
/**
* Get entry.
*/
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
if (entries.containsKey(name)) {
return new AppConfigurationEntry[] { entries.get(name) };
}
return new AppConfigurationEntry[0];
}
}
/**
* Convenience class for Kerberos + Keytab JAAS configuration.
*/
static class Krb5WithKeytabLoginConfiguration extends CustomLoginConfiguration {
/**
* Constructor taking basic Kerberos properties.
*
* @param serviceName JAAS service name
* @param principal Kerberos principal
* @param keytabFile keytab file containing key for this principal
*/
public Krb5WithKeytabLoginConfiguration(String serviceName, KerberosPrincipal principal,
File keytabFile) {
super(Collections.singletonMap(serviceName, makeMap(principal, keytabFile)));
}
/**
* Static method that creates the Map required by the parent class.
*
* @param principal Kerberos principal
* @param keytabFile keytab file containing key for this principal
*/
private static Map<String, String> makeMap(KerberosPrincipal principal, File keytabFile) {
final Map<String, String> map = new HashMap<>();
// this is the basic Kerberos information
map.put("principal", principal.getName());
map.put("useKeyTab", TRUE.toString());
map.put("keyTab", keytabFile.getAbsolutePath());
// 'fail fast'
map.put("refreshKrb5Config", TRUE.toString());
// we're doing everything programmatically so we never want to prompt the user.
map.put("doNotPrompt", TRUE.toString());
return map;
}
}
/**
* Convenience class for Kerberos + ticket cache JAAS configuration.
*/
static class Krb5WithTicketCacheLoginConfiguration extends CustomLoginConfiguration {
/**
* Constructor taking Kerberos properties. The third and fourth parameters are only required
* in advanced use cases where a TGT is required. This is often true with Hadoop services.
*
* @param serviceName JAAS service name
* @param principal Kerberos principal
* @param ticketCacheFile ticket cache file containing Kerberos TGT. Default is based on
* UID.
*/
public Krb5WithTicketCacheLoginConfiguration(String serviceName, KerberosPrincipal principal,
File ticketCacheFile) {
super(Collections.singletonMap(serviceName, makeMap(principal, ticketCacheFile)));
}
/**
* Static method that creates the Map required by the parent class.
*
* @param principal Kerberos principal
* @param ticketCacheFile ticket cache file containing Kerberos TGT. Default is based on
* UID.
*/
private static Map<String, String> makeMap(KerberosPrincipal principal,
File ticketCacheFile) {
final Map<String, String> map = new HashMap<>();
// this is the basic Kerberos information
map.put("principal", principal.getName());
map.put("useTicketCache", TRUE.toString());
map.put("ticketCache", ticketCacheFile.getAbsolutePath());
// 'fail fast'
map.put("refreshKrb5Config", TRUE.toString());
// we're doing everything programmatically so we never want to prompt the user.
map.put("doNotPrompt", TRUE.toString());
return map;
}
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration debug="true"
xmlns:log4j='http://jakarta.apache.org/log4j/'>
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern"
value="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n" />
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="console" />
</root>
</log4j:configuration>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.invariantproperties</groupId>
<artifactId>jaas-krb5</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<hadoop-yarn.version>2.7.0</hadoop-yarn.version>
<apacheds.version>2.0.0-M15</apacheds.version>
<apacheds-api.version>1.0.0-M20</apacheds-api.version>
</properties>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-minikdc</artifactId>
<version>${hadoop-yarn.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.apache.directory.api</groupId>
<artifactId>api-all</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.directory.jdbm</groupId>
<artifactId>apacheds-jdbm1</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<!-- apache ds required by minikdc -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-minikdc</artifactId>
<version>${hadoop-yarn.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.apache.directory.api</groupId>
<artifactId>api-all</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.directory.jdbm</groupId>
<artifactId>apacheds-jdbm1</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core-annotations</artifactId>
<version>${apacheds.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.apache.directory.api</groupId>
<artifactId>api-ldap-schema-data</artifactId>
</exclusion>
<exclusion>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk15</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-annotations</artifactId>
<version>${apacheds.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk15</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-service</artifactId>
<version>${apacheds.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk15</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>ISO-8859-1</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment