/**
* 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 com.huawei.streaming.storm;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.Properties;
import java.util.UUID;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.google.common.io.Files;
import com.huawei.streaming.config.StreamingConfig;
import com.huawei.streaming.exception.ErrorCode;
import com.huawei.streaming.exception.StreamingException;
/**
* Streaming访问Zookeeper安全
*
*/
public class KerberosSecurity implements StreamingSecurity
{
/**
* The jass.conf for zookeeper client security login.
*/
public static final String ZOOKEEPER_AUTH_JASSCONF = "java.security.auth.login.config";
/**
* Zookeeper quorum principal.
*/
public static final String ZOOKEEPER_AUTH_PRINCIPAL = "zookeeper.server.principal";
/**
* java security krb5 file path
*/
public static final String JAVA_SECURITY_KRB5_CONF = "java.security.krb5.conf";
private static final Logger LOG = LoggerFactory.getLogger(KerberosSecurity.class);
private static final String DEFAULT_STRING_CHARSET = "UTF-8";
private static final Charset DEFAULT_CHARSET = Charset.forName(DEFAULT_STRING_CHARSET);
private static final String STREAMING_JAAS_POSTFIX = ".streaming.jaas.conf";
private static final String LINE_SEPARATOR = IOUtils.LINE_SEPARATOR;
private boolean useKeyTab = true;
private String keyTabPath;
private String userPrincipal;
private String zookeeperPrincipal;
private boolean useTicketCache = false;
private boolean storeKey = true;
private boolean debug = false;
private String jaasPath;
private StreamingConfig conf;
private Properties backupSecurityConf = null;
private String krbFilePath = null;
/**
* 构建JaasConf文件对象
*
*/
public KerberosSecurity(StreamingConfig config)
throws StreamingException
{
initParameters(config);
resetSecurityType();
this.conf = config;
this.jaasPath = createJaasPath();
this.backupSecurityConf = new Properties();
backupSecurityConfs();
}
private void backupSecurityConfs()
{
this.backupSecurityProperties(ZOOKEEPER_AUTH_PRINCIPAL);
this.backupSecurityProperties(ZOOKEEPER_AUTH_JASSCONF);
this.backupSecurityProperties(JAVA_SECURITY_KRB5_CONF);
}
private void initParameters(StreamingConfig config)
throws StreamingException
{
readZooKeeperPrincipal(config);
readUserPrincipal(config);
readKeyTabPath(config);
readKrbConfPath(config);
}
private void resetSecurityType()
throws StreamingException
{
if (userPrincipal == null && keyTabPath == null)
{
LOG.info("Use ticket cache security.");
this.useKeyTab = false;
this.useTicketCache = true;
return;
}
LOG.info("Use keytab security.");
if (Strings.isNullOrEmpty(userPrincipal))
{
StreamingException exception =
new StreamingException(ErrorCode.CONFIG_NOT_FOUND, StreamingConfig.STREAMING_SECURITY_USER_PRINCIPAL);
LOG.error("User principal error.", exception);
throw exception;
}
if (Strings.isNullOrEmpty(keyTabPath))
{
StreamingException exception =
new StreamingException(ErrorCode.CONFIG_NOT_FOUND, StreamingConfig.STREAMING_SECURITY_KEYTAB_PATH);
LOG.error("Keytab file error.", exception);
throw exception;
}
}
/**
* 初始化安全配置
*
*/
@Override
public void initSecurity()
throws StreamingException
{
writeJaasFile();
initAuthToSysProperty();
}
/**
* 销毁安全配置
*
*/
@Override
public void destroySecurity()
throws StreamingException
{
restoreSecurityConf();
deleteJaasFile();
}
private void restoreSecurityConf()
{
this.restoreSecurityProperties(ZOOKEEPER_AUTH_PRINCIPAL);
this.restoreSecurityProperties(ZOOKEEPER_AUTH_JASSCONF);
this.restoreSecurityProperties(JAVA_SECURITY_KRB5_CONF);
}
/**
* 删除jaas文件
*
*/
private void deleteJaasFile()
throws StreamingException
{
try
{
String tmpDir = new File(conf.getStringValue(StreamingConfig.STREAMING_TEMPLATE_DIRECTORY)).getCanonicalPath();
File file = new File(jaasPath).getCanonicalFile();
if (!file.getPath().startsWith(tmpDir))
{
LOG.error("Invalid jaas path, not in config tmp path.");
throw new StreamingException(ErrorCode.SECURITY_INNER_ERROR);
}
if (file.isFile())
{
FileUtils.forceDelete(new File(jaasPath));
}
}
catch (IOException e)
{
LOG.error("Failed to delete jaas file.");
throw new StreamingException(ErrorCode.UNKNOWN_SERVER_COMMON_ERROR);
}
catch (SecurityException e1)
{
StreamingException exception = new StreamingException(ErrorCode.SECURITY_INNER_ERROR);
LOG.error("Failed to get canonical pathname for cannot be accessed.", exception);
throw exception;
}
}
/**
* 初始化安全相关系统变量
*
*/
private void initAuthToSysProperty()
throws StreamingException
{
String zkJassConf = decodePath();
System.setProperty(ZOOKEEPER_AUTH_PRINCIPAL, zookeeperPrincipal);
System.setProperty(ZOOKEEPER_AUTH_JASSCONF, zkJassConf);
if (this.krbFilePath != null)
{
System.setProperty(JAVA_SECURITY_KRB5_CONF, krbFilePath);
}
}
/**
* 文件地址重新编码,防止文件地址中有空格之类
*
*/
private String decodePath()
throws StreamingException
{
try
{
return URLDecoder.decode(jaasPath, DEFAULT_STRING_CHARSET);
}
catch (UnsupportedEncodingException e)
{
LOG.error("Unsupported encode, failed to decode jaas path {}.", jaasPath);
throw new StreamingException(ErrorCode.UNKNOWN_SERVER_COMMON_ERROR);
}
}
/**
* 写入jaas文件
*
*/
private void writeJaasFile()
throws StreamingException
{
try
{
String jaasContext = createJaasContext();
Files.write(jaasContext, new File(jaasPath), DEFAULT_CHARSET);
}
catch (IOException e)
{
LOG.error("Failed to create jaas file.");
throw new StreamingException(ErrorCode.UNKNOWN_SERVER_COMMON_ERROR);
}
}
/**
* 创建jaas文件内容
*
*/
private String createJaasContext()
{
if (useKeyTab)
{
return createKeyTabContext();
}
return createCacheContext();
}
/*
* 创建人机账户登陆的jaas文件内容
*/
private String createCacheContext()
{
StringBuilder sb = new StringBuilder();
sb.append("Client {").append(LINE_SEPARATOR);
sb.append("com.sun.security.auth.module.Krb5LoginModule required").append(LINE_SEPARATOR);
sb.append("useKeyTab=" + useKeyTab).append(LINE_SEPARATOR);
sb.append("useTicketCache=" + useTicketCache + ";").append(LINE_SEPARATOR);
sb.append("};").append(LINE_SEPARATOR);
sb.append("StormClient {").append(LINE_SEPARATOR);
sb.append("com.sun.security.auth.module.Krb5LoginModule required").append(LINE_SEPARATOR);
sb.append("useKeyTab=" + useKeyTab).append(LINE_SEPARATOR);
sb.append("useTicketCache=" + useTicketCache + ";").append(LINE_SEPARATOR);
sb.append("};");
return sb.toString();
}
/*
* 创建机机账户登陆的jaas文件内容
*/
private String createKeyTabContext()
{
StringBuilder sb = new StringBuilder();
sb.append("Client {").append(LINE_SEPARATOR);
sb.append("com.sun.security.auth.module.Krb5LoginModule required").append(LINE_SEPARATOR);
sb.append("useKeyTab=" + useKeyTab).append(LINE_SEPARATOR);
sb.append("keyTab=\"" + keyTabPath + "\"").append(LINE_SEPARATOR);
sb.append("principal=\"" + userPrincipal + "\"").append(LINE_SEPARATOR);
sb.append("useTicketCache=" + useTicketCache).append(LINE_SEPARATOR);
sb.append("storeKey=" + storeKey).append(LINE_SEPARATOR);
sb.append("debug=" + debug + ";").append(LINE_SEPARATOR);
sb.append("};").append(LINE_SEPARATOR);
sb.append("StormClient {").append(LINE_SEPARATOR);
sb.append("com.sun.security.auth.module.Krb5LoginModule required").append(LINE_SEPARATOR);
sb.append("useKeyTab=" + useKeyTab).append(LINE_SEPARATOR);
sb.append("keyTab=\"" + keyTabPath + "\"").append(LINE_SEPARATOR);
sb.append("principal=\"" + userPrincipal + "\"").append(LINE_SEPARATOR);
sb.append("useTicketCache=" + useTicketCache).append(LINE_SEPARATOR);
sb.append("storeKey=" + storeKey).append(LINE_SEPARATOR);
sb.append("debug=" + debug + ";").append(LINE_SEPARATOR);
sb.append("};");
return sb.toString();
}
/**
* 读取zookeeper的principal
*
*/
private void readZooKeeperPrincipal(StreamingConfig config)
throws StreamingException
{
Object zkPrincipal = config.get(StreamingConfig.STREAMING_SECURITY_ZOOKEEPER_PRINCIPAL);
if (zkPrincipal == null)
{
StreamingException exception =
new StreamingException(ErrorCode.CONFIG_NOT_FOUND,
StreamingConfig.STREAMING_SECURITY_ZOOKEEPER_PRINCIPAL);
LOG.error("Can't find zk principal in config.", exception);
throw exception;
}
this.zookeeperPrincipal = zkPrincipal.toString();
}
/**
* 读取用户principal,以此作为是否启用安全的标志
*
*/
private void readUserPrincipal(StreamingConfig config)
throws StreamingException
{
Object principal = config.get(StreamingConfig.STREAMING_SECURITY_USER_PRINCIPAL);
this.userPrincipal = principal == null ? null : principal.toString();
}
/**
* 从配置属性中读取keytab文件的地址
* 如果keytab文件不存在,就说明使用人机账户登陆
*
*/
private void readKeyTabPath(StreamingConfig config)
throws StreamingException
{
Object keyTablePath = config.get(StreamingConfig.STREAMING_SECURITY_KEYTAB_PATH);
this.keyTabPath = keyTablePath == null ? null : formatPath(getKeyTablePath(keyTablePath.toString()));
}
/**
* 从配置属性中读取krb.conf文件的地址
*
*/
private void readKrbConfPath(StreamingConfig config)
throws StreamingException
{
Object krbPath = config.get(StreamingConfig.STREAMING_SECURITY_KRBCONF_PATH);
this.krbFilePath = krbPath == null ? null : formatPath(getKeyTablePath(krbPath.toString()));
}
private String createJaasPath() throws StreamingException
{
UUID uuid = UUID.randomUUID();
String randomName = uuid.toString().replace("-", "");
String tmpDir = conf.getStringValue(StreamingConfig.STREAMING_TEMPLATE_DIRECTORY);
try
{
tmpDir = new File(tmpDir).getCanonicalPath();
}
catch (IOException e)
{
StreamingException exception = new StreamingException(ErrorCode.SECURITY_INNER_ERROR);
LOG.error("Failed to get canonical pathname for io error.", exception);
throw exception;
}
catch (SecurityException e1)
{
StreamingException exception = new StreamingException(ErrorCode.SECURITY_INNER_ERROR);
LOG.error("Failed to get canonical pathname for cannot be accessed.", exception);
throw exception;
}
String jaasPath = tmpDir + File.separator + randomName + STREAMING_JAAS_POSTFIX;
return jaasPath;
}
private String getKeyTablePath(String keyTabPath)
throws StreamingException
{
try
{
return new File(keyTabPath).getCanonicalPath();
}
catch (IOException e)
{
StreamingException exception = new StreamingException(ErrorCode.SECURITY_KEYTAB_PATH_ERROR, keyTabPath);
LOG.error("Failed to get canonical pathname for io error.", exception);
throw exception;
}
catch (SecurityException e1)
{
StreamingException exception = new StreamingException(ErrorCode.SECURITY_KEYTAB_PATH_ERROR, keyTabPath);
LOG.error("Failed to get canonical pathname for cannot be accessed.", exception);
throw exception;
}
}
private String formatPath(String path)
{
return path.replace("\\", "\\\\");
}
private void backupSecurityProperties(String propertyKey)
{
if (System.getProperty(propertyKey) == null)
{
backupSecurityConf.put(propertyKey, "");
}
else
{
backupSecurityConf.put(propertyKey, System.getProperty(propertyKey));
}
}
private void restoreSecurityProperties(String propertyKey)
{
if (Strings.isNullOrEmpty(backupSecurityConf.getProperty(propertyKey)))
{
System.clearProperty(propertyKey);
}
else
{
System.setProperty(propertyKey, backupSecurityConf.getProperty(propertyKey));
}
}
}