/* * 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.storm.client; import java.io.IOException; 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 javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.apache.hadoop.security.KrbPasswordSaverLoginModule; import org.apache.hadoop.security.SecureClientLogin; import org.apache.hadoop.security.authentication.util.KerberosUtil; import org.apache.log4j.Logger; import org.apache.ranger.plugin.client.BaseClient; import org.apache.ranger.plugin.client.HadoopException; import org.apache.ranger.services.storm.client.json.model.Topology; import org.apache.ranger.services.storm.client.json.model.TopologyListResponse; 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; public class StormClient { private static final Logger LOG = Logger.getLogger(StormClient.class); private static final String EXPECTED_MIME_TYPE = "application/json"; private static final String TOPOLOGY_LIST_API_ENDPOINT = "/api/v1/topology/summary"; 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."; String stormUIUrl; String userName; String password; String lookupPrincipal; String lookupKeytab; String nameRules; public StormClient(String aStormUIUrl, String aUserName, String aPassword, String lookupPrincipal, String lookupKeytab, String nameRules) { this.stormUIUrl = aStormUIUrl; this.userName = aUserName; this.password = aPassword; this.lookupPrincipal = lookupPrincipal; this.lookupKeytab = lookupKeytab; this.nameRules = nameRules; if (LOG.isDebugEnabled()) { LOG.debug("Storm Client is build with url [" + aStormUIUrl + "] user: [" + aUserName + "], password: [" + "" + "]"); } } public List<String> getTopologyList(final String topologyNameMatching, final List<String> stormTopologyList) { if (LOG.isDebugEnabled()) { LOG.debug("Getting Storm topology list for topologyNameMatching : " + topologyNameMatching); } final String errMsg = errMessage; PrivilegedAction<ArrayList<String>> topologyListGetter = new PrivilegedAction<ArrayList<String>>() { @Override public ArrayList<String> run() { if (stormUIUrl == null || stormUIUrl.trim().isEmpty()) { return null; } String[] stormUIUrls = stormUIUrl.trim().split("[,;]"); if (stormUIUrls == null || stormUIUrls.length == 0) { return null; } Client client = Client.create(); ClientResponse response = null; for (String currentUrl : stormUIUrls) { if (currentUrl == null || currentUrl.trim().isEmpty()) { continue; } String url = currentUrl.trim() + TOPOLOGY_LIST_API_ENDPOINT; try { response = getTopologyResponse(url, client); if (response != null) { if (response.getStatus() == 200) { break; } else { response.close(); } } } catch (Throwable t) { String msgDesc = "Exception while getting topology list." + " URL : " + url; LOG.error(msgDesc, t); } } ArrayList<String> lret = new ArrayList<String>(); try { if (response != null) { if (LOG.isDebugEnabled()) { LOG.debug("getTopologyList():response.getStatus()= " + response.getStatus()); } if (response.getStatus() == 200) { String jsonString = response.getEntity(String.class); Gson gson = new GsonBuilder().setPrettyPrinting().create(); TopologyListResponse topologyListResponse = gson.fromJson(jsonString, TopologyListResponse.class); if (topologyListResponse != null) { if (topologyListResponse.getTopologyList() != null) { for (Topology topology : topologyListResponse.getTopologyList()) { String topologyName = topology.getName(); if (stormTopologyList != null && stormTopologyList.contains(topologyName)) { continue; } if (LOG.isDebugEnabled()) { LOG.debug("getTopologyList():Found topology " + topologyName); LOG.debug("getTopologyList():topology Name=[" + topology.getName() + "], topologyNameMatching=[" + topologyNameMatching + "], existingStormTopologyList=[" + stormTopologyList + "]"); } if (topologyName != null) { if (topologyNameMatching == null || topologyNameMatching.isEmpty() || FilenameUtils.wildcardMatch(topology.getName(), topologyNameMatching + "*")) { if (LOG.isDebugEnabled()) { LOG.debug("getTopologyList():Adding topology " + topologyName); } lret.add(topologyName); } } } } } } } else { String msgDesc = "Unable to get a valid response for " + "expected mime type : [" + EXPECTED_MIME_TYPE + "] URL : " + stormUIUrl + " - got null response."; LOG.error(msgDesc); HadoopException hdpException = new HadoopException(msgDesc); hdpException.generateResponseDataMap(false, msgDesc, msgDesc + errMsg, null, null); throw hdpException; } } catch (HadoopException he) { throw he; } catch (Throwable t) { String msgDesc = "Exception while getting Storm TopologyList." + " URL : " + stormUIUrl; HadoopException hdpException = new HadoopException(msgDesc, t); LOG.error(msgDesc, t); hdpException.generateResponseDataMap(false, BaseClient.getMessage(t), msgDesc + errMsg, null, null); throw hdpException; } finally { if (response != null) { response.close(); } if (client != null) { client.destroy(); } } return lret; } private ClientResponse getTopologyResponse(String url, Client client) { if (LOG.isDebugEnabled()) { LOG.debug("getTopologyResponse():calling " + url); } WebResource webResource = client.resource(url); ClientResponse response = webResource.accept(EXPECTED_MIME_TYPE).get(ClientResponse.class); if (response != null) { if (LOG.isDebugEnabled()) { LOG.debug("getTopologyResponse():response.getStatus()= " + response.getStatus()); } if (response.getStatus() != 200) { LOG.info("getTopologyResponse():response.getStatus()= " + response.getStatus() + " for URL " + url + ", failed to get topology list"); String jsonString = response.getEntity(String.class); LOG.info(jsonString); } } return response; } }; List<String> ret = null; try { ret = executeUnderKerberos(this.userName, this.password, this.lookupPrincipal, this.lookupKeytab, this.nameRules, topologyListGetter); } catch (IOException e) { LOG.error("Unable to get Topology list from [" + stormUIUrl + "]", e); } return ret; } public static <T> T executeUnderKerberos(String userName, String password, String lookupPrincipal, String lookupKeytab, String nameRules, PrivilegedAction<T> action) throws IOException { final String errMsg = errMessage; class MySecureClientLoginConfiguration extends javax.security.auth.login.Configuration { private String userName; private String password; MySecureClientLoginConfiguration(String aUserName, String password) { this.userName = aUserName; this.password = password; } @Override public AppConfigurationEntry[] getAppConfigurationEntry( String appName) { Map<String, String> kerberosOptions = new HashMap<String, String>(); kerberosOptions.put("principal", this.userName); kerberosOptions.put("debug", "true"); kerberosOptions.put("useKeyTab", "false"); kerberosOptions.put(KrbPasswordSaverLoginModule.USERNAME_PARAM, this.userName); kerberosOptions.put(KrbPasswordSaverLoginModule.PASSWORD_PARAM, this.password); kerberosOptions.put("doNotPrompt", "false"); kerberosOptions.put("useFirstPass", "true"); kerberosOptions.put("tryFirstPass", "false"); kerberosOptions.put("storeKey", "true"); kerberosOptions.put("refreshKrb5Config", "true"); AppConfigurationEntry KEYTAB_KERBEROS_LOGIN = null; AppConfigurationEntry KERBEROS_PWD_SAVER = null; try { KEYTAB_KERBEROS_LOGIN = new AppConfigurationEntry( KerberosUtil.getKrb5LoginModuleName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, kerberosOptions); KERBEROS_PWD_SAVER = new AppConfigurationEntry(KrbPasswordSaverLoginModule.class.getName(), LoginModuleControlFlag.REQUIRED, kerberosOptions); } catch (IllegalArgumentException e) { String msgDesc = "executeUnderKerberos: Exception while getting Storm TopologyList."; HadoopException hdpException = new HadoopException(msgDesc, e); LOG.error(msgDesc, e); hdpException.generateResponseDataMap(false, BaseClient.getMessage(e), msgDesc + errMsg, null, null); throw hdpException; } LOG.debug("getAppConfigurationEntry():" + kerberosOptions.get("principal")); return new AppConfigurationEntry[] { KERBEROS_PWD_SAVER, KEYTAB_KERBEROS_LOGIN }; } }; T ret = null; Subject subject = null; LoginContext loginContext = null; try { Subject loginSubj = null; if(!StringUtils.isEmpty(lookupPrincipal) && !StringUtils.isEmpty(lookupKeytab)){ LOG.info("Init Lookup Login: security enabled, using lookupPrincipal/lookupKeytab"); if(StringUtils.isEmpty(nameRules)){ nameRules = "DEFAULT"; } loginSubj = SecureClientLogin.loginUserFromKeytab(lookupPrincipal, lookupKeytab, nameRules); }else{ subject = new Subject(); LOG.debug("executeUnderKerberos():user=" + userName + ",pass="); LOG.debug("executeUnderKerberos():Creating config.."); MySecureClientLoginConfiguration loginConf = new MySecureClientLoginConfiguration( userName, password); LOG.debug("executeUnderKerberos():Creating Context.."); loginContext = new LoginContext("hadoop-keytab-kerberos", subject, null, loginConf); LOG.debug("executeUnderKerberos():Logging in.."); loginContext.login(); LOG.info("Init Login: using username/password"); loginSubj = loginContext.getSubject(); } if (loginSubj != null) { ret = Subject.doAs(loginSubj, action); } } catch (LoginException le) { String msgDesc = "executeUnderKerberos: Login failure using given" + " configuration parameters, username : `" + userName + "`."; HadoopException hdpException = new HadoopException(msgDesc, le); LOG.error(msgDesc, le); hdpException.generateResponseDataMap(false, BaseClient.getMessage(le), msgDesc + errMsg, null, null); throw hdpException; } catch (SecurityException se) { String msgDesc = "executeUnderKerberos: Exception while getting Storm TopologyList."; HadoopException hdpException = new HadoopException(msgDesc, se); LOG.error(msgDesc, se); hdpException.generateResponseDataMap(false, BaseClient.getMessage(se), msgDesc + errMsg, null, null); throw hdpException; } finally { if (loginContext != null) { if (subject != null) { try { loginContext.logout(); } catch (LoginException e) { throw new IOException("logout failure", e); } } } } return ret; } public static Map<String, Object> connectionTest(String serviceName, Map<String, String> configs) { String errMsg = errMessage; boolean connectivityStatus = false; Map<String, Object> responseData = new HashMap<String, Object>(); StormClient stormClient = getStormClient(serviceName, configs); List<String> strList = getStormResources(stormClient, "",null); if (strList != null) { connectivityStatus = true; } if (connectivityStatus) { String successMsg = "ConnectionTest Successful"; BaseClient.generateResponseDataMap(connectivityStatus, successMsg, successMsg, null, null, responseData); } else { String failureMsg = "Unable to retrieve any topologies using given parameters."; BaseClient.generateResponseDataMap(connectivityStatus, failureMsg, failureMsg + errMsg, null, null, responseData); } return responseData; } public static StormClient getStormClient(String serviceName, Map<String, String> configs) { StormClient stormClient = null; if(LOG.isDebugEnabled()){ LOG.debug("Getting StormClient 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 stormUrl = configs.get("nimbus.url"); String stormAdminUser = configs.get("username"); String stormAdminPassword = configs.get("password"); String lookupPrincipal = configs.get("lookupprincipal"); String lookupKeytab = configs.get("lookupkeytab"); String nameRules = configs.get("namerules"); stormClient = new StormClient(stormUrl, stormAdminUser, stormAdminPassword, lookupPrincipal, lookupKeytab, nameRules); } return stormClient; } public static List<String> getStormResources(final StormClient stormClient, String topologyName, List<String> stormTopologyList) { List<String> resultList = new ArrayList<String>(); String errMsg = errMessage; try { if (stormClient == null) { String msgDesc = "Unable to get Storm resources: StormClient is null."; LOG.error(msgDesc); HadoopException hdpException = new HadoopException(msgDesc); hdpException.generateResponseDataMap(false, msgDesc, msgDesc + errMsg, null, null); throw hdpException; } if (topologyName != null) { String finalTopologyNameMatching = topologyName.trim(); resultList = stormClient .getTopologyList(finalTopologyNameMatching,stormTopologyList); if (resultList != null) { LOG.debug("Returning list of " + resultList.size() + " topologies"); } } } catch (HadoopException he) { throw he; } catch (Exception e) { String msgDesc = "getStormResources: Unable to get Storm resources."; LOG.error(msgDesc, e); HadoopException hdpException = new HadoopException(msgDesc); hdpException.generateResponseDataMap(false, BaseClient.getMessage(e), msgDesc + errMsg, null, null); throw hdpException; } return resultList; } }