com.facebook.presto.jdbc.internal.google.auth.oauth2.DefaultCredentialsProvider Maven / Gradle / Ivy
/*
* Copyright 2015, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.facebook.presto.jdbc.internal.google.auth.oauth2;
import com.facebook.presto.jdbc.internal.google.auth.http.HttpTransportFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessControlException;
import java.util.Collections;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Provides the Application Default Credential from the environment.
*
* An instance represents the per-process state used to get and cache the credential and
* allows overriding the state and environment for testing purposes.
**/
class DefaultCredentialsProvider {
static final DefaultCredentialsProvider DEFAULT = new DefaultCredentialsProvider();
static final String CREDENTIAL_ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS";
static final String WELL_KNOWN_CREDENTIALS_FILE = "application_default_credentials.json";
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
static final String HELP_PERMALINK =
"https://developers.google.com/accounts/docs/application-default-credentials";
static final String APP_ENGINE_SIGNAL_CLASS = "com.google.appengine.api.utils.SystemProperty";
static final String CLOUD_SHELL_ENV_VAR = "DEVSHELL_CLIENT_PORT";
static final String SKIP_APP_ENGINE_ENV_VAR = "GOOGLE_APPLICATION_CREDENTIALS_SKIP_APP_ENGINE";
static final String SPECIFICATION_VERSION = System.getProperty("java.specification.version");
static final String GAE_RUNTIME_VERSION = System.getProperty("com.google.appengine.runtime.version");
static final String RUNTIME_JETTY_LOGGER = System.getProperty("org.eclipse.jetty.util.log.class");
static final Logger LOGGER = Logger.getLogger(DefaultCredentialsProvider.class.getName());
static final String NO_GCE_CHECK_ENV_VAR = "NO_GCE_CHECK";
static final String GCE_METADATA_HOST_ENV_VAR = "GCE_METADATA_HOST";
static final String CLOUDSDK_CLIENT_ID =
"764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com";
static final String CLOUDSDK_CREDENTIALS_WARNING =
"Your application has authenticated using end user credentials from Google "
+ "Cloud SDK. We recommend that most server applications use service accounts "
+ "instead. If your application continues to use end user credentials from Cloud "
+ "SDK, you might receive a \"quota exceeded\" or \"API not enabled\" error. For "
+ "more information about service accounts, see "
+ "https://cloud.google.com/docs/authentication/.";
public static final String SUPPRESS_GCLOUD_CREDS_WARNING_ENV_VAR = "SUPPRESS_GCLOUD_CREDS_WARNING";
// These variables should only be accessed inside a synchronized block
private GoogleCredentials cachedCredentials = null;
private boolean checkedAppEngine = false;
private boolean checkedComputeEngine = false;
DefaultCredentialsProvider() {
}
/**
* Returns the Application Default Credentials.
*
*
Returns the Application Default Credentials which are used to identify and authorize the
* whole application. The following are searched (in order) to find the Application Default
* Credentials:
*
* - Credentials file pointed to by the {@code GOOGLE_APPLICATION_CREDENTIALS} environment
* variable
* - Credentials provided by the Google Cloud SDK
* {@code gcloud auth application-default login} command
* - Google App Engine built-in credentials
* - Google Cloud Shell built-in credentials
* - Google Compute Engine built-in credentials
*
*
* @param transportFactory HTTP transport factory, creates the transport used to get access
* tokens.
* @return the credentials instance.
* @throws IOException if the credentials cannot be created in the current environment.
**/
final GoogleCredentials getDefaultCredentials(HttpTransportFactory transportFactory)
throws IOException {
synchronized (this) {
if (cachedCredentials == null) {
cachedCredentials = getDefaultCredentialsUnsynchronized(transportFactory);
}
if (cachedCredentials != null) {
return cachedCredentials;
}
}
throw new IOException(String.format(
"The Application Default Credentials are not available. They are available if running"
+ " in Google Compute Engine. Otherwise, the environment variable %s must be defined"
+ " pointing to a file defining the credentials. See %s for more information.",
CREDENTIAL_ENV_VAR,
HELP_PERMALINK));
}
private final GoogleCredentials getDefaultCredentialsUnsynchronized(
HttpTransportFactory transportFactory) throws IOException {
// First try the environment variable
GoogleCredentials credentials = null;
String credentialsPath = getEnv(CREDENTIAL_ENV_VAR);
if (credentialsPath != null && credentialsPath.length() > 0) {
InputStream credentialsStream = null;
try {
File credentialsFile = new File(credentialsPath);
if (!isFile(credentialsFile)) {
// Path will be put in the message from the catch block below
throw new IOException("File does not exist.");
}
credentialsStream = readStream(credentialsFile);
credentials = GoogleCredentials.fromStream(credentialsStream, transportFactory);
} catch (IOException e) {
// Although it is also the cause, the message of the caught exception can have very
// important information for diagnosing errors, so include its message in the
// outer exception message also.
throw new IOException(String.format(
"Error reading credential file from environment variable %s, value '%s': %s",
CREDENTIAL_ENV_VAR, credentialsPath, e.getMessage()), e);
} catch (AccessControlException expected) {
// Exception querying file system is expected on App-Engine
} finally {
if (credentialsStream != null) {
credentialsStream.close();
}
}
}
// Then try the well-known file
if (credentials == null) {
File wellKnownFileLocation = getWellKnownCredentialsFile();
InputStream credentialsStream = null;
try {
if (isFile(wellKnownFileLocation)) {
credentialsStream = readStream(wellKnownFileLocation);
credentials = GoogleCredentials.fromStream(credentialsStream, transportFactory);
}
} catch (IOException e) {
throw new IOException(String.format(
"Error reading credential file from location %s: %s",
wellKnownFileLocation, e.getMessage()));
} catch (AccessControlException expected) {
// Exception querying file system is expected on App-Engine
} finally {
if (credentialsStream != null) {
credentialsStream.close();
}
}
warnAboutProblematicCredentials(credentials);
}
// Then try GAE 7 standard environment
if (credentials == null && isOnGAEStandard7() && !skipAppEngineCredentialsCheck()) {
credentials = tryGetAppEngineCredential();
}
// Then try Cloud Shell. This must be done BEFORE checking
// Compute Engine, as Cloud Shell runs on GCE VMs.
if (credentials == null) {
credentials = tryGetCloudShellCredentials();
}
// Then try Compute Engine and GAE 8 standard environment
if (credentials == null) {
credentials = tryGetComputeCredentials(transportFactory);
}
return credentials;
}
private void warnAboutProblematicCredentials(GoogleCredentials credentials) {
if (credentials instanceof UserCredentials
&& ((UserCredentials) credentials).getClientId().equals(CLOUDSDK_CLIENT_ID)
&& !Boolean.parseBoolean(getEnv(SUPPRESS_GCLOUD_CREDS_WARNING_ENV_VAR))) {
LOGGER.log(Level.WARNING, CLOUDSDK_CREDENTIALS_WARNING);
}
}
private final File getWellKnownCredentialsFile() {
File cloudConfigPath;
String os = getProperty("os.name", "").toLowerCase(Locale.US);
String envPath = getEnv("CLOUDSDK_CONFIG");
if (envPath != null) {
cloudConfigPath = new File(envPath);
} else if (os.indexOf("windows") >= 0) {
File appDataPath = new File(getEnv("APPDATA"));
cloudConfigPath = new File(appDataPath, CLOUDSDK_CONFIG_DIRECTORY);
} else {
File configPath = new File(getProperty("user.home", ""), ".config");
cloudConfigPath = new File(configPath, CLOUDSDK_CONFIG_DIRECTORY);
}
return new File(cloudConfigPath, WELL_KNOWN_CREDENTIALS_FILE);
}
private boolean runningOnAppEngine() {
Class> systemPropertyClass = null;
try {
systemPropertyClass = forName(APP_ENGINE_SIGNAL_CLASS);
} catch (ClassNotFoundException expected) {
// SystemProperty will always be present on App Engine.
return false;
}
Exception cause;
Field environmentField;
try {
environmentField = systemPropertyClass.getField("environment");
Object environmentValue = environmentField.get(null);
Class> environmentType = environmentField.getType();
Method valueMethod = environmentType.getMethod("value");
Object environmentValueValue = valueMethod.invoke(environmentValue);
return (environmentValueValue != null);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException
| IllegalAccessException | NoSuchMethodException | InvocationTargetException exception) {
cause = exception;
}
throw new RuntimeException(String.format(
"Unexpected error trying to determine if runnning on Google App Engine: %s",
cause.getMessage()), cause);
}
private GoogleCredentials tryGetCloudShellCredentials() {
String port = getEnv(CLOUD_SHELL_ENV_VAR);
if (port != null) {
return CloudShellCredentials.create(Integer.parseInt(port));
} else {
return null;
}
}
private GoogleCredentials tryGetAppEngineCredential() throws IOException {
// Checking for App Engine requires a class load, so check only once
if (checkedAppEngine) {
return null;
}
boolean onAppEngine = runningOnAppEngine();
checkedAppEngine = true;
if (!onAppEngine) {
return null;
}
return new AppEngineCredentials(Collections.emptyList());
}
private final GoogleCredentials tryGetComputeCredentials(HttpTransportFactory transportFactory) {
// Checking compute engine requires a round-trip, so check only once
if (checkedComputeEngine) {
return null;
}
boolean runningOnComputeEngine =
ComputeEngineCredentials.runningOnComputeEngine(transportFactory, this);
checkedComputeEngine = true;
if (runningOnComputeEngine) {
return ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();
}
return null;
}
// Skip app engine check if environment variable
// GOOGLE_APPLICATION_CREDENTIALS_SKIP_APP_ENGINE = 1 or true
private boolean skipAppEngineCredentialsCheck() {
boolean skip = false; // do not skip by default
String value = getEnv(SKIP_APP_ENGINE_ENV_VAR);
if (value != null) {
skip = value.equalsIgnoreCase("true") || value.equals("1");
}
return skip;
}
protected boolean isOnGAEStandard7() {
return GAE_RUNTIME_VERSION != null
&& (SPECIFICATION_VERSION.equals("1.7") || RUNTIME_JETTY_LOGGER == null);
}
/*
* Start of methods to allow overriding in the test code to isolate from the environment.
*/
Class> forName(String className) throws ClassNotFoundException {
return Class.forName(className);
}
String getEnv(String name) {
return System.getenv(name);
}
String getProperty(String property, String def) {
return System.getProperty(property, def);
}
boolean isFile(File file) {
return file.isFile();
}
InputStream readStream(File file) throws FileNotFoundException {
return new FileInputStream(file);
}
/*
* End of methods to allow overriding in the test code to isolate from the environment.
*/
}