/*
* Licensed 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.
* under the License.
*/
package org.apache.karaf.jaas.modules.ldap;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.SystemUtils;
import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
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.exception.LdapException;
import org.apache.directory.api.util.Strings;
import org.apache.directory.server.annotations.CreateKdcServer;
import org.apache.directory.server.annotations.CreateLdapServer;
import org.apache.directory.server.annotations.CreateTransport;
import org.apache.directory.server.annotations.SaslMechanism;
import org.apache.directory.server.core.annotations.ApplyLdifs;
import org.apache.directory.server.core.annotations.ContextEntry;
import org.apache.directory.server.core.annotations.CreateDS;
import org.apache.directory.server.core.annotations.CreateIndex;
import org.apache.directory.server.core.annotations.CreatePartition;
import org.apache.directory.server.core.integ.FrameworkRunner;
import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
import org.apache.directory.server.kerberos.kdc.AbstractKerberosITest;
import org.apache.directory.server.kerberos.kdc.KerberosTestUtils;
import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmMechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
import org.apache.directory.server.protocol.shared.transport.TcpTransport;
import org.apache.directory.server.protocol.shared.transport.Transport;
import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
import org.apache.directory.shared.kerberos.crypto.checksum.ChecksumType;
import org.apache.felix.utils.properties.Properties;
import org.apache.karaf.jaas.boot.principal.RolePrincipal;
import org.apache.karaf.jaas.boot.principal.UserPrincipal;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KerberosTicket;
import javax.security.auth.login.LoginException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.Principal;
import java.util.Collections;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(FrameworkRunner.class)
@CreateDS(name = "GSSAPILdapLoginModuleTest-class",
partitions =
{
@CreatePartition(
name = "example",
suffix = "dc=example,dc=com",
contextEntry = @ContextEntry(
entryLdif =
"dn: dc=example,dc=com\n" +
"dc: example\n" +
"objectClass: top\n" +
"objectClass: domain\n\n"),
indexes =
{
@CreateIndex(attribute = "objectClass"),
@CreateIndex(attribute = "dc"),
@CreateIndex(attribute = "ou")
})
},
additionalInterceptors =
{
KeyDerivationInterceptor.class
})
@CreateLdapServer(
transports =
{
@CreateTransport(protocol = "LDAP")
},
saslHost = "localhost",
saslPrincipal = "ldap/localhost@EXAMPLE.COM",
saslMechanisms =
{
@SaslMechanism(name = SupportedSaslMechanisms.PLAIN, implClass = PlainMechanismHandler.class),
@SaslMechanism(name = SupportedSaslMechanisms.CRAM_MD5, implClass = CramMd5MechanismHandler.class),
@SaslMechanism(name = SupportedSaslMechanisms.DIGEST_MD5, implClass = DigestMd5MechanismHandler.class),
@SaslMechanism(name = SupportedSaslMechanisms.GSSAPI, implClass = GssapiMechanismHandler.class),
@SaslMechanism(name = SupportedSaslMechanisms.NTLM, implClass = NtlmMechanismHandler.class),
@SaslMechanism(name = SupportedSaslMechanisms.GSS_SPNEGO, implClass = NtlmMechanismHandler.class)
})
@CreateKdcServer(
transports =
{
@CreateTransport(protocol = "UDP", port = 6088),
@CreateTransport(protocol = "TCP", port = 6088)
})
@ApplyLdifs({
"dn: ou=users,dc=example,dc=com",
"objectClass: top",
"objectClass: organizationalUnit",
"ou: users",
"dn: ou=groups,dc=example,dc=com",
"objectClass: top",
"objectClass: organizationalUnit",
"ou: groups",
"dn: cn=admin,ou=groups,dc=example,dc=com",
"objectClass: top",
"objectClass: groupOfNames",
"cn: admin",
"member: uid=hnelson,ou=users,dc=example,dc=com"
})
public class GSSAPILdapLoginModuleTest extends AbstractKerberosITest {
private static boolean loginConfigUpdated;
@Before
public void setUp() throws Exception {
super.setUp();
// Set up a partition for EXAMPLE.COM and add user and service principals to test authentication with.
KerberosTestUtils.fixServicePrincipalName(
"ldap/" + KerberosTestUtils.getHostName() + "@EXAMPLE.COM", null, getLdapServer());
setupEnv(TcpTransport.class,
EncryptionType.AES128_CTS_HMAC_SHA1_96, ChecksumType.HMAC_SHA1_96_AES128);
kdcServer.getConfig().setPaEncTimestampRequired(false);
String basedir = System.getProperty("basedir");
if (basedir == null) {
basedir = new File(".").getCanonicalPath();
}
File config = new File(basedir + "/target/test-classes/org/apache/karaf/jaas/modules/ldap/gssapi.login.config");
System.setProperty("java.security.auth.login.config", config.toString());
updatePort();
}
public void updatePort() throws Exception {
if (!loginConfigUpdated) {
String basedir = System.getProperty("basedir");
if (basedir == null) {
basedir = new File(".").getCanonicalPath();
}
// Read in ldap.properties and substitute in the correct port
File f = new File(basedir + "/src/test/resources/org/apache/karaf/jaas/modules/ldap/gssapi.ldap.properties");
FileInputStream inputStream = new FileInputStream(f);
String content = IOUtils.toString(inputStream, "UTF-8");
inputStream.close();
content = content.replaceAll("portno", "" + super.getLdapServer().getPort());
content = content.replaceAll("address", KerberosTestUtils.getHostName());
File f2 = new File(basedir + "/target/test-classes/org/apache/karaf/jaas/modules/ldap/gssapi.ldap.properties");
FileOutputStream outputStream = new FileOutputStream(f2);
IOUtils.write(content, outputStream, "UTF-8");
outputStream.close();
loginConfigUpdated = true;
}
}
@After
public void tearDown() throws Exception {
LDAPCache.clear();
super.tearDown();
}
@Test
public void testSuccess() throws Exception {
Properties options = ldapLoginModuleOptions();
GSSAPILdapLoginModule module = new GSSAPILdapLoginModule();
CallbackHandler cb = new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback cb : callbacks) {
if (cb instanceof NameCallback) {
((NameCallback) cb).setName("hnelson");
} else if (cb instanceof PasswordCallback) {
((PasswordCallback) cb).setPassword("secret".toCharArray());
}
}
}
};
Subject subject = new Subject();
module.initialize(subject, cb, null, options);
assertEquals("Precondition", 0, subject.getPrincipals().size());
assertTrue(module.login());
assertTrue(module.commit());
assertEquals(3, subject.getPrincipals().size());
boolean foundKrb5User = false;
boolean foundUser = false;
boolean foundRole = false;
boolean foundTicket = false;
for (Principal pr : subject.getPrincipals()) {
if (pr instanceof KerberosPrincipal) {
assertEquals("hnelson@EXAMPLE.COM", pr.getName());
foundKrb5User = true;
} else if (pr instanceof UserPrincipal) {
assertEquals("hnelson", pr.getName());
foundUser = true;
} else if (pr instanceof RolePrincipal) {
assertEquals("admin", pr.getName());
foundRole = true;
}
}
for (Object crd : subject.getPrivateCredentials()) {
if (crd instanceof KerberosTicket) {
assertEquals("hnelson@EXAMPLE.COM", ((KerberosTicket) crd).getClient().getName());
assertEquals("krbtgt/EXAMPLE.COM@EXAMPLE.COM", ((KerberosTicket) crd).getServer().getName());
foundTicket = true;
break;
}
}
assertTrue("Principals should contains kerberos user", foundKrb5User);
assertTrue("Principals should contains ldap user", foundUser);
assertTrue("Principals should contains ldap role", foundRole);
assertTrue("PricatePrincipals should contains kerberos ticket", foundTicket);
assertTrue(module.logout());
assertEquals("Principals should be gone as the user has logged out", 0, subject.getPrincipals().size());
}
@Test(expected = LoginException.class)
public void testUsernameFailure() throws Exception {
Properties options = ldapLoginModuleOptions();
GSSAPILdapLoginModule module = new GSSAPILdapLoginModule();
CallbackHandler cb = new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback cb : callbacks) {
if (cb instanceof NameCallback) {
((NameCallback) cb).setName("hnelson0");
} else if (cb instanceof PasswordCallback) {
((PasswordCallback) cb).setPassword("secret".toCharArray());
}
}
}
};
Subject subject = new Subject();
module.initialize(subject, cb, null, options);
assertEquals("Precondition", 0, subject.getPrincipals().size());
assertTrue(module.login()); // should throw LoginException
}
@Test(expected = LoginException.class)
public void testPasswordFailure() throws Exception {
Properties options = ldapLoginModuleOptions();
GSSAPILdapLoginModule module = new GSSAPILdapLoginModule();
CallbackHandler cb = new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback cb : callbacks) {
if (cb instanceof NameCallback) {
((NameCallback) cb).setName("hnelson");
} else if (cb instanceof PasswordCallback) {
((PasswordCallback) cb).setPassword("secret0".toCharArray());
}
}
}
};
Subject subject = new Subject();
module.initialize(subject, cb, null, options);
assertEquals("Precondition", 0, subject.getPrincipals().size());
assertTrue(module.login());
}
@Test(expected = LoginException.class)
public void testUserNotFound() throws Exception {
Properties options = ldapLoginModuleOptions();
GSSAPILdapLoginModule module = new GSSAPILdapLoginModule();
CallbackHandler cb = new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback cb : callbacks) {
if (cb instanceof NameCallback) {
((NameCallback) cb).setName("test");
} else if (cb instanceof PasswordCallback) {
((PasswordCallback) cb).setPassword("test".toCharArray());
}
}
}
};
Subject subject = new Subject();
module.initialize(subject, cb, null, options);
assertEquals("Precondition", 0, subject.getPrincipals().size());
assertFalse(module.login());
}
@Test(expected = LoginException.class)
public void testNoRealm() throws Exception {
Properties options = ldapLoginModuleOptions();
options.remove(GSSAPILdapLoginModule.REALM_PROPERTY);
GSSAPILdapLoginModule module = new GSSAPILdapLoginModule();
CallbackHandler cb = new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback cb : callbacks) {
if (cb instanceof NameCallback) {
((NameCallback) cb).setName("hnelson0");
} else if (cb instanceof PasswordCallback) {
((PasswordCallback) cb).setPassword("secret".toCharArray());
}
}
}
};
Subject subject = new Subject();
module.initialize(subject, cb, null, options);
assertEquals("Precondition", 0, subject.getPrincipals().size());
assertTrue(module.login()); // should throw LoginException
}
protected void setupEnv(Class<? extends Transport> transport, EncryptionType encryptionType,
ChecksumType checksumType)
throws Exception {
// create krb5.conf with proper encryption type
String krb5confPath = createKrb5Conf(checksumType, encryptionType, transport == TcpTransport.class);
System.setProperty("java.security.krb5.conf", krb5confPath);
// change encryption type in KDC
kdcServer.getConfig().setEncryptionTypes(Collections.singleton(encryptionType));
// create principals
createPrincipal("uid=" + USER_UID, "Last", "admin",
USER_UID, USER_PASSWORD, USER_UID + "@" + REALM);
createPrincipal("uid=krbtgt", "KDC Service", "KDC Service",
"krbtgt", "secret", "krbtgt/" + REALM + "@" + REALM);
String servicePrincipal = LDAP_SERVICE_NAME + "/" + HOSTNAME + "@" + REALM;
createPrincipal("uid=ldap", "Service", "LDAP Service",
"ldap", "randall", servicePrincipal);
}
private String createKrb5Conf(ChecksumType checksumType, EncryptionType encryptionType, boolean isTcp) throws IOException {
File file = folder.newFile("krb5.conf");
String data = "";
data += "[libdefaults]" + SystemUtils.LINE_SEPARATOR;
data += "default_realm = " + REALM + SystemUtils.LINE_SEPARATOR;
data += "default_tkt_enctypes = " + encryptionType.getName() + SystemUtils.LINE_SEPARATOR;
data += "default_tgs_enctypes = " + encryptionType.getName() + SystemUtils.LINE_SEPARATOR;
data += "permitted_enctypes = " + encryptionType.getName() + SystemUtils.LINE_SEPARATOR;
// data += "default_checksum = " + checksumType.getName() + SystemUtils.LINE_SEPARATOR;
// data += "ap_req_checksum_type = " + checksumType.getName() + SystemUtils.LINE_SEPARATOR;
data += "default-checksum_type = " + checksumType.getName() + SystemUtils.LINE_SEPARATOR;
if (isTcp) {
data += "udp_preference_limit = 1" + SystemUtils.LINE_SEPARATOR;
}
data += "[realms]" + SystemUtils.LINE_SEPARATOR;
data += REALM + " = {" + SystemUtils.LINE_SEPARATOR;
data += "kdc = " + HOSTNAME + ":" + kdcServer.getTransports()[0].getPort() + SystemUtils.LINE_SEPARATOR;
data += "}" + SystemUtils.LINE_SEPARATOR;
data += "[domain_realm]" + SystemUtils.LINE_SEPARATOR;
data += "." + Strings.lowerCaseAscii(REALM) + " = " + REALM + SystemUtils.LINE_SEPARATOR;
data += Strings.lowerCaseAscii(REALM) + " = " + REALM + SystemUtils.LINE_SEPARATOR;
FileUtils.writeStringToFile(file, data);
return file.getAbsolutePath();
}
private void createPrincipal(String rdn, String sn, String cn,
String uid, String userPassword, String principalName) throws LdapException {
Entry entry = new DefaultEntry();
entry.setDn(rdn + "," + USERS_DN);
entry.add("objectClass", "top", "person", "inetOrgPerson", "krb5principal", "krb5kdcentry");
entry.add("cn", cn);
entry.add("sn", sn);
entry.add("uid", uid);
entry.add("userPassword", userPassword);
entry.add("krb5PrincipalName", principalName);
entry.add("krb5KeyVersionNumber", "0");
conn.add(entry);
}
protected Properties ldapLoginModuleOptions() throws IOException {
String basedir = System.getProperty("basedir");
if (basedir == null) {
basedir = new File(".").getCanonicalPath();
}
File file = new File(basedir + "/target/test-classes/org/apache/karaf/jaas/modules/ldap/gssapi.ldap.properties");
return new Properties(file);
}
}