Skip to content

Commit

Permalink
feat: add loading and executing of default client cert source (#353)
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer authored Aug 17, 2021
1 parent f3f0df8 commit 930be8f
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 0 deletions.
65 changes: 65 additions & 0 deletions src/CredentialsLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\Credentials\UserRefreshCredentials;
use GuzzleHttp\ClientInterface;
use RuntimeException;
use UnexpectedValueException;

/**
* CredentialsLoader contains the behaviour used to locate and find default
Expand All @@ -34,6 +36,8 @@ abstract class CredentialsLoader implements
const ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS';
const WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json';
const NON_WINDOWS_WELL_KNOWN_PATH_BASE = '.config';
const MTLS_WELL_KNOWN_PATH = '.secureConnect/context_aware_metadata.json';
const MTLS_CERT_ENV_VAR = 'GOOGLE_API_USE_CLIENT_CERTIFICATE';

/**
* @param string $cause
Expand Down Expand Up @@ -247,4 +251,65 @@ public function updateMetadata(

return $metadata_copy;
}

/**
* Gets a callable which returns the default device certification.
*
* @throws UnexpectedValueException
* @return callable|null
*/
public static function getDefaultClientCertSource()
{
if (!$clientCertSourceJson = self::loadDefaultClientCertSourceFile()) {
return null;
}
$clientCertSourceCmd = $clientCertSourceJson['cert_provider_command'];

return function () use ($clientCertSourceCmd) {
$cmd = array_map('escapeshellarg', $clientCertSourceCmd);
exec(implode(' ', $cmd), $output, $returnVar);

if (0 === $returnVar) {
return implode(PHP_EOL, $output);
}
throw new RuntimeException(
'"cert_provider_command" failed with a nonzero exit code'
);
};
}

/**
* Determines whether or not the default device certificate should be loaded.
*
* @return bool
*/
public static function shouldLoadClientCertSource()
{
return filter_var(getenv(self::MTLS_CERT_ENV_VAR), FILTER_VALIDATE_BOOLEAN);
}

private static function loadDefaultClientCertSourceFile()
{
$rootEnv = self::isOnWindows() ? 'APPDATA' : 'HOME';
$path = sprintf('%s/%s', getenv($rootEnv), self::MTLS_WELL_KNOWN_PATH);
if (!file_exists($path)) {
return null;
}
$jsonKey = file_get_contents($path);
$clientCertSourceJson = json_decode($jsonKey, true);
if (!$clientCertSourceJson) {
throw new UnexpectedValueException('Invalid client cert source JSON');
}
if (!isset($clientCertSourceJson['cert_provider_command'])) {
throw new UnexpectedValueException(
'cert source requires "cert_provider_command"'
);
}
if (!is_array($clientCertSourceJson['cert_provider_command'])) {
throw new UnexpectedValueException(
'cert source expects "cert_provider_command" to be an array'
);
}
return $clientCertSourceJson;
}
}
119 changes: 119 additions & 0 deletions tests/CredentialsLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,125 @@ public function testUpdateMetadataSkipsWhenAuthenticationisSet()
$this->assertArrayHasKey('authentication', $metadata);
$this->assertEquals('foo', $metadata['authentication']);
}

/** @runInSeparateProcess */
public function testGetDefaultClientCertSource()
{
putenv('HOME=' . __DIR__ . '/fixtures4/valid');

$callback = CredentialsLoader::getDefaultClientCertSource();
$this->assertNotNull($callback);

$output = $callback();
$this->assertEquals('foo', $output);
}

/** @runInSeparateProcess */
public function testNonExistantDefaultClientCertSource()
{
putenv('HOME=');

$callback = CredentialsLoader::getDefaultClientCertSource();
$this->assertNull($callback);
}

/**
* @runInSeparateProcess
* @expectedException UnexpectedValueException
* @expectedExceptionMessage Invalid client cert source JSON
*/
public function testDefaultClientCertSourceInvalidJsonThrowsException()
{
putenv('HOME=' . __DIR__ . '/fixtures4/invalidjson');

CredentialsLoader::getDefaultClientCertSource();
}

/**
* @runInSeparateProcess
* @expectedException UnexpectedValueException
* @expectedExceptionMessage cert source requires "cert_provider_command"
*/
public function testDefaultClientCertSourceInvalidKeyThrowsException()
{
putenv('HOME=' . __DIR__ . '/fixtures4/invalidkey');

CredentialsLoader::getDefaultClientCertSource();
}

/**
* @runInSeparateProcess
* @expectedException UnexpectedValueException
* @expectedExceptionMessage cert source expects "cert_provider_command" to be an array
*/
public function testDefaultClientCertSourceInvalidValueThrowsException()
{
putenv('HOME=' . __DIR__ . '/fixtures4/invalidvalue');

CredentialsLoader::getDefaultClientCertSource();
}

/**
* @runInSeparateProcess
*/
public function testActualDefaultClientCertSource()
{
$clientCertSource = CredentialsLoader::getDefaultClientCertSource();
if (is_null($clientCertSource)) {
$this->markTestSkipped('No client cert source found');
}
$creds = $clientCertSource();
$this->assertTrue(is_string($creds));
$this->assertContains('-----BEGIN CERTIFICATE-----', $creds);
$this->assertContains('-----BEGIN PRIVATE KEY-----', $creds);
}

/**
* @runInSeparateProcess
* @expectedException RuntimeException
* @expectedExceptionMessage "cert_provider_command" failed with a nonzero exit code
*/
public function testDefaultClientCertSourceInvalidCmdThrowsException()
{
putenv('HOME=' . __DIR__ . '/fixtures4/invalidcmd');

$callback = CredentialsLoader::getDefaultClientCertSource();

// Close stderr so output doesnt show in our test runner
fclose(STDERR);

$callback();
}

/**
* @runInSeparateProcess
*/
public function testShouldLoadClientCertSourceInvalidValueIsFalse()
{
putenv(CredentialsLoader::MTLS_CERT_ENV_VAR . '=foo');

$this->assertFalse(CredentialsLoader::shouldLoadClientCertSource());
}

/**
* @runInSeparateProcess
*/
public function testShouldLoadClientCertSourceDefaultValueIsFalse()
{
putenv(CredentialsLoader::MTLS_CERT_ENV_VAR);

$this->assertFalse(CredentialsLoader::shouldLoadClientCertSource());
}

/**
* @runInSeparateProcess
*/
public function testShouldLoadClientCertSourceIsTrue()
{
putenv(CredentialsLoader::MTLS_CERT_ENV_VAR . '=true');

$this->assertTrue(CredentialsLoader::shouldLoadClientCertSource());
}
}

class TestCredentialsLoader extends CredentialsLoader
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"cert_provider_command":["invalid command", "2>", "/dev/null"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is not json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"this-is-the-wrong-key":["echo","foo"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"cert_provider_command":"this is the wrong value"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"cert_provider_command":["echo","foo"]}

0 comments on commit 930be8f

Please sign in to comment.