/*
* 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.ranger.services.kms.client;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.security.auth.Subject;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.security.HadoopKerberosName;
import org.apache.hadoop.security.ProviderUtils;
import org.apache.hadoop.security.SecureClientLogin;
import org.apache.log4j.Logger;
import org.apache.ranger.plugin.client.BaseClient;
import org.apache.ranger.plugin.util.PasswordUtils;
import org.apache.ranger.plugin.client.HadoopException;
import org.apache.ranger.services.kms.client.KMSClient;
import com.google.common.base.Strings;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
public class KMSClient {
private static final Logger LOG = Logger.getLogger(KMSClient.class);
private static final String EXPECTED_MIME_TYPE = "application/json";
private static final String KMS_LIST_API_ENDPOINT = "v1/keys/names"; // GET
private static final String errMessage = " You can still save the repository and start creating "
+ "policies, but you would not be able to use autocomplete for "
+ "resource names. Check ranger_admin.log for more info.";
private static final String AUTH_TYPE_KERBEROS = "kerberos";
String provider;
String username;
String password;
String rangerPrincipal;
String rangerKeytab;
String nameRules;
String authType;
public KMSClient(String provider, String username, String password, String rangerPrincipal, String rangerKeytab, String nameRules, String authType) {
this.provider = provider;
this.username = username;
this.password = password;
this.rangerPrincipal = rangerPrincipal;
this.rangerKeytab = rangerKeytab;
this.nameRules = nameRules;
this.authType = authType;
if (LOG.isDebugEnabled()) {
LOG.debug("Kms Client is build with url [" + provider + "] user: ["
+ username + "]");
}
}
private String[] createProvider(String uri) throws IOException,
URISyntaxException {
URI providerUri = new URI(uri);
URL origUrl = new URL(extractKMSPath(providerUri).toString());
String authority = origUrl.getAuthority();
// check for ';' which delimits the backup hosts
if (Strings.isNullOrEmpty(authority)) {
throw new IOException("No valid authority in kms uri [" + origUrl
+ "]");
}
// Check if port is present in authority
// In the current scheme, all hosts have to run on the same port
int port = -1;
String hostsPart = authority;
if (authority.contains(":")) {
String[] t = authority.split(":");
try {
port = Integer.parseInt(t[1]);
} catch (Exception e) {
throw new IOException("Could not parse port in kms uri ["
+ origUrl + "]");
}
hostsPart = t[0];
}
return createProvider(providerUri, origUrl, port, hostsPart);
}
private static Path extractKMSPath(URI uri) throws MalformedURLException,
IOException {
return ProviderUtils.unnestUri(uri);
}
private String[] createProvider(URI providerUri, URL origUrl, int port,
String hostsPart) throws IOException {
String[] hosts = hostsPart.split(";");
String[] providers = new String[hosts.length];
if (hosts.length == 1) {
providers[0] = origUrl.toString();
} else {
for (int i = 0; i < hosts.length; i++) {
try {
String url = origUrl.getProtocol() + "://" + hosts[i] + ":"
+ port + origUrl.getPath();
providers[i] = new URI(url).toString();
} catch (URISyntaxException e) {
throw new IOException("Could not Prase KMS URL..", e);
}
}
}
return providers;
}
public List<String> getKeyList(final String keyNameMatching,
final List<String> existingKeyList) {
String providers[] = null;
try {
providers = createProvider(provider);
} catch (IOException | URISyntaxException e) {
return null;
}
final String errMsg = errMessage;
List<String> lret = null;
for (int i = 0; i < providers.length; i++) {
lret = new ArrayList<String>();
if (LOG.isDebugEnabled()) {
LOG.debug("Getting Kms Key list for keyNameMatching : " + keyNameMatching);
}
String uri = providers[i] + (providers[i].endsWith("/") ? KMS_LIST_API_ENDPOINT : ("/" + KMS_LIST_API_ENDPOINT));
Client client = null;
ClientResponse response = null;
boolean isKerberos = false;
try {
ClientConfig cc = new DefaultClientConfig();
cc.getProperties().put(ClientConfig.PROPERTY_FOLLOW_REDIRECTS, true);
client = Client.create(cc);
if(authType != null && authType.equalsIgnoreCase(AUTH_TYPE_KERBEROS)){
isKerberos = true;
}
Subject sub = new Subject();
if(!isKerberos){
uri = uri.concat("?user.name="+username);
WebResource webResource = client.resource(uri);
response = webResource.accept(EXPECTED_MIME_TYPE).get(ClientResponse.class);
LOG.info("Init Login: security not enabled, using username");
sub = SecureClientLogin.login(username);
}else{
if(!StringUtils.isEmpty(rangerPrincipal) && !StringUtils.isEmpty(rangerKeytab)){
LOG.info("Init Lookup Login: security enabled, using rangerPrincipal/rangerKeytab");
if(StringUtils.isEmpty(nameRules)){
nameRules = "DEFAULT";
}
String shortName = new HadoopKerberosName(rangerPrincipal).getShortName();
uri = uri.concat("?doAs="+shortName);
sub = SecureClientLogin.loginUserFromKeytab(rangerPrincipal, rangerKeytab, nameRules);
}
else{
LOG.info("Init Login: using username/password");
String shortName = new HadoopKerberosName(username).getShortName();
uri = uri.concat("?doAs="+shortName);
String decryptedPwd = PasswordUtils.decryptPassword(password);
sub = SecureClientLogin.loginUserWithPassword(username, decryptedPwd);
}
}
final WebResource webResource = client.resource(uri);
response = Subject.doAs(sub, new PrivilegedAction<ClientResponse>() {
@Override
public ClientResponse run() {
return webResource.accept(EXPECTED_MIME_TYPE).get(ClientResponse.class);
}
});
if (LOG.isDebugEnabled()) {
LOG.debug("getKeyList():calling " + uri);
}
if (response != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("getKeyList():response.getStatus()= "
+ response.getStatus());
}
if (response.getStatus() == 200) {
String jsonString = response.getEntity(String.class);
Gson gson = new GsonBuilder().setPrettyPrinting()
.create();
@SuppressWarnings("unchecked")
List<String> keys = gson.fromJson(jsonString,
List.class);
if (keys != null) {
for (String key : keys) {
if (existingKeyList != null
&& existingKeyList.contains(key)) {
continue;
}
if (keyNameMatching == null
|| keyNameMatching.isEmpty()
|| key.startsWith(keyNameMatching)) {
if (LOG.isDebugEnabled()) {
LOG.debug("getKeyList():Adding kmsKey "
+ key);
}
lret.add(key);
}
}
return lret;
}
} else if (response.getStatus() == 401) {
LOG.info("getKeyList():response.getStatus()= "
+ response.getStatus() + " for URL " + uri
+ ", so returning null list");
String msgDesc = response.getEntity(String.class);
HadoopException hdpException = new HadoopException(msgDesc);
hdpException.generateResponseDataMap(false, msgDesc,
msgDesc + errMsg, null, null);
lret = null;
throw hdpException;
} else if (response.getStatus() == 403) {
LOG.info("getKeyList():response.getStatus()= "
+ response.getStatus() + " for URL " + uri
+ ", so returning null list");
String msgDesc = response.getEntity(String.class);
HadoopException hdpException = new HadoopException(msgDesc);
hdpException.generateResponseDataMap(false, msgDesc,
msgDesc + errMsg, null, null);
lret = null;
throw hdpException;
} else {
LOG.info("getKeyList():response.getStatus()= "
+ response.getStatus() + " for URL " + uri
+ ", so returning null list");
String jsonString = response.getEntity(String.class);
LOG.info(jsonString);
lret = null;
}
} else {
String msgDesc = "Unable to get a valid response for "
+ "expected mime type : [" + EXPECTED_MIME_TYPE
+ "] URL : " + uri + " - got null response.";
LOG.error(msgDesc);
HadoopException hdpException = new HadoopException(msgDesc);
hdpException.generateResponseDataMap(false, msgDesc,
msgDesc + errMsg, null, null);
lret = null;
throw hdpException;
}
} catch (HadoopException he) {
lret = null;
throw he;
} catch (Throwable t) {
String msgDesc = "Exception while getting Kms Key List. URL : "
+ uri;
HadoopException hdpException = new HadoopException(msgDesc, t);
LOG.error(msgDesc, t);
hdpException.generateResponseDataMap(false,
BaseClient.getMessage(t), msgDesc + errMsg, null, null);
lret = null;
throw hdpException;
} finally {
if (response != null) {
response.close();
}
if (client != null) {
client.destroy();
}
if(lret == null){
if (i != providers.length - 1)
continue;
}
}
}
return lret;
}
public static Map<String, Object> testConnection(String serviceName,
Map<String, String> configs) {
List<String> strList = new ArrayList<String>();
String errMsg = errMessage;
boolean connectivityStatus = false;
Map<String, Object> responseData = new HashMap<String, Object>();
KMSClient kmsClient = getKmsClient(serviceName, configs);
strList = getKmsKey(kmsClient, "", null);
if (strList != null) {
connectivityStatus = true;
}
if (connectivityStatus) {
String successMsg = "TestConnection Successful";
BaseClient.generateResponseDataMap(connectivityStatus, successMsg,
successMsg, null, null, responseData);
} else {
String failureMsg = "Unable to retrieve any Kms Key using given URL.";
BaseClient.generateResponseDataMap(connectivityStatus, failureMsg,
failureMsg + errMsg, null, null, responseData);
}
return responseData;
}
public static KMSClient getKmsClient(String serviceName,
Map<String, String> configs) {
KMSClient kmsClient = null;
if (LOG.isDebugEnabled()) {
LOG.debug("Getting KmsClient for datasource: " + serviceName);
LOG.debug("configMap: " + configs);
}
String errMsg = errMessage;
if (configs == null || configs.isEmpty()) {
String msgDesc = "Could not connect as Connection ConfigMap is empty.";
LOG.error(msgDesc);
HadoopException hdpException = new HadoopException(msgDesc);
hdpException.generateResponseDataMap(false, msgDesc, msgDesc
+ errMsg, null, null);
throw hdpException;
} else {
String kmsUrl = configs.get("provider");
String kmsUserName = configs.get("username");
String kmsPassWord = configs.get("password");
String rangerPrincipal = configs.get("rangerprincipal");
String rangerKeytab = configs.get("rangerkeytab");
String nameRules = configs.get("namerules");
String authType = configs.get("authtype");
kmsClient = new KMSClient(kmsUrl, kmsUserName, kmsPassWord, rangerPrincipal, rangerKeytab, nameRules, authType);
}
return kmsClient;
}
public static List<String> getKmsKey(final KMSClient kmsClient,
String keyName, List<String> existingKeyName) {
List<String> resultList = new ArrayList<String>();
String errMsg = errMessage;
try {
if (kmsClient == null) {
String msgDesc = "Unable to get Kms Key : KmsClient is null.";
LOG.error(msgDesc);
HadoopException hdpException = new HadoopException(msgDesc);
hdpException.generateResponseDataMap(false, msgDesc, msgDesc
+ errMsg, null, null);
throw hdpException;
}
if (keyName != null) {
String finalkmsKeyName = keyName.trim();
resultList = kmsClient.getKeyList(finalkmsKeyName,
existingKeyName);
if (resultList != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Returning list of " + resultList.size()
+ " Kms Keys");
}
}
}
} catch (HadoopException he) {
resultList = null;
throw he;
} catch (Exception e) {
String msgDesc = "Unable to get a valid response from the provider : "+e.getMessage();
LOG.error(msgDesc, e);
HadoopException hdpException = new HadoopException(msgDesc);
hdpException.generateResponseDataMap(false, msgDesc, msgDesc
+ errMsg, null, null);
resultList = null;
throw hdpException;
}
return resultList;
}
}