blob: c65048facf6860a7ea8737e9650614f1d488b5c3 [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.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;
}
}