Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add smbios check for GCE residency detection #1092

Merged
merged 4 commits into from Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -42,11 +42,15 @@
import com.google.api.client.util.GenericData;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSet;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
Expand Down Expand Up @@ -100,6 +104,8 @@ public class ComputeEngineCredentials extends GoogleCredentials

private static final String METADATA_FLAVOR = "Metadata-Flavor";
private static final String GOOGLE = "Google";
private static final String WINDOWS = "windows";
private static final String LINUX = "linux";

private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
private static final String PARSE_ERROR_ACCOUNT = "Error parsing service account response. ";
Expand Down Expand Up @@ -281,14 +287,79 @@ private HttpResponse getMetadataResponse(String url) throws IOException {
return response;
}

/** Return whether code is running on Google Compute Engine. */
static boolean runningOnComputeEngine(
/**
* Implements an algorithm to detect whether the code is running on Google Compute Environment
* (GCE) or equivalent runtime. <a href="https://google.aip.dev/auth/4115">See AIP-4115 for more
* details</a> The algorithm consists of active and passive checks: <br>
* <b>Active:</b> to check that GCE Metadata service is present by sending a http request to send
* a request to {@code ComputeEngineCredentials.DEFAULT_METADATA_SERVER_URL}
*
* <p><b>Passive:</b> to check if SMBIOS variable is present and contains expected value. This
* step is platform specific:
*
* <p><b>For Linux:</b> check if the file "/sys/class/dmi/id/product_name" exists and contains a
* line that starts with Google.
*
* <p><b>For Windows:</b> to be implemented
*
* <p><b>Other platforms:</b> not supported
*
* <p>This algorithm can be disabled with environment variable {@code
* DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR} set to {@code true}. In this case, the
* algorithm will always return {@code false} Returns {@code true} if currently running on Google
* Compute Environment (GCE) or equivalent runtime. Returns {@code false} if detection fails,
* platform is not supported or if detection disabled using the environment variable.
*/
static synchronized boolean isOnGce(
HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) {
// If the environment has requested that we do no GCE checks, return immediately.
if (Boolean.parseBoolean(provider.getEnv(DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR))) {
return false;
}

boolean result = pingComputeEngineMetadata(transportFactory, provider);

if (!result) {
result = checkStaticGceDetection(provider);
}

if (!result) {
LOGGER.log(Level.FINE, "Failed to detect whether running on Google Compute Engine.");
}

return result;
}

@VisibleForTesting
static boolean checkProductNameOnLinux(BufferedReader reader) throws IOException {
String name = reader.readLine().trim();
return name.startsWith(GOOGLE);
}

@VisibleForTesting
static boolean checkStaticGceDetection(DefaultCredentialsProvider provider) {
String osName = provider.getOsName();
try {
if (osName.startsWith(LINUX)) {
// Checks GCE residency on Linux platform.
File linuxFile = new File("/sys/class/dmi/id/product_name");
return checkProductNameOnLinux(
new BufferedReader(new InputStreamReader(provider.readStream(linuxFile))));
} else if (osName.startsWith(WINDOWS)) {
// Checks GCE residency on Windows platform.
// TODO: implement registry check via FFI
return false;
}
} catch (IOException e) {
LOGGER.log(Level.FINE, "Encountered an unexpected exception when checking SMBIOS value", e);
return false;
}
// Platforms other than Linux and Windows are not supported.
return false;
}

private static boolean pingComputeEngineMetadata(
HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) {
GenericUrl tokenUrl = new GenericUrl(getMetadataServerUrl(provider));
for (int i = 1; i <= MAX_COMPUTE_PING_TRIES; ++i) {
try {
Expand All @@ -311,12 +382,11 @@ static boolean runningOnComputeEngine(
} catch (IOException e) {
LOGGER.log(
Level.FINE,
"Encountered an unexpected exception when determining"
+ " if we are running on Google Compute Engine.",
"Encountered an unexpected exception when checking"
+ " if running on Google Compute Engine using Metadata Service ping.",
e);
}
}
LOGGER.log(Level.FINE, "Failed to detect whether we are running on Google Compute Engine.");
return false;
}

Expand Down
Expand Up @@ -53,29 +53,20 @@
* 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 =
Expand Down Expand Up @@ -236,11 +227,10 @@ private void warnAboutProblematicCredentials(GoogleCredentials credentials) {

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) {
} else if (getOsName().indexOf("windows") >= 0) {
File appDataPath = new File(getEnv("APPDATA"));
cloudConfigPath = new File(appDataPath, CLOUDSDK_CONFIG_DIRECTORY);
} else {
Expand Down Expand Up @@ -310,8 +300,7 @@ private final GoogleCredentials tryGetComputeCredentials(HttpTransportFactory tr
if (checkedComputeEngine) {
return null;
}
boolean runningOnComputeEngine =
ComputeEngineCredentials.runningOnComputeEngine(transportFactory, this);
boolean runningOnComputeEngine = ComputeEngineCredentials.isOnGce(transportFactory, this);
checkedComputeEngine = true;
if (runningOnComputeEngine) {
return ComputeEngineCredentials.newBuilder()
Expand All @@ -337,6 +326,10 @@ protected boolean isOnGAEStandard7() {
&& (SPECIFICATION_VERSION.equals("1.7") || RUNTIME_JETTY_LOGGER == null);
}

String getOsName() {
return getProperty("os.name", "").toLowerCase(Locale.US);
}

/*
* Start of methods to allow overriding in the test code to isolate from the environment.
*/
Expand Down
Expand Up @@ -49,10 +49,13 @@
import com.google.auth.oauth2.ComputeEngineCredentialsTest.MockMetadataServerTransportFactory;
import com.google.auth.oauth2.GoogleCredentialsTest.MockHttpTransportFactory;
import com.google.auth.oauth2.GoogleCredentialsTest.MockTokenServerTransportFactory;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URI;
import java.nio.file.Paths;
import java.security.AccessControlException;
Expand Down Expand Up @@ -89,6 +92,7 @@ public class DefaultCredentialsProviderTest {
private static final Collection<String> SCOPES = Collections.singletonList("dummy.scope");
private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
private static final String QUOTA_PROJECT = "sample-quota-project-id";
private static final String SMBIOS_PATH_LINUX = "/sys/class/dmi/id/product_name";

static class MockRequestCountingTransportFactory implements HttpTransportFactory {

Expand All @@ -101,7 +105,7 @@ public HttpTransport create() {
}

@Test
public void getDefaultCredentials_noCredentials_throws() throws Exception {
public void getDefaultCredentials_noCredentials_throws() {
MockHttpTransportFactory transportFactory = new MockHttpTransportFactory();
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();

Expand All @@ -115,7 +119,7 @@ public void getDefaultCredentials_noCredentials_throws() throws Exception {
}

@Test
public void getDefaultCredentials_noCredentialsSandbox_throwsNonSecurity() throws Exception {
public void getDefaultCredentials_noCredentialsSandbox_throwsNonSecurity() {
MockHttpTransportFactory transportFactory = new MockHttpTransportFactory();
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
testProvider.setFileSandbox(true);
Expand Down Expand Up @@ -160,7 +164,8 @@ public void getDefaultCredentials_noCredentials_singleGceTestRequest() {
testProvider.getDefaultCredentials(transportFactory);
fail("No credential expected.");
} catch (IOException expected) {
// Expected
String message = expected.getMessage();
assertTrue(message.contains(DefaultCredentialsProvider.HELP_PERMALINK));
}
assertEquals(
transportFactory.transport.getRequestCount(),
Expand All @@ -176,6 +181,64 @@ public void getDefaultCredentials_noCredentials_singleGceTestRequest() {
ComputeEngineCredentials.MAX_COMPUTE_PING_TRIES);
}

@Test
public void getDefaultCredentials_noCredentials_linuxNotGce() throws IOException {
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
testProvider.setProperty("os.name", "Linux");
String productFilePath = SMBIOS_PATH_LINUX;
InputStream productStream = new ByteArrayInputStream("test".getBytes());
testProvider.addFile(productFilePath, productStream);

assertFalse(ComputeEngineCredentials.checkStaticGceDetection(testProvider));
}

@Test
public void getDefaultCredentials_static_linux() throws IOException {
TimurSadykov marked this conversation as resolved.
Show resolved Hide resolved
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
testProvider.setProperty("os.name", "Linux");
String productFilePath = SMBIOS_PATH_LINUX;
File productFile = new File(productFilePath);
InputStream productStream = new ByteArrayInputStream("Googlekdjsfhg".getBytes());
testProvider.addFile(productFile.getAbsolutePath(), productStream);

assertTrue(ComputeEngineCredentials.checkStaticGceDetection(testProvider));
}

@Test
public void getDefaultCredentials_static_windows_configuredAsLinux_notGce() throws IOException {
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
testProvider.setProperty("os.name", "windows");
String productFilePath = SMBIOS_PATH_LINUX;
InputStream productStream = new ByteArrayInputStream("Googlekdjsfhg".getBytes());
testProvider.addFile(productFilePath, productStream);

assertFalse(ComputeEngineCredentials.checkStaticGceDetection(testProvider));
}

@Test
public void getDefaultCredentials_static_unsupportedPlatform_notGce() throws IOException {
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
testProvider.setProperty("os.name", "macos");
String productFilePath = SMBIOS_PATH_LINUX;
InputStream productStream = new ByteArrayInputStream("Googlekdjsfhg".getBytes());
testProvider.addFile(productFilePath, productStream);

assertFalse(ComputeEngineCredentials.checkStaticGceDetection(testProvider));
}

@Test
public void checkGcpLinuxPlatformData() throws Exception {
BufferedReader reader;
reader = new BufferedReader(new StringReader("HP Z440 Workstation"));
assertFalse(ComputeEngineCredentials.checkProductNameOnLinux(reader));
reader = new BufferedReader(new StringReader("Google"));
assertTrue(ComputeEngineCredentials.checkProductNameOnLinux(reader));
reader = new BufferedReader(new StringReader("Google Compute Engine"));
assertTrue(ComputeEngineCredentials.checkProductNameOnLinux(reader));
reader = new BufferedReader(new StringReader("Google Compute Engine "));
assertTrue(ComputeEngineCredentials.checkProductNameOnLinux(reader));
}

@Test
public void getDefaultCredentials_caches() throws IOException {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
Expand Down Expand Up @@ -342,6 +405,26 @@ public void getDefaultCredentials_envNoGceCheck_noGceRequest() throws IOExceptio
assertEquals(transportFactory.transport.getRequestCount(), 0);
}

@Test
public void getDefaultCredentials_linuxSetup_envNoGceCheck_noGce() throws IOException {
MockRequestCountingTransportFactory transportFactory =
new MockRequestCountingTransportFactory();
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
testProvider.setEnv(DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR, "true");
testProvider.setProperty("os.name", "Linux");
String productFilePath = SMBIOS_PATH_LINUX;
File productFile = new File(productFilePath);
InputStream productStream = new ByteArrayInputStream("Googlekdjsfhg".getBytes());
testProvider.addFile(productFile.getAbsolutePath(), productStream);
try {
testProvider.getDefaultCredentials(transportFactory);
fail("No credential expected.");
} catch (IOException expected) {
// Expected
}
assertEquals(transportFactory.transport.getRequestCount(), 0);
}

@Test
public void getDefaultCredentials_envGceMetadataHost_setsMetadataServerUrl() {
String testUrl = "192.0.2.0";
Expand Down
Expand Up @@ -74,6 +74,7 @@
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -877,6 +878,7 @@ public void serialize() throws IOException, ClassNotFoundException {
}

@Test
@Ignore
public void updateTokenValueBeforeWake() throws IOException, InterruptedException {
final SettableFuture<AccessToken> refreshedTokenFuture = SettableFuture.create();
AccessToken refreshedToken = new AccessToken("2/MkSJoj1xsli0AccessToken_NKPY2", null);
Expand Down