001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.hadoop.security; 020 021import java.io.IOException; 022import java.io.InputStream; 023import java.net.URI; 024import java.net.URISyntaxException; 025import java.net.URL; 026 027import com.google.common.annotations.VisibleForTesting; 028import org.apache.commons.io.IOUtils; 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031import org.apache.hadoop.conf.Configuration; 032import org.apache.hadoop.fs.FileSystem; 033import org.apache.hadoop.fs.Path; 034import org.apache.hadoop.security.alias.CredentialProviderFactory; 035import org.apache.hadoop.security.alias.JavaKeyStoreProvider; 036import org.apache.hadoop.security.alias.LocalJavaKeyStoreProvider; 037 038/** 039 * Utility methods for both key and credential provider APIs. 040 * 041 */ 042public final class ProviderUtils { 043 @VisibleForTesting 044 public static final String NO_PASSWORD_WARN = 045 "WARNING: You have accepted the use of the default provider password\n" + 046 "by not configuring a password in one of the two following locations:\n"; 047 @VisibleForTesting 048 public static final String NO_PASSWORD_ERROR = 049 "ERROR: The provider cannot find a password in the expected " + 050 "locations.\nPlease supply a password using one of the " + 051 "following two mechanisms:\n"; 052 @VisibleForTesting 053 public static final String NO_PASSWORD_CONT = 054 "Continuing with the default provider password.\n"; 055 @VisibleForTesting 056 public static final String NO_PASSWORD_INSTRUCTIONS_DOC = 057 "Please review the documentation regarding provider passwords in\n" + 058 "the keystore passwords section of the Credential Provider API\n"; 059 060 private static final Log LOG = LogFactory.getLog(ProviderUtils.class); 061 062 /** 063 * Hidden ctor to ensure that this utility class isn't 064 * instantiated explicitly. 065 */ 066 private ProviderUtils() { 067 // hide ctor for checkstyle compliance 068 } 069 070 /** 071 * Convert a nested URI to decode the underlying path. The translation takes 072 * the authority and parses it into the underlying scheme and authority. 073 * For example, "myscheme://hdfs@nn/my/path" is converted to 074 * "hdfs://nn/my/path". 075 * @param nestedUri the URI from the nested URI 076 * @return the unnested path 077 */ 078 public static Path unnestUri(URI nestedUri) { 079 StringBuilder result = new StringBuilder(); 080 String authority = nestedUri.getAuthority(); 081 if (authority != null) { 082 String[] parts = nestedUri.getAuthority().split("@", 2); 083 result.append(parts[0]); 084 result.append("://"); 085 if (parts.length == 2) { 086 result.append(parts[1]); 087 } 088 } 089 result.append(nestedUri.getPath()); 090 if (nestedUri.getQuery() != null) { 091 result.append("?"); 092 result.append(nestedUri.getQuery()); 093 } 094 if (nestedUri.getFragment() != null) { 095 result.append("#"); 096 result.append(nestedUri.getFragment()); 097 } 098 return new Path(result.toString()); 099 } 100 101 /** 102 * Mangle given local java keystore file URI to allow use as a 103 * LocalJavaKeyStoreProvider. 104 * @param localFile absolute URI with file scheme and no authority component. 105 * i.e. return of File.toURI, 106 * e.g. file:///home/larry/creds.jceks 107 * @return URI of the form localjceks://file/home/larry/creds.jceks 108 * @throws IllegalArgumentException if localFile isn't not a file uri or if it 109 * has an authority component. 110 * @throws URISyntaxException if the wrapping process violates RFC 2396 111 */ 112 public static URI nestURIForLocalJavaKeyStoreProvider(final URI localFile) 113 throws URISyntaxException { 114 if (!("file".equals(localFile.getScheme()))) { 115 throw new IllegalArgumentException("passed URI had a scheme other than " + 116 "file."); 117 } 118 if (localFile.getAuthority() != null) { 119 throw new IllegalArgumentException("passed URI must not have an " + 120 "authority component. For non-local keystores, please use " + 121 JavaKeyStoreProvider.class.getName()); 122 } 123 return new URI(LocalJavaKeyStoreProvider.SCHEME_NAME, 124 "//file" + localFile.getSchemeSpecificPart(), localFile.getFragment()); 125 } 126 127 /** 128 * There are certain integrations of the credential provider API in 129 * which a recursive dependency between the provider and the hadoop 130 * filesystem abstraction causes a problem. These integration points 131 * need to leverage this utility method to remove problematic provider 132 * types from the existing provider path within the configuration. 133 * 134 * @param config the existing configuration with provider path 135 * @param fileSystemClass the class which providers must be compatible 136 * @return Configuration clone with new provider path 137 */ 138 public static Configuration excludeIncompatibleCredentialProviders( 139 Configuration config, Class<? extends FileSystem> fileSystemClass) 140 throws IOException { 141 142 String providerPath = config.get( 143 CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH); 144 145 if (providerPath == null) { 146 return config; 147 } 148 StringBuffer newProviderPath = new StringBuffer(); 149 String[] providers = providerPath.split(","); 150 Path path = null; 151 for (String provider: providers) { 152 try { 153 path = unnestUri(new URI(provider)); 154 Class<? extends FileSystem> clazz = null; 155 try { 156 String scheme = path.toUri().getScheme(); 157 clazz = FileSystem.getFileSystemClass(scheme, config); 158 } catch (IOException ioe) { 159 // not all providers are filesystem based 160 // for instance user:/// will not be able to 161 // have a filesystem class associated with it. 162 if (newProviderPath.length() > 0) { 163 newProviderPath.append(","); 164 } 165 newProviderPath.append(provider); 166 } 167 if (clazz != null) { 168 if (fileSystemClass.isAssignableFrom(clazz)) { 169 LOG.debug("Filesystem based provider" + 170 " excluded from provider path due to recursive dependency: " 171 + provider); 172 } else { 173 if (newProviderPath.length() > 0) { 174 newProviderPath.append(","); 175 } 176 newProviderPath.append(provider); 177 } 178 } 179 } catch (URISyntaxException e) { 180 LOG.warn("Credential Provider URI is invalid." + provider); 181 } 182 } 183 184 String effectivePath = newProviderPath.toString(); 185 if (effectivePath.equals(providerPath)) { 186 return config; 187 } 188 189 Configuration conf = new Configuration(config); 190 if (effectivePath.equals("")) { 191 conf.unset(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH); 192 } else { 193 conf.set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH, 194 effectivePath); 195 } 196 return conf; 197 } 198 199 /** 200 * The password is either found in the environment or in a file. This 201 * routine implements the logic for locating the password in these 202 * locations. 203 * 204 * @param envWithPass The name of the environment variable that might 205 * contain the password. Must not be null. 206 * @param fileWithPass The name of a file that could contain the password. 207 * Can be null. 208 * @return The password as a char []; null if not found. 209 * @throws IOException If fileWithPass is non-null and points to a 210 * nonexistent file or a file that fails to open and be read properly. 211 */ 212 public static char[] locatePassword(String envWithPass, String fileWithPass) 213 throws IOException { 214 char[] pass = null; 215 if (System.getenv().containsKey(envWithPass)) { 216 pass = System.getenv(envWithPass).toCharArray(); 217 } 218 if (pass == null) { 219 if (fileWithPass != null) { 220 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 221 URL pwdFile = cl.getResource(fileWithPass); 222 if (pwdFile == null) { 223 // Provided Password file does not exist 224 throw new IOException("Password file does not exist"); 225 } 226 try (InputStream is = pwdFile.openStream()) { 227 pass = IOUtils.toString(is).trim().toCharArray(); 228 } 229 } 230 } 231 return pass; 232 } 233 234 private static String noPasswordInstruction(String envKey, String fileKey) { 235 return 236 " * In the environment variable " + envKey + "\n" + 237 " * In a file referred to by the configuration entry\n" + 238 " " + fileKey + ".\n" + 239 NO_PASSWORD_INSTRUCTIONS_DOC; 240 } 241 242 public static String noPasswordWarning(String envKey, String fileKey) { 243 return NO_PASSWORD_WARN + noPasswordInstruction(envKey, fileKey) + 244 NO_PASSWORD_CONT; 245 } 246 247 public static String noPasswordError(String envKey, String fileKey) { 248 return NO_PASSWORD_ERROR + noPasswordInstruction(envKey, fileKey); 249 } 250}