/* * 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.brooklyn.core.location; import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth; import java.io.ByteArrayInputStream; import java.io.File; import java.security.KeyPair; import java.security.PublicKey; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.BrooklynFeatureEnablement; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.location.cloud.CloudLocationConfig; import org.apache.brooklyn.core.location.internal.LocationInternal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.collections.MutableSet; import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.util.core.config.ConfigBag; import org.apache.brooklyn.util.core.crypto.SecureKeys; import org.apache.brooklyn.util.core.crypto.SecureKeys.PassphraseProblem; import org.apache.brooklyn.util.crypto.AuthorizedKeysParser; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.os.Os; import org.apache.brooklyn.util.text.StringFunctions; import org.apache.brooklyn.util.text.Strings; import com.google.common.annotations.Beta; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; public class LocationConfigUtils { private static final Logger log = LoggerFactory.getLogger(LocationConfigUtils.class); /** Creates an instance of {@link OsCredential} by inspecting {@link LocationConfigKeys#PASSWORD}; * {@link LocationConfigKeys#PRIVATE_KEY_DATA} and {@link LocationConfigKeys#PRIVATE_KEY_FILE}; * {@link LocationConfigKeys#PRIVATE_KEY_PASSPHRASE} if needed, and * {@link LocationConfigKeys#PRIVATE_KEY_DATA} and {@link LocationConfigKeys#PRIVATE_KEY_FILE} * (defaulting to the private key file + ".pub"). **/ public static OsCredential getOsCredential(ConfigBag config) { return OsCredential.newInstance(config); } /** Convenience class for holding private/public keys and passwords, inferring from config keys. * See {@link LocationConfigUtils#getOsCredential(ConfigBag)}. */ @Beta // would be nice to replace with a builder pattern public static class OsCredential { private final ConfigBag config; private boolean preferPassword = false; private boolean tryDefaultKeys = true; private boolean requirePublicKey = true; private boolean doKeyValidation = BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_VALIDATE_LOCATION_SSH_KEYS); private boolean warnOnErrors = true; private boolean throwOnErrors = false; private boolean dirty = true;; private String privateKeyData; private String publicKeyData; private String password; private OsCredential(ConfigBag config) { this.config = config; } /** throws if there are any problems */ public OsCredential checkNotEmpty() { checkNoErrors(); if (!hasKey() && !hasPassword()) { if (warningMessages.size()>0) throw new IllegalStateException("Could not find credentials: "+warningMessages); else throw new IllegalStateException("Could not find credentials"); } return this; } /** throws if there were errors resolving (e.g. explicit keys, none of which were found/valid, or public key required and not found) * @return */ public OsCredential checkNoErrors() { throwOnErrors(true); dirty(); infer(); return this; } public OsCredential logAnyWarnings() { if (!warningMessages.isEmpty()) log.warn("When reading credentials: "+warningMessages); return this; } public Set<String> getWarningMessages() { return warningMessages; } /** returns either the key or password or null; if both a key and a password this prefers the key unless otherwise set * via {@link #preferPassword()} */ public synchronized String getPreferredCredential() { infer(); if (isUsingPassword()) return password; if (hasKey()) return privateKeyData; return null; } /** if there is no credential (ignores public key) */ public boolean isEmpty() { return !hasKey() && !hasPassword(); } public boolean hasKey() { infer(); // key has stricter non-blank check than password return Strings.isNonBlank(privateKeyData); } public boolean hasPassword() { infer(); // blank, even empty passwords are allowed return password!=null; } /** if a password is available, and either this is preferred over a key or there is no key */ public boolean isUsingPassword() { return hasPassword() && (!hasKey() || preferPassword); } public String getPrivateKeyData() { infer(); return privateKeyData; } public String getPublicKeyData() { infer(); return publicKeyData; } public String getPassword() { infer(); return password; } /** if both key and password supplied, prefer the key; the default */ public OsCredential preferKey() { preferPassword = false; return dirty(); } /** if both key and password supplied, prefer the password; see {@link #preferKey()} */ public OsCredential preferPassword() { preferPassword = true; return dirty(); } /** if false, do not mind if there is no public key corresponding to any private key; * defaults to true; only applies if a private key is set */ public OsCredential requirePublicKey(boolean requirePublicKey) { this.requirePublicKey = requirePublicKey; return dirty(); } /** whether to check the private/public keys and passphrase are coherent; default true */ public OsCredential doKeyValidation(boolean doKeyValidation) { this.doKeyValidation = doKeyValidation; return dirty(); } /** if true (the default) this will look at default locations set on keys */ public OsCredential useDefaultKeys(boolean tryDefaultKeys) { this.tryDefaultKeys = tryDefaultKeys; return dirty(); } /** whether to log warnings on problems */ public OsCredential warnOnErrors(boolean warnOnErrors) { this.warnOnErrors = warnOnErrors; return dirty(); } /** whether to throw on problems */ public OsCredential throwOnErrors(boolean throwOnErrors) { this.throwOnErrors = throwOnErrors; return dirty(); } private OsCredential dirty() { dirty = true; return this; } public static OsCredential newInstance(ConfigBag config) { return new OsCredential(config); } private synchronized void infer() { if (!dirty) return; warningMessages.clear(); log.debug("Inferring OS credentials"); privateKeyData = config.get(LocationConfigKeys.PRIVATE_KEY_DATA); password = config.get(LocationConfigKeys.PASSWORD); publicKeyData = getKeyDataFromDataKeyOrFileKey(config, LocationConfigKeys.PUBLIC_KEY_DATA, LocationConfigKeys.PUBLIC_KEY_FILE); KeyPair privateKey = null; if (Strings.isBlank(privateKeyData)) { // look up private key files String privateKeyFiles = null; boolean privateKeyFilesExplicitlySet = config.containsKey(LocationConfigKeys.PRIVATE_KEY_FILE); if (privateKeyFilesExplicitlySet || (tryDefaultKeys && password==null)) privateKeyFiles = config.get(LocationConfigKeys.PRIVATE_KEY_FILE); if (Strings.isNonBlank(privateKeyFiles)) { Iterator<String> fi = Arrays.asList(privateKeyFiles.split(File.pathSeparator)).iterator(); while (fi.hasNext()) { String file = fi.next(); if (Strings.isNonBlank(file)) { try { // real URL's won't actual work, due to use of path separator above // not real important, but we get it for free if "files" is a list instead. // using ResourceUtils is useful for classpath resources if (file!=null) privateKeyData = ResourceUtils.create().getResourceAsString(file); // else use data already set privateKey = getValidatedPrivateKey(file); if (privateKeyData==null) { // was cleared due to validation error } else if (Strings.isNonBlank(publicKeyData)) { log.debug("Loaded private key data from "+file+" (public key data explicitly set)"); break; } else { String publicKeyFile = (file!=null ? file+".pub" : "(data)"); try { publicKeyData = ResourceUtils.create().getResourceAsString(publicKeyFile); log.debug("Loaded private key data from "+file+ " and public key data from "+publicKeyFile); break; } catch (Exception e) { Exceptions.propagateIfFatal(e); log.debug("No public key file "+publicKeyFile+"; will try extracting from private key"); publicKeyData = AuthorizedKeysParser.encodePublicKey(privateKey.getPublic()); if (publicKeyData==null) { if (requirePublicKey) { addWarning("Unable to find or extract public key for "+file, "skipping"); } else { log.debug("Loaded private key data from "+file+" (public key data not found but not required)"); break; } } else { log.debug("Loaded private key data from "+file+" (public key data extracted)"); break; } privateKeyData = null; } } } catch (Exception e) { Exceptions.propagateIfFatal(e); String message = "Missing/invalid private key file "+file; if (privateKeyFilesExplicitlySet) addWarning(message, (!fi.hasNext() ? "no more files to try" : "trying next file")+": "+e); } } } if (privateKeyFilesExplicitlySet && Strings.isBlank(privateKeyData)) error("No valid private keys found", ""+warningMessages); } } else { privateKey = getValidatedPrivateKey("(data)"); } if (privateKeyData!=null) { if (requirePublicKey && Strings.isBlank(publicKeyData)) { if (privateKey!=null) { publicKeyData = AuthorizedKeysParser.encodePublicKey(privateKey.getPublic()); } if (Strings.isBlank(publicKeyData)) { error("If explicit "+LocationConfigKeys.PRIVATE_KEY_DATA.getName()+" is supplied, then " + "the corresponding "+LocationConfigKeys.PUBLIC_KEY_DATA.getName()+" must also be supplied.", null); } else { log.debug("Public key data extracted"); } } if (doKeyValidation && privateKey!=null && privateKey.getPublic()!=null && Strings.isNonBlank(publicKeyData)) { PublicKey decoded = null; try { decoded = AuthorizedKeysParser.decodePublicKey(publicKeyData); } catch (Exception e) { Exceptions.propagateIfFatal(e); addWarning("Invalid public key: "+decoded); } if (decoded!=null && !privateKey.getPublic().equals( decoded )) { error("Public key inferred from does not match public key extracted from private key", null); } } } log.debug("OS credential inference: "+this); dirty = false; } private KeyPair getValidatedPrivateKey(String label) { KeyPair privateKey = null; String passphrase = config.get(CloudLocationConfig.PRIVATE_KEY_PASSPHRASE); try { privateKey = SecureKeys.readPem(new ByteArrayInputStream(privateKeyData.getBytes()), passphrase); if (passphrase!=null) { // get the unencrypted key data for our internal use (jclouds requires this) privateKeyData = SecureKeys.toPem(privateKey); } } catch (PassphraseProblem e) { if (doKeyValidation) { log.debug("Encountered error handling key "+label+": "+e, e); if (Strings.isBlank(passphrase)) addWarning("Passphrase required for key '"+label+"'"); else addWarning("Invalid passphrase for key '"+label+"'"); privateKeyData = null; } } catch (Exception e) { Exceptions.propagateIfFatal(e); if (doKeyValidation) { addWarning("Unable to parse private key from '"+label+"': unknown format"); privateKeyData = null; } } return privateKey; } Set<String> warningMessages = MutableSet.of(); private void error(String msg, String logExtension) { addWarning(msg); if (warnOnErrors) log.warn(msg+(logExtension==null ? "" : ": "+logExtension)); if (throwOnErrors) throw new IllegalStateException(msg+(logExtension==null ? "" : "; "+logExtension)); } private void addWarning(String msg) { addWarning(msg, null); } private void addWarning(String msg, String debugExtension) { log.debug(msg+(debugExtension==null ? "" : "; "+debugExtension)); warningMessages.add(msg); } @Override public String toString() { return getClass().getSimpleName()+"["+ (Strings.isNonBlank(publicKeyData) ? publicKeyData : "no-public-key")+";"+ (Strings.isNonBlank(privateKeyData) ? "private-key-present" : "no-private-key")+","+ (password!=null ? "password(len="+password.length()+")" : "no-password")+"]"; } } /** @deprecated since 0.7.0, use #getOsCredential(ConfigBag) */ @Deprecated public static String getPrivateKeyData(ConfigBag config) { return getKeyData(config, LocationConfigKeys.PRIVATE_KEY_DATA, LocationConfigKeys.PRIVATE_KEY_FILE); } /** @deprecated since 0.7.0, use #getOsCredential(ConfigBag) */ @Deprecated public static String getPublicKeyData(ConfigBag config) { String data = getKeyData(config, LocationConfigKeys.PUBLIC_KEY_DATA, LocationConfigKeys.PUBLIC_KEY_FILE); if (groovyTruth(data)) return data; String privateKeyFile = config.get(LocationConfigKeys.PRIVATE_KEY_FILE); if (groovyTruth(privateKeyFile)) { List<String> privateKeyFiles = Arrays.asList(privateKeyFile.split(File.pathSeparator)); List<String> publicKeyFiles = ImmutableList.copyOf(Iterables.transform(privateKeyFiles, StringFunctions.append(".pub"))); List<String> publicKeyFilesTidied = tidyFilePaths(publicKeyFiles); String fileData = getFileContents(publicKeyFilesTidied); if (groovyTruth(fileData)) { if (log.isDebugEnabled()) log.debug("Loaded "+LocationConfigKeys.PUBLIC_KEY_DATA.getName()+" from inferred files, based on "+LocationConfigKeys.PRIVATE_KEY_FILE.getName() + ": used " + publicKeyFilesTidied + " for "+config.getDescription()); config.put(LocationConfigKeys.PUBLIC_KEY_DATA, fileData); return fileData; } else { log.info("Not able to load "+LocationConfigKeys.PUBLIC_KEY_DATA.getName()+" from inferred files, based on "+LocationConfigKeys.PRIVATE_KEY_FILE.getName() + ": tried " + publicKeyFilesTidied + " for "+config.getDescription()); } } return null; } /** @deprecated since 0.7.0, use #getOsCredential(ConfigBag) */ @Deprecated public static String getKeyData(ConfigBag config, ConfigKey<String> dataKey, ConfigKey<String> fileKey) { return getKeyDataFromDataKeyOrFileKey(config, dataKey, fileKey); } private static String getKeyDataFromDataKeyOrFileKey(ConfigBag config, ConfigKey<String> dataKey, ConfigKey<String> fileKey) { boolean unused = config.isUnused(dataKey); String data = config.get(dataKey); if (groovyTruth(data) && !unused) { return data; } String file = config.get(fileKey); if (groovyTruth(file)) { List<String> files = Arrays.asList(file.split(File.pathSeparator)); List<String> filesTidied = tidyFilePaths(files); String fileData = getFileContents(filesTidied); if (fileData == null) { log.warn("Invalid file" + (files.size() > 1 ? "s" : "") + " for " + fileKey + " (given " + files + (files.equals(filesTidied) ? "" : "; converted to " + filesTidied) + ") " + "may fail provisioning " + config.getDescription()); } else if (groovyTruth(data)) { if (!fileData.trim().equals(data.trim())) log.warn(dataKey.getName()+" and "+fileKey.getName()+" both specified; preferring the former"); } else { data = fileData; config.put(dataKey, data); config.get(dataKey); } } return data; } /** * Reads the given file(s) in-order, returning the contents of the first file that can be read. * Returns the file contents, or null if none of the files can be read. * * @param files list of file paths */ private static String getFileContents(Iterable<String> files) { Iterator<String> fi = files.iterator(); while (fi.hasNext()) { String file = fi.next(); if (groovyTruth(file)) { try { // see comment above String result = ResourceUtils.create().getResourceAsString(file); if (result!=null) return result; log.debug("Invalid file "+file+" ; " + (!fi.hasNext() ? "no more files to try" : "trying next file")+" (null)"); } catch (Exception e) { Exceptions.propagateIfFatal(e); log.debug("Invalid file "+file+" ; " + (!fi.hasNext() ? "no more files to try" : "trying next file"), e); } } } return null; } private static List<String> tidyFilePaths(Iterable<String> files) { List<String> result = Lists.newArrayList(); for (String file : files) { result.add(Os.tidyPath(file)); } return result; } /** @deprecated since 0.6.0 use configBag.getWithDeprecation */ @Deprecated @SuppressWarnings("unchecked") public static <T> T getConfigCheckingDeprecatedAlternatives(ConfigBag configBag, ConfigKey<T> preferredKey, ConfigKey<?> ...deprecatedKeys) { T value1 = (T) configBag.getWithDeprecation(preferredKey, deprecatedKeys); T value2 = getConfigCheckingDeprecatedAlternativesInternal(configBag, preferredKey, deprecatedKeys); if (!Objects.equal(value1, value2)) { // points to a bug in one of the get-with-deprecation methods log.warn("Deprecated getConfig with deprecated keys "+Arrays.toString(deprecatedKeys)+" gets different value with " + "new strategy "+preferredKey+" ("+value1+") and old ("+value2+"); preferring old value for now, but this behaviour will change"); return value2; } return value1; } @SuppressWarnings("unchecked") private static <T> T getConfigCheckingDeprecatedAlternativesInternal(ConfigBag configBag, ConfigKey<T> preferredKey, ConfigKey<?> ...deprecatedKeys) { ConfigKey<?> keyProvidingValue = null; T value = null; boolean found = false; if (configBag.containsKey(preferredKey)) { value = configBag.get(preferredKey); found = true; keyProvidingValue = preferredKey; } for (ConfigKey<?> deprecatedKey: deprecatedKeys) { T altValue = null; boolean altFound = false; if (configBag.containsKey(deprecatedKey)) { altValue = (T) configBag.get(deprecatedKey); altFound = true; if (altFound) { if (found) { if (Objects.equal(value, altValue)) { // fine -- nothing } else { log.warn("Detected deprecated key "+deprecatedKey+" with value "+altValue+" used in addition to "+keyProvidingValue+" " + "with value "+value+" for "+configBag.getDescription()+"; ignoring"); configBag.remove(deprecatedKey); } } else { log.warn("Detected deprecated key "+deprecatedKey+" with value "+altValue+" used instead of recommended "+preferredKey+"; " + "promoting to preferred key status; will not be supported in future versions"); configBag.put(preferredKey, altValue); configBag.remove(deprecatedKey); value = altValue; found = true; keyProvidingValue = deprecatedKey; } } } } if (found) { return value; } else { return configBag.get(preferredKey); // get the default } } public static Map<ConfigKey<String>,String> finalAndOriginalSpecs(String finalSpec, Object ...sourcesForOriginalSpec) { // yuck!: TODO should clean up how these things get passed around Map<ConfigKey<String>,String> result = MutableMap.of(); if (finalSpec!=null) result.put(LocationInternal.FINAL_SPEC, finalSpec); String originalSpec = null; for (Object source: sourcesForOriginalSpec) { if (source instanceof CharSequence) originalSpec = source.toString(); else if (source instanceof Map) { if (originalSpec==null) originalSpec = Strings.toString( ((Map<?,?>)source).get(LocationInternal.ORIGINAL_SPEC) ); if (originalSpec==null) originalSpec = Strings.toString( ((Map<?,?>)source).get(LocationInternal.ORIGINAL_SPEC.getName()) ); } if (originalSpec!=null) break; } if (originalSpec==null) originalSpec = finalSpec; if (originalSpec!=null) result.put(LocationInternal.ORIGINAL_SPEC, originalSpec); return result; } public static boolean isEnabled(ManagementContext mgmt, String prefix) { ConfigKey<Boolean> key = ConfigKeys.newConfigKeyWithPrefix(prefix+".", LocationConfigKeys.ENABLED); Boolean enabled = mgmt.getConfig().getConfig(key); if (enabled!=null) return enabled.booleanValue(); return true; } }