Skip to content

Commit

Permalink
feat: add smbios check for GCE residency detection (#1092)
Browse files Browse the repository at this point in the history
* feat: add smbios check to GCE detection
  • Loading branch information
TimurSadykov committed Nov 11, 2022
1 parent 31fe461 commit bfe7d93
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 21 deletions.
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 {
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

0 comments on commit bfe7d93

Please sign in to comment.