/* * 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.ambari.server.checks; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.ambari.server.AmbariException; import org.apache.ambari.server.configuration.ComponentSSLConfiguration; import org.apache.ambari.server.controller.PrereqCheckRequest; import org.apache.ambari.server.controller.internal.URLStreamProvider; import org.apache.ambari.server.state.Cluster; import org.apache.ambari.server.state.StackId; import org.apache.ambari.server.state.stack.PrereqCheckStatus; import org.apache.ambari.server.state.stack.PrerequisiteCheck; import org.apache.ambari.server.state.stack.upgrade.UpgradeType; import org.apache.ambari.server.utils.VersionUtils; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; import com.google.inject.Singleton; /** * Used to make sure that the password in Ambari matches that for Ranger, in case the * user had changed the password using the Ranger UI. */ @Singleton @UpgradeCheck( group = UpgradeCheckGroup.CONFIGURATION_WARNING, order = 23.0f, required = { UpgradeType.ROLLING, UpgradeType.NON_ROLLING, UpgradeType.HOST_ORDERED }) public class RangerPasswordCheck extends AbstractCheckDescriptor { private static final Logger LOG = LoggerFactory.getLogger(RangerPasswordCheck.class); static final String KEY_RANGER_PASSWORD_MISMATCH = "could_not_verify_password"; static final String KEY_RANGER_COULD_NOT_ACCESS = "could_not_access"; static final String KEY_RANGER_UNKNOWN_RESPONSE = "unknown_response"; static final String KEY_RANGER_USERS_ELEMENT_MISSING = "missing_vxusers"; static final String KEY_RANGER_OTHER_ISSUE = "invalid_response"; static final String KEY_RANGER_CONFIG_MISSING = "missing_config"; /** * Constructor. */ public RangerPasswordCheck() { super(CheckDescription.SERVICES_RANGER_PASSWORD_VERIFY); } /** * Verifies that the check can be run. If the stack is HDP and 2.3 or higher, allow * this to run. If the stack is not HDP, the check should run. */ @Override public boolean isApplicable(PrereqCheckRequest request) throws AmbariException { if (!super.isApplicable(request, Arrays.asList("RANGER"), true)) { return false; } final Cluster cluster = clustersProvider.get().getCluster(request.getClusterName()); StackId clusterStackId = cluster.getCurrentStackVersion(); if (clusterStackId.getStackName().equals("HDP")) { String sourceVersion = request.getSourceStackId().getStackVersion(); return VersionUtils.compareVersions(sourceVersion, "2.3.0.0") >= 0; } return true; } @Override public void perform(PrerequisiteCheck check, PrereqCheckRequest request) throws AmbariException { // !!! ComponentSSLConfiguration is an old-school singleton which doesn't // get initialized until after Guice is done - because this check is bound // as a singleton via Guice, we can't initialize the stream provider in the // constructor since the SSL configuration instance hasn't been initialized URLStreamProvider streamProvider = new URLStreamProvider(2000, 2000, ComponentSSLConfiguration.instance()); String rangerUrl = checkEmpty("admin-properties", "policymgr_external_url", check, request); if (null == rangerUrl) { // !!! check results already filled return; } String adminUsername = checkEmpty("ranger-env", "admin_username", check, request); if (null == adminUsername) { return; } String adminPassword = checkEmpty("ranger-env", "admin_password", check, request); if (null == adminPassword) { return; } String rangerAdminUsername = checkEmpty("ranger-env", "ranger_admin_username", check, request); if (null == rangerAdminUsername) { return; } String rangerAdminPassword = checkEmpty("ranger-env", "ranger_admin_password", check, request); if (null == rangerAdminPassword) { return; } if (rangerUrl.endsWith("/")) { rangerUrl = rangerUrl.substring(0, rangerUrl.length()-1); } String rangerAuthUrl = String.format("%s/%s", rangerUrl, "service/public/api/repository/count"); String rangerUserUrl = String.format("%s/%s", rangerUrl, "service/xusers/users"); List<String> failReasons = new ArrayList<>(); List<String> warnReasons = new ArrayList<>(); // !!! first, just try the service with the admin credentials try { int response = checkLogin(streamProvider, rangerAuthUrl, adminUsername, adminPassword); switch (response) { case 401: { String reason = getFailReason(KEY_RANGER_PASSWORD_MISMATCH, check, request); failReasons.add(String.format(reason, adminUsername)); break; } case 200: { break; } default: { String reason = getFailReason(KEY_RANGER_UNKNOWN_RESPONSE, check, request); warnReasons.add(String.format(reason, adminUsername, response, rangerAuthUrl)); break; } } } catch (IOException e) { LOG.warn("Could not access the url {}. Message: {}", rangerAuthUrl, e.getMessage(), e); LOG.debug("Could not access the url {}. Message: {}", rangerAuthUrl, e.getMessage()); String reason = getFailReason(KEY_RANGER_COULD_NOT_ACCESS, check, request); warnReasons.add(String.format(reason, adminUsername, rangerAuthUrl, e.getMessage())); } // !!! shortcut when something happened with the admin user if (!failReasons.isEmpty()) { check.setFailReason(StringUtils.join(failReasons, '\n')); check.getFailedOn().add("RANGER"); check.setStatus(PrereqCheckStatus.FAIL); return; } else if (!warnReasons.isEmpty()) { check.setFailReason(StringUtils.join(warnReasons, '\n')); check.getFailedOn().add("RANGER"); check.setStatus(PrereqCheckStatus.WARNING); return; } // !!! Check for the user, capture exceptions as a warning. boolean hasUser = checkRangerUser(streamProvider, rangerUserUrl, adminUsername, adminPassword, rangerAdminUsername, check, request, warnReasons); if (hasUser) { // !!! try credentials for specific user try { int response = checkLogin(streamProvider, rangerAuthUrl, rangerAdminUsername, rangerAdminPassword); switch (response) { case 401: { String reason = getFailReason(KEY_RANGER_PASSWORD_MISMATCH, check, request); failReasons.add(String.format(reason, rangerAdminUsername)); break; } case 200: { break; } default: { String reason = getFailReason(KEY_RANGER_UNKNOWN_RESPONSE, check, request); warnReasons.add(String.format(reason, rangerAdminUsername, response, rangerAuthUrl)); break; } } } catch (IOException e) { LOG.warn("Could not access the url {}. Message: {}", rangerAuthUrl, e.getMessage()); LOG.debug("Could not access the url {}. Message: {}", rangerAuthUrl, e.getMessage(), e); String reason = getFailReason(KEY_RANGER_COULD_NOT_ACCESS, check, request); warnReasons.add(String.format(reason, rangerAdminUsername, rangerAuthUrl, e.getMessage())); } } if (!failReasons.isEmpty()) { check.setFailReason(StringUtils.join(failReasons, '\n')); check.getFailedOn().add("RANGER"); check.setStatus(PrereqCheckStatus.FAIL); } else if (!warnReasons.isEmpty()) { check.setFailReason(StringUtils.join(warnReasons, '\n')); check.getFailedOn().add("RANGER"); check.setStatus(PrereqCheckStatus.WARNING); } else { check.setStatus(PrereqCheckStatus.PASS); } } /** * Checks the credentials. From the Ranger team, bad credentials result in a * successful call, but the Ranger admin server will redirect to the home * page. They recommend parsing the result. If it parses, the credentials are * good, otherwise consider the user as unverified. * * @param streamProvider * the stream provider to use when making requests * @param url * the url to check * @param username * the user to check * @param password * the password to check * @return the http response code * @throws IOException * if there was an error reading the response */ private int checkLogin(URLStreamProvider streamProvider, String url, String username, String password) throws IOException { Map<String, List<String>> headers = getHeaders(username, password); HttpURLConnection conn = streamProvider.processURL(url, "GET", (InputStream) null, headers); int result = conn.getResponseCode(); // !!! see javadoc if (result == 200) { Gson gson = new Gson(); try { gson.fromJson(new InputStreamReader(conn.getInputStream()), Object.class); } catch (Exception e) { result = 401; } } return result; } /** * @param streamProvider * the stream provider to use when making requests * @param rangerUserUrl * the url to use when looking for the user * @param username * the username to use when loading the url * @param password * the password for the user url * @param userToSearch * the user to look for * @param check * the check instance for loading failure reasons * @param request * the request instance for loading failure reasons * @param warnReasons * the list of warn reasons to fill * @return {@code true} if the user was found */ private boolean checkRangerUser(URLStreamProvider streamProvider, String rangerUserUrl, String username, String password, String userToSearch, PrerequisiteCheck check, PrereqCheckRequest request, List<String> warnReasons) { String url = String.format("%s?name=%s", rangerUserUrl, userToSearch); Map<String, List<String>> headers = getHeaders(username, password); try { HttpURLConnection conn = streamProvider.processURL(url, "GET", (InputStream) null, headers); int result = conn.getResponseCode(); if (result == 200) { Gson gson = new Gson(); Object o = gson.fromJson(new InputStreamReader(conn.getInputStream()), Object.class); Map<?, ?> map = (Map<?,?>) o; if (!map.containsKey("vXUsers")) { String reason = getFailReason(KEY_RANGER_USERS_ELEMENT_MISSING, check, request); warnReasons.add(String.format(reason, url)); return false; } @SuppressWarnings("unchecked") List<Map<?, ?>> list = (List<Map<?, ?>>) map.get("vXUsers"); for (Map<?, ?> listMap : list) { if (listMap.containsKey("name") && listMap.get("name").equals(userToSearch)) { return true; } } } } catch (IOException e) { LOG.warn("Could not determine user {}. Error is {}", userToSearch, e.getMessage()); LOG.debug("Could not determine user {}. Error is {}", userToSearch, e.getMessage(), e); String reason = getFailReason(KEY_RANGER_COULD_NOT_ACCESS, check, request); warnReasons.add(String.format(reason, username, url, e.getMessage())); } catch (Exception e) { LOG.warn("Could not determine user {}. Error is {}", userToSearch, e.getMessage()); LOG.debug("Could not determine user {}. Error is {}", userToSearch, e.getMessage(), e); String reason = getFailReason(KEY_RANGER_OTHER_ISSUE, check, request); warnReasons.add(String.format(reason, e.getMessage(), url)); } return false; } /** * Generates a list of headers, including {@code Basic} authentication * @param username the username * @param password the password * @return the map of headers */ private Map<String, List<String>> getHeaders(String username, String password) { Map<String, List<String>> headers = new HashMap<>(); String base64 = Base64.encodeBase64String( String.format("%s:%s", username, password).getBytes(Charset.forName("UTF8"))); headers.put("Content-Type", Arrays.asList("application/json")); headers.put("Accept", Arrays.asList("application/json")); headers.put("Authorization", Arrays.asList(String.format("Basic %s", base64))); return headers; } /** * Finds the property value. If not found, then the failure reason for the check * is filled in and processing should not continue. * * @param type the type of property to find * @param key the key in configs matching the type * @param check the check for loading failure reasons * @param request the request for loading failure reasons * @return the property value, or {@code null} if the property doesn't exist * @throws AmbariException */ private String checkEmpty(String type, String key, PrerequisiteCheck check, PrereqCheckRequest request) throws AmbariException { String value = getProperty(request, type, key); if (null == value) { String reason = getFailReason(KEY_RANGER_CONFIG_MISSING, check, request); reason = String.format(reason, type, key); check.setFailReason(reason); check.getFailedOn().add("RANGER"); check.setStatus(PrereqCheckStatus.WARNING); } return value; } }