Skip to content

Commit

Permalink
Adds a AwsSigningHttpClientFactory
Browse files Browse the repository at this point in the history
Signed-off-by: Kim Pepper <[email protected]>
  • Loading branch information
kimpepper committed Feb 27, 2025
1 parent 88235f6 commit 059936d
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 37 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## [Unreleased]

### Added
- Added a AwsSigningHttpClientFactory to create a signing HTTP client ([#314](https://github.com/opensearch-project/opensearch-php/pull/314))
### Changed
### Deprecated
### Removed
Expand Down
79 changes: 42 additions & 37 deletions guides/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,53 +72,58 @@ $client = (new \OpenSearch\ClientBuilder())

### Using a Psr Client

```php
$symfonyPsr18Client = (new \Symfony\Component\HttpClient\Psr18Client())->withOptions([
'base_uri' => $endpoint,
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
]);
We can use the `AwsSigningHttpClientFactory` to create an HTTP Client to sign the requests using the AWS SDK for PHP.

$serializer = new \OpenSearch\Serializers\SmartSerializer();
$endpointFactory = new \OpenSearch\EndpointFactory();
Require a PSR-18 client (e.g. Symfony) and the AWS SDK for PHP:

$signer = new Aws\Signature\SignatureV4(
$service,
$region
);
```bash
composer require symfony/http-client aws/aws-sdk-php
```

$credentials = new Aws\Credentials\Credentials(
$aws_access_key_id,
$aws_secret_access_key,
$aws_session_token
);
Create a PSR HTTP Client (e.g. Symfony):

$signingClient = new \OpenSearch\Aws\SigningClientDecorator(
$symfonyPsr18Client,
$credentials,
$signer,
[
'Host' => parse_url(getenv("ENDPOINT"))['host']
]
);
```php
$endpoint = 'https://search-example.us-west-2.es.amazonaws.com';

$symfonyClient = (new \OpenSearch\HttpClient\SymfonyHttpClientFactory()->create([
'base_uri' => $endpoint, // required.
]);
```

Use the `AwsSigningHttpClientFactory` to create a signing HTTP client:
```php
$signingHttpClient = (new \OpenSearch\Aws\AwsSigningHttpClientFactory($symfonyClient))->create([
'access_key' => '...', // optional. Will fallback to AWS SDK credential discovery.
'secret_key' => '...', // optional. Will fallback to AWS SDK credential discovery.
'base_uri' => $endpoint, // required for signing.
'region' => 'us-west-2', // required for signing.
'session_token' => '...', // optional.
'service' => 'es', // optional. Allowed values are: 'es', 'aoss'. Defaults to 'es'.
],
]);
```

Create a request factory:
```php
$serializer = new \OpenSearch\Serializers\SmartSerializer();

$requestFactory = new \OpenSearch\RequestFactory(
$symfonyPsr18Client,
$symfonyPsr18Client,
$symfonyPsr18Client,
$symfonyClient,
$symfonyClient,
$symfonyClient,
$serializer,
);
```

Create a transport:
```php
$transport = (new \OpenSearch\TransportFactory())
->setHttpClient($signingClient)
->setHttpClient($signingHttpClient)
->setRequestFactory($requestFactory)
->create();
```

$client = new \OpenSearch\Client(
$transport,
$endpointFactory,
[]
);
```
Create the OpenSearch client:
```php
$client = new \OpenSearch\Client($transport, new \OpenSearch\EndpointFactory());
```
106 changes: 106 additions & 0 deletions src/OpenSearch/Aws/AwsSigningHttpClientFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

namespace OpenSearch\Aws;

use Aws\Credentials\CredentialProvider;
use Aws\Credentials\Credentials;
use Aws\Exception\CredentialsException;
use Aws\Signature\SignatureInterface;
use Aws\Signature\SignatureV4;
use OpenSearch\HttpClient\HttpClientFactoryInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;

/**
* A decorator client that signs requests using the provided AWS credentials.
*/
class AwsSigningHttpClientFactory implements HttpClientFactoryInterface
{
public function __construct(
protected ClientInterface $innerClient,
protected ?SignatureInterface $signer = null,
protected ?CredentialProvider $provider = null,
protected ?LoggerInterface $logger = null,
) {
}

/**
* {@inheritdoc}
*/
public function create(array $options): ClientInterface
{
if (!isset($options['base_uri'])) {
throw new \InvalidArgumentException('The base_uri option is required.');
}

// Get the credentials.
$provider = $this->getCredentialProvider($options);
$promise = $provider();
try {
$credentials = $promise->wait();
} catch (CredentialsException $e) {
$this->logger?->error('Failed to get AWS credentials: @message', ['@message' => $e->getMessage()]);
$credentials = new Credentials('', '');
}

// Get the signer.
$signer = $this->getSigner($options);

// Host header is required.
$host = parse_url($options['base_uri'], PHP_URL_HOST);

return new SigningClientDecorator($this->innerClient, $credentials, $signer, ['host' => $host]);
}

/**
* Gets the credential provider.
*
* @param array<string,mixed> $options
* The options array.
*/
protected function getCredentialProvider(array $options): CredentialProvider|\Closure|null|callable
{
// Check for a provided credential provider.
if ($this->provider) {
return $this->provider;
}

// Check for provided access key and secret.
if (isset($options['access_key']) && isset($options['secret_key'])) {
return CredentialProvider::fromCredentials(
new Credentials(
$options['access_key'],
$options['secret_key'],
$options['session_token'] ?? null,
)
);
}

// Fallback to the default provider.
return CredentialProvider::defaultProvider();
}

/**
* Gets the request signer.
*
* @param array<string,string> $options
* The options.
*/
protected function getSigner(array $options): SignatureInterface
{
if ($this->signer) {
return $this->signer;
}

if (!isset($options['region'])) {
throw new \InvalidArgumentException('The region option is required.');
}

$service = $options['service'] ?? 'es';

return new SignatureV4($service, $options['region'], $options);
}

}
36 changes: 36 additions & 0 deletions tests/Aws/AwsSigningHttpClientFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace OpenSearch\Tests\Aws;

use OpenSearch\Aws\AwsSigningHttpClientFactory;
use OpenSearch\HttpClient\SymfonyHttpClientFactory;
use PHPUnit\Framework\TestCase;
use Psr\Http\Client\ClientInterface;

/**
* @coversDefaultClass \OpenSearch\Aws\AwsSigningHttpClientFactory
*/
class AwsSigningHttpClientFactoryTest extends TestCase
{
/**
* @covers ::create
*/
public function testCreate(): void
{
$symfonyClient = (new SymfonyHttpClientFactory())->create([
'base_uri' => 'http://localhost:9200',
]);
$factory = new AwsSigningHttpClientFactory($symfonyClient);
$client = $factory->create([
'base_uri' => 'http://localhost:9200',
'access_key' => 'foo',
'secret_key' => 'bar',
'region' => 'us-east-1',
]);

// Check we get a client back.
$this->assertInstanceOf(ClientInterface::class, $client);
}
}

0 comments on commit 059936d

Please sign in to comment.