Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions doc/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ If you already have a valid token, you can use it to authenticate.

.. sample:: Setup/token_id.php

Overriding catalog endpoints
----------------------------

Normally the endpoint URL for a service is read from the Identity catalog returned by the authentication request.
In some cases that URL is not the one that should be used, for example when an application credential is used to
access an object container that belongs to another project. The ``catalog_overrides`` option lets you replace the
catalog URL for a service.

Each override must contain a ``url`` and at least one of ``name`` or ``type``. Every key that is specified must
match the requested service, while omitted keys act as wildcards. The ``region`` and ``interface`` keys are
therefore optional and can be used to restrict the override further.

.. sample:: Setup/catalog_override.php

If no override matches the requested service, the URL from the catalog is used as usual.

Other options
-------------

Expand Down
21 changes: 21 additions & 0 deletions samples/Setup/catalog_override.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

require 'vendor/autoload.php';

$openstack = new OpenStack\OpenStack([
'authUrl' => '{authUrl}',
'region' => '{region}',
'user' => [
'id' => '{userId}',
'password' => '{password}'
],
'catalog_overrides' => [
[
'name' => 'swift',
'type' => 'object-store',
'region' => '{region}',
'interface' => 'public',
'url' => '{serviceUrl}',
],
],
]);
40 changes: 40 additions & 0 deletions src/Identity/v3/Models/Catalog.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,46 @@ public function populateFromArray(array $data): self
return $this;
}

/**
* Override a given service's predetermined endpoint URL.
*
* @param string $name the name of the service as it appears in the catalog
* @param string $type the type of the service as it appears in the catalog
* @param string $region the region of the service as it appears in the catalog
* @param string $interface the interface of the service as it appears in the catalog
* @param array $overrides endpoint overrides to match against
*
* @return string|null NULL if no URL found
*/
public function getServiceUrlOverride(
string $name,
string $type,
string $region,
string $interface,
array $overrides
): ?string {
foreach ($overrides as $override) {
if (empty($override['name']) && empty($override['type'])) {
throw new \RuntimeException('Endpoint override must specify either a "name" or a "type".');
}

if (empty($override['url'])) {
throw new \RuntimeException(sprintf("Endpoint override must specify an \"url\".\nName: %s\nType: %s\nRegion: %s\nInterface: %s", $override['name'] ?? '', $override['type'] ?? '', $override['region'] ?? '', $override['interface'] ?? ''));
}

if (
(empty($override['name']) || $name == $override['name'])
&& (empty($override['type']) || $type == $override['type'])
&& (empty($override['region']) || $region == $override['region'])
&& (empty($override['interface']) || $interface == $override['interface'])
) {
return $override['url'];
}
}

return null;
}

/**
* Retrieve a base URL for a service, according to its catalog name, type, region.
*
Expand Down
13 changes: 13 additions & 0 deletions src/Identity/v3/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ public function authenticate(array $options): array
$region = $options['region'];
$interface = $options['interface'] ?? Enum::INTERFACE_PUBLIC;

if (!empty($options['catalog_overrides'])) {
$baseUrl = $token->catalog->getServiceUrlOverride(
$name,
$type,
$region,
$interface,
$options['catalog_overrides']
);
if ($baseUrl) {
return [$token, $baseUrl];
}
}

if ($baseUrl = $token->catalog->getServiceUrl($name, $type, $region, $interface)) {
return [$token, $baseUrl];
}
Expand Down
1 change: 1 addition & 0 deletions src/OpenStack.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class OpenStack
* ['messageFormatter'] = (MessageFormatter) Must set if debugLog is true [OPTIONAL]
* ['requestOptions'] = (array) Guzzle Http request options [OPTIONAL]
* ['cachedToken'] = (array) Cached token credential [OPTIONAL]
* ['catalog_overrides'] = (array) Override catalog endpoint URLs [OPTIONAL]
*/
public function __construct(array $options = [], ?Builder $builder = null)
{
Expand Down
123 changes: 123 additions & 0 deletions tests/unit/Identity/v3/Models/CatalogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,129 @@ public function test_it_returns_service_url()
self::assertEquals($url, $this->catalog->getServiceUrl('foo', 'bar', 'baz', ''));
}

public function test_it_returns_service_url_override_matching_name_and_type()
{
$overrides = [
['name' => 'swift', 'type' => 'object-store', 'url' => 'http://override.example'],
];

self::assertEquals(
'http://override.example',
$this->catalog->getServiceUrlOverride('swift', 'object-store', 'RegionOne', 'public', $overrides)
);
}

public function test_it_returns_service_url_override_matching_only_name()
{
$overrides = [
['name' => 'swift', 'url' => 'http://override.example'],
];

self::assertEquals(
'http://override.example',
$this->catalog->getServiceUrlOverride('swift', 'object-store', 'RegionOne', 'public', $overrides)
);
}

public function test_it_returns_service_url_override_matching_only_type()
{
$overrides = [
['type' => 'object-store', 'url' => 'http://override.example'],
];

self::assertEquals(
'http://override.example',
$this->catalog->getServiceUrlOverride('swift', 'object-store', 'RegionOne', 'public', $overrides)
);
}

public function test_it_returns_service_url_override_matching_region_and_interface()
{
$overrides = [
[
'name' => 'swift',
'region' => 'RegionOne',
'interface' => 'public',
'url' => 'http://override.example',
],
];

self::assertEquals(
'http://override.example',
$this->catalog->getServiceUrlOverride('swift', 'object-store', 'RegionOne', 'public', $overrides)
);
}

public function test_it_returns_null_when_no_override_matches()
{
$overrides = [
['name' => 'nova', 'url' => 'http://override.example'],
];

self::assertNull(
$this->catalog->getServiceUrlOverride('swift', 'object-store', 'RegionOne', 'public', $overrides)
);
}

public function test_it_returns_null_when_region_or_interface_does_not_match()
{
$overrides = [
[
'name' => 'swift',
'region' => 'RegionTwo',
'interface' => 'internal',
'url' => 'http://override.example',
],
];

self::assertNull(
$this->catalog->getServiceUrlOverride('swift', 'object-store', 'RegionOne', 'public', $overrides)
);
}

public function test_it_returns_null_for_empty_overrides()
{
self::assertNull(
$this->catalog->getServiceUrlOverride('swift', 'object-store', 'RegionOne', 'public', [])
);
}

public function test_it_throws_when_matching_override_has_neither_name_nor_type()
{
$overrides = [
['region' => 'RegionOne', 'url' => 'http://override.example'],
];

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('must specify either a "name" or a "type"');

$this->catalog->getServiceUrlOverride('swift', 'object-store', 'RegionOne', 'public', $overrides);
}

public function test_it_throws_when_matching_override_has_no_url()
{
$overrides = [
['name' => 'swift'],
];

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('must specify an "url"');

$this->catalog->getServiceUrlOverride('swift', 'object-store', 'RegionOne', 'public', $overrides);
}

public function test_it_throws_when_invalid_override_does_not_match()
{
$overrides = [
['name' => 'nova'],
];

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('must specify an "url"');

$this->catalog->getServiceUrlOverride('swift', 'object-store', 'RegionOne', 'public', $overrides);
}

public function test_it_throws_if_no_url_found()
{
$service = $this->prophesize(Service::class);
Expand Down
82 changes: 82 additions & 0 deletions tests/unit/Identity/v3/ServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,68 @@ public function test_it_authenticates_using_cache_token()
self::assertEquals('http://example.org:8080/v1/AUTH_e00abf65afca49609eedd163c515cf10', $url);
}

public function test_it_authenticates_using_catalog_override()
{
$userOptions = [
'catalogName' => 'swift',
'catalogType' => 'object-store',
'region' => 'RegionOne',
'cachedToken' => $this->cachedToken(),
'catalog_overrides' => [
[
'name' => 'swift',
'type' => 'object-store',
'region' => 'RegionOne',
'interface' => 'public',
'url' => 'http://override.example/v1/AUTH_override',
],
],
];

[$token, $url] = $this->service->authenticate($userOptions);

self::assertInstanceOf(Models\Token::class, $token);
self::assertEquals('http://override.example/v1/AUTH_override', $url);
}

public function test_it_uses_public_interface_by_default_for_catalog_override()
{
$userOptions = [
'catalogName' => 'swift',
'catalogType' => 'object-store',
'region' => 'RegionOne',
'cachedToken' => $this->cachedToken(),
'catalog_overrides' => [
[
'name' => 'swift',
'interface' => Enum::INTERFACE_PUBLIC,
'url' => 'http://override.example/v1/AUTH_override',
],
],
];

$result = $this->service->authenticate($userOptions);

self::assertEquals('http://override.example/v1/AUTH_override', $result[1]);
}

public function test_it_falls_back_to_catalog_url_when_override_does_not_match()
{
$userOptions = [
'catalogName' => 'swift',
'catalogType' => 'object-store',
'region' => 'RegionOne',
'cachedToken' => $this->cachedToken(),
'catalog_overrides' => [
['name' => 'nova', 'url' => 'http://override.example/v1/AUTH_override'],
],
];

$result = $this->service->authenticate($userOptions);

self::assertEquals('http://example.org:8080/v1/AUTH_catalog', $result[1]);
}

public function test_it_authenticates_and_throws_exception_when_authenticate_with_expired_cached_token()
{
$cachedToken = [
Expand Down Expand Up @@ -684,4 +746,24 @@ public function test_it_gets_endpoint()
{
$this->getTest($this->createFn($this->service, 'getEndpoint', 'id'), 'endpoint');
}

private function cachedToken(): array
{
return [
'expires_at' => '2199-11-24T04:47:49.000000Z',
'catalog' => [
[
'name' => 'swift',
'type' => 'object-store',
'endpoints' => [
[
'region' => 'RegionOne',
'interface' => 'public',
'url' => 'http://example.org:8080/v1/AUTH_catalog',
],
],
],
],
];
}
}