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}