| /* |
| * 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; |
| } |
| |
| } |