Skip to content

Commit

Permalink
feat: add windows residency check (#553)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hectorhammett authored May 31, 2024
1 parent 23e8e69 commit ec13a53
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 9 deletions.
13 changes: 9 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,30 @@ permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: [ "8.0", "8.1", "8.2", "8.3" ]
name: PHP ${{matrix.php }} Unit Test
os: [ ubuntu-latest ]
include:
- os: windows-latest
php: "8.1"
runs-on: ${{ matrix.os }}
name: PHP ${{ matrix.php }} Unit Test ${{ matrix.os == 'windows-latest' && 'on Windows' || '' }}
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ matrix.os == 'windows-latest' && 'gmp, php_com_dotnet' || '' }}
- name: Install Dependencies
uses: nick-invision/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: composer install
- name: Run Script
run: vendor/bin/phpunit
run: vendor/bin/phpunit ${{ matrix.os == 'windows-latest' && '--filter GCECredentialsTest' || '' }}
test_lowest:
runs-on: ubuntu-latest
name: Test Prefer Lowest
Expand Down Expand Up @@ -73,4 +78,4 @@ jobs:
run: |
composer install
composer global require phpstan/phpstan:^1.8
~/.composer/vendor/bin/phpstan analyse
~/.composer/vendor/bin/phpstan analyse --autoload-file tests/phpstan-autoload.php
48 changes: 44 additions & 4 deletions src/Credentials/GCECredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

namespace Google\Auth\Credentials;

use COM;
use com_exception;
use Google\Auth\CredentialsLoader;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\HttpHandler\HttpClientCache;
Expand Down Expand Up @@ -110,6 +112,21 @@ class GCECredentials extends CredentialsLoader implements
*/
private const GKE_PRODUCT_NAME_FILE = '/sys/class/dmi/id/product_name';

/**
* The Windows Registry key path to the product name
*/
private const WINDOWS_REGISTRY_KEY_PATH = 'HKEY_LOCAL_MACHINE\\SYSTEM\\HardwareConfig\\Current\\';

/**
* The Windows registry key name for the product name
*/
private const WINDOWS_REGISTRY_KEY_NAME = 'SystemProductName';

/**
* The Name of the product expected from the windows registry
*/
private const PRODUCT_NAME = 'Google';

private const CRED_TYPE = 'mds';

/**
Expand Down Expand Up @@ -377,9 +394,10 @@ public static function onGce(callable $httpHandler = null)
}
}

if (PHP_OS === 'Windows') {
// @TODO: implement GCE residency detection on Windows
return false;
if (PHP_OS === 'Windows' || PHP_OS === 'WINNT') {
return self::detectResidencyWindows(
self::WINDOWS_REGISTRY_KEY_PATH . self::WINDOWS_REGISTRY_KEY_NAME
);
}

// Detect GCE residency on Linux
Expand All @@ -390,11 +408,33 @@ private static function detectResidencyLinux(string $productNameFile): bool
{
if (file_exists($productNameFile)) {
$productName = trim((string) file_get_contents($productNameFile));
return 0 === strpos($productName, 'Google');
return 0 === strpos($productName, self::PRODUCT_NAME);
}
return false;
}

private static function detectResidencyWindows(string $registryProductKey): bool
{
if (!class_exists(COM::class)) {
// the COM extension must be installed and enabled to detect Windows residency
// see https://www.php.net/manual/en/book.com.php
return false;
}

$shell = new COM('WScript.Shell');
$productName = null;

try {
$productName = $shell->regRead($registryProductKey);
} catch(com_exception) {
// This means that we tried to read a key that doesn't exist on the registry
// which might mean that it is a windows instance that is not on GCE
return false;
}

return 0 === strpos($productName, self::PRODUCT_NAME);
}

/**
* Implements FetchAuthTokenInterface#fetchAuthToken.
*
Expand Down
66 changes: 65 additions & 1 deletion tests/Credentials/GCECredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

namespace Google\Auth\Tests\Credentials;

use COM;
use Exception;
use Google\Auth\Credentials\GCECredentials;
use Google\Auth\HttpHandler\HttpClientCache;
Expand All @@ -29,6 +30,7 @@
use InvalidArgumentException;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use ReflectionClass;

/**
* @group credentials
Expand Down Expand Up @@ -96,7 +98,7 @@ public function testCheckProductNameFile()
{
$tmpFile = tempnam(sys_get_temp_dir(), 'gce-test-product-name');

$method = (new \ReflectionClass(GCECredentials::class))
$method = (new ReflectionClass(GCECredentials::class))
->getMethod('detectResidencyLinux');
$method->setAccessible(true);

Expand Down Expand Up @@ -124,6 +126,68 @@ public function testOnGceWithResidency()
$this->assertTrue(GCECredentials::onGCE($httpHandler));
}

public function testOnWindowsGceWithResidencyWithNoCom()
{
if (PHP_OS !== 'Windows' && PHP_OS !== 'WINNT') {
$this->markTestSkipped('This test only works while running on Windows');
}

if (class_exists(COM::class)) {
throw $this->markTestSkipped('This test in meant to handle when the COM extension is not present');
}

$method = (new ReflectionClass(GCECredentials::class))
->getMethod('detectResidencyWindows');

$method->setAccessible(true);

$this->assertFalse($method->invoke(null, 'thisShouldBeFalse'));
}

public function testOnWindowsGceWithResidencyNotOnGCE()
{
if (!class_exists(COM::class)) {
throw $this->markTestSkipped('This test only works while running on windows COM extension enabled');
}

if (GCECredentials::onGce()) {
$this->markTestSkipped('This test runs only on non GCE machines');
}

$keyPathProperty = 'HKEY_LOCAL_MACHINE\\SYSTEM\\HardwareConfig\\Current\\';
$keyName = 'SystemProductName';

$method = (new ReflectionClass(GCECredentials::class))
->getMethod('detectResidencyWindows');
$method->setAccessible(true);

$this->assertFalse($method->invoke(null, $keyPathProperty . $keyName));
}

public function testOnWindowsGceWithResidency()
{
if (PHP_OS !== 'Windows' && PHP_OS !== 'WINNT') {
$this->markTestSkipped('This test only works while running on Windows');
}

if (!class_exists(COM::class)) {
$this->markTestSkipped('This test only works with the COM extension enabled');
}

if (!GCECredentials::onGce()) {
$this->markTestSkipped('This test only works while running on GCE');
}

$keyPathProperty = 'HKEY_LOCAL_MACHINE\\SYSTEM\\HardwareConfig\\Current\\';
$keyName = 'SystemProductName';

$method = (new ReflectionClass(GCECredentials::class))
->getMethod('detectResidencyWindows');
$method->setAccessible(true);

$this->assertTrue($method->invoke(null, $keyPathProperty . $keyName));
}

public function testOnGCEIsFalseOnOkStatusWithoutExpectedHeader()
{
$httpHandler = getHandler([
Expand Down
20 changes: 20 additions & 0 deletions tests/phpstan-autoload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

require_once __DIR__ . '/../vendor/autoload.php';

// moc the windows-only COM class so that phpstan understands it
if (!class_exists(COM::class)) {
class COM
{
public function __construct(string $command)
{
//do nothing
}

public function regRead(string $key): string
{
// do nothing
return '';
}
}
}

0 comments on commit ec13a53

Please sign in to comment.