Last active
May 14, 2018 17:48
-
-
Save beargiles/3753104737c874ffe6bd7ccff931b674 to your computer and use it in GitHub Desktop.
JAAS with Kerberos; Unit Test using Apache Hadoop Mini-KDC.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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