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