blob: f1c62bf2419d1239b1f9d15e7adbe1666b3f8b2b [file] [log] [blame]
/*
* 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.qpid.server.test;
import static java.lang.Boolean.TRUE;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.PrivilegedExceptionAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
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.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import com.google.common.io.ByteStreams;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.qpid.test.utils.JvmVendor;
import org.apache.qpid.test.utils.SystemPropertySetter;
public class KerberosUtilities
{
public static final String REALM = "QPID.ORG";
public static final String HOST_NAME = InetAddress.getLoopbackAddress().getCanonicalHostName();
public static final String CLIENT_PRINCIPAL_NAME = "client";
public static final String CLIENT_PRINCIPAL_FULL_NAME = CLIENT_PRINCIPAL_NAME + "@" + REALM;
public static final String SERVER_PROTOCOL = "AMQP";
public static final String SERVICE_PRINCIPAL_NAME = SERVER_PROTOCOL + "/" + HOST_NAME;
public static final String ACCEPT_SCOPE = isIBM() ? "com.ibm.security.jgss.krb5.accept" : "com.sun.security.jgss.accept";
private static final String USE_SUBJECT_CREDS_ONLY = "javax.security.auth.useSubjectCredsOnly";
public static final String LOGIN_CONFIG = "java.security.auth.login.config";
private static final String INITIATE_SCOPE = isIBM() ? "com.ibm.security.jgss.krb5.initiate" : "com.sun.security.jgss.initiate";
private static final Logger LOGGER = LoggerFactory.getLogger(KerberosUtilities.class);
private static final String IBM_LOGIN_MODULE_CLASS = "com.ibm.security.auth.module.Krb5LoginModule";
private static final String SUN_LOGIN_MODULE_CLASS = "com.sun.security.auth.module.Krb5LoginModule";
private static final String KERBEROS_LOGIN_MODULE_CLASS = isIBM() ? IBM_LOGIN_MODULE_CLASS : SUN_LOGIN_MODULE_CLASS;
private static final String LOGIN_CONFIG_RESOURCE = "login.config";
private static final String LOGIN_IBM_CONFIG_RESOURCE = "login.ibm.config";
private static final String SERVICE_PRINCIPAL_FULL_NAME = SERVICE_PRINCIPAL_NAME + "@" + REALM;
private static final String BROKER_KEYTAB = "broker.keytab";
private static final String CLIENT_KEYTAB = "client.keytab";
public File prepareKeyTabs(final EmbeddedKdcResource kdc) throws Exception
{
final File clientKeyTabFile;
kdc.createPrincipal(BROKER_KEYTAB, SERVICE_PRINCIPAL_FULL_NAME);
clientKeyTabFile = kdc.createPrincipal(CLIENT_KEYTAB, CLIENT_PRINCIPAL_FULL_NAME);
return clientKeyTabFile;
}
public String prepareConfiguration(final String hostName, final SystemPropertySetter systemPropertySetter)
throws IOException
{
final Path loginConfig = transformLoginConfig(hostName);
final String configLocation = URLDecoder.decode(loginConfig.toFile().getAbsolutePath(), UTF_8.name());
systemPropertySetter.setSystemProperty(LOGIN_CONFIG, configLocation);
systemPropertySetter.setSystemProperty(USE_SUBJECT_CREDS_ONLY, "false");
return configLocation;
}
public byte[] buildToken(String clientPrincipalName, File clientKeyTabFile, String targetServerPrincipalName)
throws Exception
{
final LoginContext lc = createKerberosKeyTabLoginContext(INITIATE_SCOPE,
clientPrincipalName,
clientKeyTabFile);
Subject clientSubject = null;
String useSubjectCredsOnly = System.getProperty(USE_SUBJECT_CREDS_ONLY);
try
{
debug("Before login");
lc.login();
clientSubject = lc.getSubject();
debug("LoginContext subject {}", clientSubject);
System.setProperty(USE_SUBJECT_CREDS_ONLY, "true");
return Subject.doAs(clientSubject,
(PrivilegedExceptionAction<byte[]>) () -> buildTokenWithinSubjectWithKerberosTicket(
clientPrincipalName,
targetServerPrincipalName));
}
finally
{
if (useSubjectCredsOnly == null)
{
System.clearProperty(USE_SUBJECT_CREDS_ONLY);
}
else
{
System.setProperty(USE_SUBJECT_CREDS_ONLY, useSubjectCredsOnly);
}
if (clientSubject != null)
{
lc.logout();
}
}
}
private byte[] buildTokenWithinSubjectWithKerberosTicket(String clientPrincipalName,
String targetServerPrincipalName) throws GSSException
{
debug("Building token for client principal '{}' and server principal '{}'",
clientPrincipalName,
targetServerPrincipalName);
final GSSManager manager = GSSManager.getInstance();
final GSSName clientName = manager.createName(clientPrincipalName, GSSName.NT_USER_NAME);
final GSSCredential credential;
try
{
credential = manager.createCredential(clientName,
GSSCredential.DEFAULT_LIFETIME,
new Oid("1.2.840.113554.1.2.2"),
GSSCredential.INITIATE_ONLY);
}
catch (GSSException e)
{
debug("Failure to create credential for {}", clientName, e);
throw e;
}
debug("Client credential '{}'", credential);
final GSSName serverName = manager.createName(targetServerPrincipalName, GSSName.NT_USER_NAME);
final Oid spnegoMechOid = new Oid("1.3.6.1.5.5.2");
final GSSContext clientContext = manager.createContext(serverName.canonicalize(spnegoMechOid),
spnegoMechOid,
credential,
GSSContext.DEFAULT_LIFETIME);
debug("Requesting ticket using initiator's credentials");
try
{
clientContext.requestCredDeleg(true);
debug("Requesting ticket");
return clientContext.initSecContext(new byte[]{}, 0, 0);
}
catch (GSSException e)
{
debug("Failure to request token", e);
throw e;
}
finally
{
clientContext.dispose();
}
}
public LoginContext createKerberosKeyTabLoginContext(final String scopeName,
final String principalName,
final File keyTabFile)
throws LoginException
{
final KerberosPrincipal principal = new KerberosPrincipal(principalName);
final KeyTab keyTab = getKeyTab(principal, keyTabFile);
final Subject subject = new Subject(false,
Collections.singleton(principal),
Collections.emptySet(),
Collections.singleton(keyTab));
return createLoginContext(scopeName,
subject,
createKeyTabConfiguration(scopeName, keyTabFile, principal.getName()));
}
public KerberosKeyTabLoginConfiguration createKeyTabConfiguration(final String scopeName,
final File keyTabFile,
final String name)
{
return new KerberosKeyTabLoginConfiguration(scopeName, name, keyTabFile);
}
private LoginContext createLoginContext(final String serviceName, final Subject subject, final Configuration config)
throws LoginException
{
return new LoginContext(serviceName, subject, callbacks -> {
for (Callback callback : callbacks)
{
if (callback instanceof TextOutputCallback)
{
LOGGER.error(((TextOutputCallback) callback).getMessage());
}
}
}, config);
}
private KeyTab getKeyTab(final KerberosPrincipal principal, final File keyTabFile)
{
if (!keyTabFile.exists() || !keyTabFile.canRead())
{
throw new IllegalArgumentException("Specified file does not exist or is not readable.");
}
final KeyTab keytab = KeyTab.getInstance(principal, keyTabFile);
if (!keytab.exists())
{
throw new IllegalArgumentException("Specified file is not a keyTab file.");
}
final KerberosKey[] keys = keytab.getKeys(principal);
if (keys.length == 0)
{
throw new IllegalArgumentException("Specified file does not contain at least one key for this principal.");
}
for (final KerberosKey key : keys)
{
try
{
key.destroy();
}
catch (DestroyFailedException e)
{
LOGGER.debug("Unable to destroy key", e);
}
}
return keytab;
}
public static class KerberosKeyTabLoginConfiguration extends Configuration
{
private final String _scopeName;
private final AppConfigurationEntry _entry;
KerberosKeyTabLoginConfiguration(final String scopeName,
final String principalName,
final File keyTabFile)
{
final Map<String, String> options = new HashMap<>();
options.put("principal", principalName);
if (isIBM())
{
options.put("useKeytab", keyTabFile.getAbsolutePath());
options.put("credsType", "both");
}
else
{
options.put("keyTab", keyTabFile.getAbsolutePath());
options.put("useKeyTab", TRUE.toString());
options.put("doNotPrompt", TRUE.toString());
options.put("refreshKrb5Config", TRUE.toString());
}
_entry = new AppConfigurationEntry(KERBEROS_LOGIN_MODULE_CLASS,
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
options);
_scopeName = scopeName;
}
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name)
{
if (_scopeName.equals(name))
{
return new AppConfigurationEntry[]{_entry};
}
return new AppConfigurationEntry[0];
}
}
public void debug(String message, Object... args)
{
LOGGER.debug(message, args);
if (Boolean.TRUE.toString().equalsIgnoreCase(System.getProperty("sun.security.krb5.debug")))
{
System.out.println(String.format(message.replace("{}", "%s"), args));
}
}
private Path transformLoginConfig(String hostName) throws IOException
{
final String resourceName = isIBM() ? LOGIN_IBM_CONFIG_RESOURCE : LOGIN_CONFIG_RESOURCE;
final URL resource = KerberosUtilities.class.getClassLoader().getResource(resourceName);
if (resource == null)
{
throw new IllegalArgumentException(String.format("Unknown resource '%s'", resourceName));
}
final String config;
try (InputStream is = resource.openStream())
{
config = new String(ByteStreams.toByteArray(is), UTF_8);
}
catch (IOException e)
{
throw new IOException(String.format("Failed to load resource '%s'", resource.toExternalForm()), e);
}
final String newConfig = config.replace("AMQP/localhost", "AMQP/" + hostName)
.replace("target/" + BROKER_KEYTAB, toAbsolutePath(BROKER_KEYTAB))
.replace("target/" + CLIENT_KEYTAB, toAbsolutePath(CLIENT_KEYTAB));
final Path file = Paths.get("target", LOGIN_CONFIG_RESOURCE);
Files.write(file,
newConfig.getBytes(UTF_8),
StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
return file.toRealPath(LinkOption.NOFOLLOW_LINKS);
}
private String toAbsolutePath(String fileName)
{
final Path path = Paths.get("target", fileName)
.toAbsolutePath()
.normalize();
return path.toUri().getPath();
}
private static boolean isIBM()
{
return JvmVendor.getJvmVendor() == JvmVendor.IBM;
}
}