/** * 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.atlas.security; import org.apache.atlas.AtlasException; import org.apache.commons.collections.MapUtils; import org.apache.commons.configuration.ConfigurationConverter; import org.apache.commons.lang.ArrayUtils; import org.apache.hadoop.security.SecurityUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.SortedSet; import java.util.StringTokenizer; import java.util.TreeSet; /** * InMemoryJAASConfiguration * * An utility class - which has a static method init to load all JAAS configuration from Application * properties file (eg: atlas.properties) and set it as part of the default lookup configuration for * all JAAS configuration lookup. * * Example settings in jaas-application.properties: * * atlas.jaas.KafkaClient.loginModuleName = com.sun.security.auth.module.Krb5LoginModule * atlas.jaas.KafkaClient.loginModuleControlFlag = required * atlas.jaas.KafkaClient.option.useKeyTab = true * atlas.jaas.KafkaClient.option.storeKey = true * atlas.jaas.KafkaClient.option.serviceName = kafka * atlas.jaas.KafkaClient.option.keyTab = /etc/security/keytabs/kafka_client.keytab * atlas.jaas.KafkaClient.option.principal = kafka-client-1@EXAMPLE.COM * atlas.jaas.MyClient.0.loginModuleName = com.sun.security.auth.module.Krb5LoginModule * atlas.jaas.MyClient.0.loginModuleControlFlag = required * atlas.jaas.MyClient.0.option.useKeyTab = true * atlas.jaas.MyClient.0.option.storeKey = true * atlas.jaas.MyClient.0.option.serviceName = kafka * atlas.jaas.MyClient.0.option.keyTab = /etc/security/keytabs/kafka_client.keytab * atlas.jaas.MyClient.0.option.principal = kafka-client-1@EXAMPLE.COM * atlas.jaas.MyClient.1.loginModuleName = com.sun.security.auth.module.Krb5LoginModule * atlas.jaas.MyClient.1.loginModuleControlFlag = optional * atlas.jaas.MyClient.1.option.useKeyTab = true * atlas.jaas.MyClient.1.option.storeKey = true * atlas.jaas.MyClient.1.option.serviceName = kafka * atlas.jaas.MyClient.1.option.keyTab = /etc/security/keytabs/kafka_client.keytab * atlas.jaas.MyClient.1.option.principal = kafka-client-1@EXAMPLE.COM * This will set the JAAS configuration - equivalent to the jaas.conf file entries: * KafkaClient { * com.sun.security.auth.module.Krb5LoginModule required * useKeyTab=true * storeKey=true * serviceName=kafka * keyTab="/etc/security/keytabs/kafka_client.keytab" * principal="kafka-client-1@EXAMPLE.COM"; * }; * MyClient { * com.sun.security.auth.module.Krb5LoginModule required * useKeyTab=true * storeKey=true * serviceName=kafka keyTab="/etc/security/keytabs/kafka_client.keytab" * principal="kafka-client-1@EXAMPLE.COM"; * }; * MyClient { * com.sun.security.auth.module.Krb5LoginModule optional * useKeyTab=true * storeKey=true * serviceName=kafka * keyTab="/etc/security/keytabs/kafka_client.keytab" * principal="kafka-client-1@EXAMPLE.COM"; * }; * * Here is the syntax for atlas.properties to add JAAS configuration: * * The property name has to begin with 'atlas.jaas.' + clientId (in case of Kafka client, * it expects the clientId to be KafkaClient). * The following property must be there to specify the JAAS loginModule name * 'atlas.jaas.' + clientId + '.loginModuleName' * The following optional property should be set to specify the loginModuleControlFlag * 'atlas.jaas.' + clientId + '.loginModuleControlFlag' * Default value : required , Possible values: required, optional, sufficient, requisite * Then you can add additional optional parameters as options for the configuration using the following * syntax: * 'atlas.jaas.' + clientId + '.option.' + <optionName> = <optionValue> * * The current setup will lookup JAAS configration from the atlas-application.properties first, if not available, * it will delegate to the original configuration * */ public final class InMemoryJAASConfiguration extends Configuration { private static final Logger LOG = LoggerFactory.getLogger(InMemoryJAASConfiguration.class); private static final String JAAS_CONFIG_PREFIX_PARAM = "atlas.jaas."; private static final String JAAS_CONFIG_LOGIN_MODULE_NAME_PARAM = "loginModuleName"; private static final String JAAS_CONFIG_LOGIN_MODULE_CONTROL_FLAG_PARAM = "loginModuleControlFlag"; private static final String JAAS_CONFIG_LOGIN_OPTIONS_PREFIX = "option"; private static final String JAAS_PRINCIPAL_PROP = "principal"; private static final Map<String, String> CONFIG_SECTION_REDIRECTS = new HashMap<>(); private Configuration parent = null; private Map<String, List<AppConfigurationEntry>> applicationConfigEntryMap = new HashMap<>(); public static void init(String propFile) throws AtlasException { if (LOG.isDebugEnabled()) { LOG.debug("==> InMemoryJAASConfiguration.init({})", propFile); } InputStream in = null; try { Properties properties = new Properties(); in = ClassLoader.getSystemResourceAsStream(propFile); if (in == null) { if (!propFile.startsWith("/")) { in = ClassLoader.getSystemResourceAsStream("/" + propFile); } if (in == null) { in = new FileInputStream(new File(propFile)); } } properties.load(in); init(properties); } catch (IOException e) { throw new AtlasException("Failed to load JAAS application properties", e); } finally { if (in != null) { try { in.close(); } catch (Exception exception) { // Ignore } } } if (LOG.isDebugEnabled()) { LOG.debug("<== InMemoryJAASConfiguration.init({})", propFile); } } public static void init(org.apache.commons.configuration.Configuration atlasConfiguration) throws AtlasException { LOG.debug("==> InMemoryJAASConfiguration.init()"); if (atlasConfiguration != null && !atlasConfiguration.isEmpty()) { Properties properties = ConfigurationConverter.getProperties(atlasConfiguration); init(properties); } else { throw new AtlasException("Failed to load JAAS application properties: configuration NULL or empty!"); } LOG.debug("<== InMemoryJAASConfiguration.init()"); } public static void init(Properties properties) throws AtlasException { LOG.debug("==> InMemoryJAASConfiguration.init()"); if (properties != null && MapUtils.isNotEmpty(properties)) { InMemoryJAASConfiguration conf = new InMemoryJAASConfiguration(properties); Configuration.setConfiguration(conf); } else { throw new AtlasException("Failed to load JAAS application properties: properties NULL or empty!"); } LOG.debug("<== InMemoryJAASConfiguration.init()"); } @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { if (LOG.isDebugEnabled()) { LOG.debug("==> InMemoryJAASConfiguration.getAppConfigurationEntry({})", name); } AppConfigurationEntry[] ret = null; List<AppConfigurationEntry> retList = null; String redirectedName = getConfigSectionRedirect(name); if (redirectedName != null) { retList = applicationConfigEntryMap.get(redirectedName); if (LOG.isDebugEnabled()) { LOG.debug("Redirected jaasConfigSection ({} -> {}): ", name, redirectedName, retList); } } if (retList == null || retList.size() == 0) { retList = applicationConfigEntryMap.get(name); } if (retList == null || retList.size() == 0) { if (parent != null) { ret = parent.getAppConfigurationEntry(name); } } else { int sz = retList.size(); ret = new AppConfigurationEntry[sz]; ret = retList.toArray(ret); } if (LOG.isDebugEnabled()) { LOG.debug("<== InMemoryJAASConfiguration.getAppConfigurationEntry({}): {}", name, ArrayUtils.toString(ret)); } return ret; } private InMemoryJAASConfiguration(Properties prop) { parent = Configuration.getConfiguration(); initialize(prop); } private void initialize(Properties properties) { LOG.debug("==> InMemoryJAASConfiguration.initialize()"); int prefixLen = JAAS_CONFIG_PREFIX_PARAM.length(); Map<String, SortedSet<Integer>> jaasClients = new HashMap<>(); for (String key : properties.stringPropertyNames()) { if (key.startsWith(JAAS_CONFIG_PREFIX_PARAM)) { String jaasKey = key.substring(prefixLen); StringTokenizer tokenizer = new StringTokenizer(jaasKey, "."); int tokenCount = tokenizer.countTokens(); if (tokenCount > 0) { String clientId = tokenizer.nextToken(); SortedSet<Integer> indexList = jaasClients.get(clientId); if (indexList == null) { indexList = new TreeSet<>(); jaasClients.put(clientId, indexList); } String indexStr = tokenizer.nextToken(); int indexId = isNumeric(indexStr) ? Integer.parseInt(indexStr) : -1; Integer clientIdIndex = Integer.valueOf(indexId); if (!indexList.contains(clientIdIndex)) { indexList.add(clientIdIndex); } } } } for (String jaasClient : jaasClients.keySet()) { for (Integer index : jaasClients.get(jaasClient)) { String keyPrefix = JAAS_CONFIG_PREFIX_PARAM + jaasClient + "."; if (index > -1) { keyPrefix = keyPrefix + String.valueOf(index) + "."; } String keyParam = keyPrefix + JAAS_CONFIG_LOGIN_MODULE_NAME_PARAM; String loginModuleName = properties.getProperty(keyParam); if (loginModuleName == null) { LOG.error("Unable to add JAAS configuration for client [{}] as it is missing param [{}]. Skipping JAAS config for [{}]", jaasClient, keyParam, jaasClient); continue; } else { loginModuleName = loginModuleName.trim(); } keyParam = keyPrefix + JAAS_CONFIG_LOGIN_MODULE_CONTROL_FLAG_PARAM; String controlFlag = properties.getProperty(keyParam); AppConfigurationEntry.LoginModuleControlFlag loginControlFlag = null; if (controlFlag != null) { controlFlag = controlFlag.trim().toLowerCase(); switch (controlFlag) { case "optional": loginControlFlag = AppConfigurationEntry.LoginModuleControlFlag.OPTIONAL; break; case "requisite": loginControlFlag = AppConfigurationEntry.LoginModuleControlFlag.REQUISITE; break; case "sufficient": loginControlFlag = AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT; break; case "required": loginControlFlag = AppConfigurationEntry.LoginModuleControlFlag.REQUIRED; break; default: String validValues = "optional|requisite|sufficient|required"; LOG.warn("Unknown JAAS configuration value for ({}) = [{}], valid value are [{}] using the default value, REQUIRED", keyParam, controlFlag, validValues); loginControlFlag = AppConfigurationEntry.LoginModuleControlFlag.REQUIRED; break; } } else { LOG.warn("Unable to find JAAS configuration ({}); using the default value, REQUIRED", keyParam); loginControlFlag = AppConfigurationEntry.LoginModuleControlFlag.REQUIRED; } Map<String, String> options = new HashMap<>(); String optionPrefix = keyPrefix + JAAS_CONFIG_LOGIN_OPTIONS_PREFIX + "."; int optionPrefixLen = optionPrefix.length(); for (String key : properties.stringPropertyNames()) { if (key.startsWith(optionPrefix)) { String optionKey = key.substring(optionPrefixLen); String optionVal = properties.getProperty(key); if (optionVal != null) { optionVal = optionVal.trim(); try { if (optionKey.equalsIgnoreCase(JAAS_PRINCIPAL_PROP)) { optionVal = SecurityUtil.getServerPrincipal(optionVal, (String) null); } } catch (IOException e) { LOG.warn("Failed to build serverPrincipal. Using provided value:[{}]", optionVal); } } options.put(optionKey, optionVal); } } AppConfigurationEntry entry = new AppConfigurationEntry(loginModuleName, loginControlFlag, options); if (LOG.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); sb.append("Adding client: [").append(jaasClient).append("{").append(index).append("}]\n"); sb.append("\tloginModule: [").append(loginModuleName).append("]\n"); sb.append("\tcontrolFlag: [").append(loginControlFlag).append("]\n"); for (String key : options.keySet()) { String val = options.get(key); sb.append("\tOptions: [").append(key).append("] => [").append(val).append("]\n"); } LOG.debug(sb.toString()); } List<AppConfigurationEntry> retList = applicationConfigEntryMap.get(jaasClient); if (retList == null) { retList = new ArrayList<>(); applicationConfigEntryMap.put(jaasClient, retList); } retList.add(entry); } } LOG.debug("<== InMemoryJAASConfiguration.initialize({})", applicationConfigEntryMap); } private static boolean isNumeric(String str) { return str.matches("-?\\d+(\\.\\d+)?"); //match a number with optional '-' and decimal. } public static void setConfigSectionRedirect(String name, String redirectTo) { if (LOG.isDebugEnabled()) { LOG.debug("setConfigSectionRedirect({}, {})", name, redirectTo); } if (name != null) { if (redirectTo != null) { CONFIG_SECTION_REDIRECTS.put(name, redirectTo); } else { CONFIG_SECTION_REDIRECTS.remove(name); } } } private static String getConfigSectionRedirect(String name) { return name != null ? CONFIG_SECTION_REDIRECTS.get(name) : null; } }