Skip to content

Commit

Permalink
feat: add universe domain support to core, bigquery, storage, and pub…
Browse files Browse the repository at this point in the history
…sub (#6850)
  • Loading branch information
bshaffer authored Jan 9, 2024
1 parent d80219d commit 52bc721
Show file tree
Hide file tree
Showing 19 changed files with 353 additions and 47 deletions.
2 changes: 1 addition & 1 deletion BigQuery/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"minimum-stability": "stable",
"require": {
"php": ">=7.4",
"google/cloud-core": "^1.52.7",
"google/cloud-core": "^1.53",
"ramsey/uuid": "^3.0|^4.0"
},
"require-dev": {
Expand Down
11 changes: 9 additions & 2 deletions BigQuery/src/Connection/Rest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

namespace Google\Cloud\BigQuery\Connection;

use Google\Auth\GetUniverseDomainInterface;
use Google\Cloud\BigQuery\BigQueryClient;
use Google\Cloud\BigQuery\Connection\ConnectionInterface;
use Google\Cloud\Core\RequestBuilder;
Expand All @@ -43,8 +44,13 @@ class Rest implements ConnectionInterface
*/
const BASE_URI = 'https://www.googleapis.com/bigquery/v2/';

/**
* @deprecated
*/
const DEFAULT_API_ENDPOINT = 'https://bigquery.googleapis.com';

private const DEFAULT_API_ENDPOINT_TEMPLATE = 'https://bigquery.UNIVERSE_DOMAIN';

/**
* @deprecated
*/
Expand All @@ -65,10 +71,11 @@ public function __construct(array $config = [])
$config += [
'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/bigquery-v2.json',
'componentVersion' => BigQueryClient::VERSION,
'apiEndpoint' => self::DEFAULT_API_ENDPOINT
'apiEndpoint' => null,
'universeDomain' => GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN,
];

$apiEndpoint = $this->getApiEndpoint(self::DEFAULT_API_ENDPOINT, $config);
$apiEndpoint = $this->getApiEndpoint(null, $config, self::DEFAULT_API_ENDPOINT_TEMPLATE);

$this->setRequestWrapper(new RequestWrapper($config));
$this->setRequestBuilder(new RequestBuilder(
Expand Down
29 changes: 21 additions & 8 deletions BigQuery/tests/Unit/Connection/RestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Http\Message\RequestInterface;
use UnexpectedValueException;

/**
* @group bigquery
Expand All @@ -45,21 +46,33 @@ public function setUp(): void
$this->successBody = '{"canI":"kickIt"}';
}

public function testApiEndpoint()
/**
* @dataProvider clientUniverseDomainConfigProvider
*/
public function testApiEndpointForUniverseDomain($config, $expectedEndpoint, $expectException = false)
{
$endpoint = 'https://foobar.com/';
$rest = TestHelpers::stub(Rest::class, [
[
'apiEndpoint' => $endpoint
]
], ['requestBuilder']);
if ($expectException) {
$this->expectException(UnexpectedValueException::class);
}
$rest = TestHelpers::stub(Rest::class, [$config], ['requestBuilder']);

$rb = $rest->___getProperty('requestBuilder');
$r = new \ReflectionObject($rb);
$p = $r->getProperty('baseUri');
$p->setAccessible(true);

$this->assertEquals($endpoint . 'bigquery/v2/', $p->getValue($rb));
$this->assertEquals($expectedEndpoint, $p->getValue($rb));
}

public function clientUniverseDomainConfigProvider()
{
return [
[[], 'https://bigquery.googleapis.com/bigquery/v2/'], // default
[['apiEndpoint' => 'https://foobar.com'], 'https://foobar.com/bigquery/v2/'],
[['universeDomain' => 'googleapis.com'], 'https://bigquery.googleapis.com/bigquery/v2/'],
[['universeDomain' => 'abc.def.ghi'], 'https://bigquery.abc.def.ghi/bigquery/v2/'],
[['universeDomain' => null], '', true],
];
}

/**
Expand Down
2 changes: 1 addition & 1 deletion Core/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"require": {
"php": ">=7.4",
"rize/uri-template": "~0.3",
"google/auth": "^1.34.0",
"google/auth": "^1.34",
"guzzlehttp/guzzle": "^6.5.8|^7.4.4",
"guzzlehttp/promises": "^1.4||^2.0",
"guzzlehttp/psr7": "^2.6",
Expand Down
12 changes: 9 additions & 3 deletions Core/src/GrpcTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

namespace Google\Cloud\Core;

use Google\Auth\GetUniverseDomainInterface;
use Google\ApiCore\CredentialsWrapper;
use Google\Cloud\Core\ArrayTrait;
use Google\Cloud\Core\Duration;
Expand Down Expand Up @@ -94,10 +95,14 @@ public function send(callable $request, array $args, $whitelisted = false)
*
* @param string $version
* @param callable|null $authHttpHandler
* @param string|null $universeDomain
* @return array
*/
private function getGaxConfig($version, callable $authHttpHandler = null)
{
private function getGaxConfig(
$version,
callable $authHttpHandler = null,
string $universeDomain = null
) {
$config = [
'libName' => 'gccl',
'libVersion' => $version,
Expand All @@ -110,7 +115,8 @@ private function getGaxConfig($version, callable $authHttpHandler = null)
if (class_exists(CredentialsWrapper::class)) {
$config['credentials'] = new CredentialsWrapper(
$this->requestWrapper->getCredentialsFetcher(),
$authHttpHandler
$authHttpHandler,
$universeDomain ?: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN
);
} else {
$config += [
Expand Down
57 changes: 56 additions & 1 deletion Core/src/RequestWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
use Google\Auth\FetchAuthTokenCache;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\GetUniverseDomainInterface;
use Google\Auth\HttpHandler\Guzzle6HttpHandler;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\UpdateMetadataInterface;
use Google\Cloud\Core\Exception\ServiceException;
use Google\Cloud\Core\RequestWrapperTrait;
use Google\Cloud\Core\Exception\GoogleException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Utils;
Expand Down Expand Up @@ -94,6 +96,16 @@ class RequestWrapper
*/
private $calcDelayFunction;

/**
* @var string The universe domain to verify against the credentials.
*/
private string $universeDomain;

/**
* @var bool Ensure we only check the universe domain once.
*/
private bool $hasCheckedUniverse = false;

/**
* @param array $config [optional] {
* Configuration options. Please see
Expand Down Expand Up @@ -125,6 +137,7 @@ class RequestWrapper
* @type callable $restCalcDelayFunction Sets the conditions for
* determining how long to wait between attempts to retry. Function
* signature should match: `function (int $attempt) : int`.
* @type string $universeDomain The expected universe of the credentials. Defaults to "googleapis.com".
* }
*/
public function __construct(array $config = [])
Expand All @@ -140,7 +153,8 @@ public function __construct(array $config = [])
'componentVersion' => null,
'restRetryFunction' => null,
'restDelayFunction' => null,
'restCalcDelayFunction' => null
'restCalcDelayFunction' => null,
'universeDomain' => GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN,
];

$this->componentVersion = $config['componentVersion'];
Expand All @@ -155,6 +169,7 @@ public function __construct(array $config = [])
$this->httpHandler = $config['httpHandler'] ?: HttpHandlerFactory::build();
$this->authHttpHandler = $config['authHttpHandler'] ?: $this->httpHandler;
$this->asyncHttpHandler = $config['asyncHttpHandler'] ?: $this->buildDefaultAsyncHandler();
$this->universeDomain = $config['universeDomain'];

if ($this->credentialsFetcher instanceof AnonymousCredentials) {
$this->shouldSignRequest = false;
Expand Down Expand Up @@ -313,9 +328,14 @@ private function applyHeaders(RequestInterface $request, array $options = [])
$quotaProject = $this->quotaProject;

if ($this->accessToken) {
// if an access token is provided, check the universe domain against "googleapis.com"
$this->checkUniverseDomain(null);
$request = $request->withHeader('authorization', 'Bearer ' . $this->accessToken);
} else {
// if a credentials fetcher is provided, check the universe domain against the
// credential's universe domain
$credentialsFetcher = $this->getCredentialsFetcher();
$this->checkUniverseDomain($credentialsFetcher);
$request = $this->addAuthHeaders($request, $credentialsFetcher);

if ($credentialsFetcher instanceof GetQuotaProjectInterface) {
Expand All @@ -326,6 +346,9 @@ private function applyHeaders(RequestInterface $request, array $options = [])
if ($quotaProject) {
$request = $request->withHeader('X-Goog-User-Project', $quotaProject);
}
} else {
// If we are not signing the request, check the universe domain against "googleapis.com"
$this->checkUniverseDomain(null);
}

return $request;
Expand Down Expand Up @@ -484,4 +507,36 @@ private function buildDefaultAsyncHandler()
? [$this->httpHandler, 'async']
: [HttpHandlerFactory::build(), 'async'];
}

/**
* Verify that the expected universe domain matches the universe domain from the credentials.
*/
private function checkUniverseDomain(FetchAuthTokenInterface $credentialsFetcher = null)
{
if (false === $this->hasCheckedUniverse) {
if ($this->universeDomain === '') {
throw new GoogleException('The universe domain cannot be empty.');
}
if (is_null($credentialsFetcher)) {
if ($this->universeDomain !== GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN) {
throw new GoogleException(sprintf(
'The accessToken option is not supported outside of the default universe domain (%s).',
GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN
));
}
} else {
$credentialsUniverse = $credentialsFetcher instanceof GetUniverseDomainInterface
? $credentialsFetcher->getUniverseDomain()
: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN;
if ($credentialsUniverse !== $this->universeDomain) {
throw new GoogleException(sprintf(
'The configured universe domain (%s) does not match the credential universe domain (%s)',
$this->universeDomain,
$credentialsUniverse
));
}
}
$this->hasCheckedUniverse = true;
}
}
}
3 changes: 3 additions & 0 deletions Core/src/RequestWrapperTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ trait RequestWrapperTrait
*/
private $quotaProject;

private string $universeDomain;
private bool $hasCheckedUniverse = false;

/**
* Sets common defaults between request wrappers.
*
Expand Down
40 changes: 33 additions & 7 deletions Core/src/RestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

use Google\Cloud\Core\Exception\NotFoundException;
use Google\Cloud\Core\Exception\ServiceException;
use UnexpectedValueException;

/**
* Provides shared functionality for REST service implementations.
Expand Down Expand Up @@ -118,20 +119,45 @@ public function send($resource, $method, array $options = [], $whitelisted = fal
*
* @param string $default
* @param array $config
* @param string $apiEndpointTemplate
* @return string
*/
private function getApiEndpoint($default, array $config)
private function getApiEndpoint($default, array $config, string $apiEndpointTemplate = null)
{
$res = $config['apiEndpoint'] ?? $default;
// If the $default parameter is provided, or the user has set an "apiEndoint" config option,
// fall back to the previous behavior.
if ($res = $config['apiEndpoint'] ?? $default) {
if (substr($res, -1) !== '/') {
$res = $res . '/';
}

if (strpos($res, '//') === false) {
$res = 'https://' . $res;
}

return $res;
}

if (substr($res, -1) !== '/') {
$res = $res . '/';
// One of the $default or the $template must always be set
if (!$apiEndpointTemplate) {
throw new UnexpectedValueException(
'An API endpoint template must be provided if no "apiEndpoint" or default endpoint is set.'
);
}

if (strpos($res, '//') === false) {
$res = 'https://' . $res;
if (!isset($config['universeDomain'])) {
throw new UnexpectedValueException(
'The "universeDomain" config value must be set to use the default API endpoint template.'
);
}

return $res;
$apiEndpoint = str_replace(
'UNIVERSE_DOMAIN',
$config['universeDomain'],
$apiEndpointTemplate
);

// Preserve the behavior of guaranteeing a trailing "/"
return $apiEndpoint . (substr($apiEndpoint, -1) !== '/' ? '/' : '');
}
}
Loading

0 comments on commit 52bc721

Please sign in to comment.