Skip to content

Commit

Permalink
Added optional headers to the AWS SigningDecorator. (#253)
Browse files Browse the repository at this point in the history
* Fix up UPGRADING.md.

Signed-off-by: dblock <[email protected]>

* Added optional headers to the AWS SigningDecorator.

Signed-off-by: dblock <[email protected]>

* Raise an error when host header is empty.

Signed-off-by: dblock <[email protected]>

* Require Host.

Co-authored-by: Kim Pepper <[email protected]>
Signed-off-by: Daniel (dB.) Doubrovkine <[email protected]>

* Fix tests.

Signed-off-by: dblock <[email protected]>

---------

Signed-off-by: dblock <[email protected]>
Signed-off-by: Daniel (dB.) Doubrovkine <[email protected]>
Co-authored-by: Kim Pepper <[email protected]>
  • Loading branch information
dblock and kimpepper authored Jan 18, 2025
1 parent d30188f commit 70e71ff
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 11 deletions.
1 change: 1 addition & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ $transport = (new \OpenSearch\TransportFactory())
->setRequestFactory($requestFactory)
->create();

$endpointFactory = new \OpenSearch\EndpointFactory();
$client = new \OpenSearch\Client($transport, $endpointFactory, []);

// Send a request to the 'info' endpoint.
Expand Down
2 changes: 2 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,6 @@ $client = \OpenSearch\ClientBuilder::fromConfig($config);

## Advanced Features

* [Authentication](guides/auth.md)
* [ML Commons](guides/ml-commons.md)
* [Sending Raw JSON Requests](guides/raw-request.md)
124 changes: 124 additions & 0 deletions guides/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
- [Authentication](#authentication)
- [Basic Auth](#basic-auth)
- [Using `\OpenSearch\ClientBuilder`](#using-opensearchclientbuilder)
- [Using a Psr Client](#using-a-psr-client)
- [IAM Authentication](#iam-authentication)
- [Using `\OpenSearch\ClientBuilder`](#using-opensearchclientbuilder-1)
- [Using a Psr Client](#using-a-psr-client-1)

# Authentication

OpenSearch allows you to use different methods for the authentication.

## Basic Auth

```php
$endpoint = "https://localhost:9200"
$username = "admin"
$password = "..."
```

### Using `\OpenSearch\ClientBuilder`

```php
$client = (new \OpenSearch\ClientBuilder())
->setBasicAuthentication($username, $password)
->setHosts([$endpoint])
->setSSLVerification(false) // for testing only
->build();
```

### Using a Psr Client

```php
$symfonyPsr18Client = (new \Symfony\Component\HttpClient\Psr18Client())->withOptions([
'base_uri' => $endpoint,
'auth_basic' => [$username, $password],
'verify_peer' => false, // for testing only
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
]);
```

## IAM Authentication

This library supports IAM-based authentication when communicating with OpenSearch clusters running in Amazon Managed OpenSearch and OpenSearch Serverless.

```php
$endpoint = "https://search-....us-west-2.es.amazonaws.com"
$region = "us-west-2"
$service = "es"
$aws_access_key_id = ...
$aws_secret_access_key = ...
$aws_session_token = ...
```

### Using `\OpenSearch\ClientBuilder`

```php
$client = (new \OpenSearch\ClientBuilder())
->setHosts([$endpoint])
->setSigV4Region($region)
->setSigV4Service('es')
->setSigV4CredentialProvider([
'key' => $aws_access_key_id,
'secret' => $aws_secret_access_key,
'token' => $aws_session_token
])
->build();
```

### Using a Psr Client

```php
$symfonyPsr18Client = (new \Symfony\Component\HttpClient\Psr18Client())->withOptions([
'base_uri' => $endpoint,
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
]);

$serializer = new \OpenSearch\Serializers\SmartSerializer();
$endpointFactory = new \OpenSearch\EndpointFactory();

$signer = new Aws\Signature\SignatureV4(
$service,
$region
);

$credentials = new Aws\Credentials\Credentials(
$aws_access_key_id,
$aws_secret_access_key,
$aws_session_token
);

$signingClient = new \OpenSearch\Aws\SigningClientDecorator(
$symfonyPsr18Client,
$credentials,
$signer,
[
'Host' => parse_url(getenv("ENDPOINT"))['host']
]
);

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

$transport = (new \OpenSearch\TransportFactory())
->setHttpClient($signingClient)
->setRequestFactory($requestFactory)
->create();

$client = new \OpenSearch\Client(
$transport,
$endpointFactory,
[]
);
```
6 changes: 3 additions & 3 deletions guides/raw-request.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Raw JSON Requests
# Sending Raw JSON Requests

Opensearch client implements many high-level APIs out of the box. However, there are times when you need to send a raw
OpenSearch client implements many high-level APIs out of the box. However, there are times when you need to send a raw
JSON request to the server. This can be done using the `request()` method of the client.

The `request()` method expects the following parameters:

| Parameter | Description |
|---------------|------------------------------------------------------------------------------|
| ------------- | ---------------------------------------------------------------------------- |
| `$method` | The HTTP method to use for the request, `GET`, `POST`, `PUT`, `DELETE`, etc. |
| `$uri` | The URI to send the request to, e.g. `/_search`. |
| `$attributes` | An array of request options. `body`, `params` & `options` |
Expand Down
16 changes: 16 additions & 0 deletions src/OpenSearch/Aws/SigningClientDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,31 @@
*/
class SigningClientDecorator implements ClientInterface
{
/**
* @param ClientInterface $inner The client to decorate.
* @param CredentialsInterface $credentials The AWS credentials to use for signing requests.
* @param SignatureInterface $signer The AWS signer to use for signing requests.
* @param array $headers Additional headers to add to the request. `Host` is required.
* @return void
*/
public function __construct(
protected ClientInterface $inner,
protected CredentialsInterface $credentials,
protected SignatureInterface $signer,
protected array $headers = []
) {
}

public function sendRequest(RequestInterface $request): ResponseInterface
{
foreach ($this->headers as $name => $value) {
$request = $request->withHeader($name, $value);
}

if (empty($request->getHeaderLine('Host'))) {
throw new \RuntimeException('Missing Host header.');
}

$request = $request->withHeader('x-amz-content-sha256', hash('sha256', (string) $request->getBody()));
$request = $this->signer->signRequest($request, $this->credentials);
return $this->inner->sendRequest($request);
Expand Down
11 changes: 9 additions & 2 deletions tests/Aws/SigningClientDecoratorGuzzleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ public function testGuzzleRequestIsSigned(): void
$credentials = $this->createMock(CredentialsInterface::class);
$signer = new SignatureV4('es', 'us-east-1');

$decorator = new SigningClientDecorator($guzzleClient, $credentials, $signer);
$decorator = new SigningClientDecorator(
$guzzleClient,
$credentials,
$signer,
[
'Host' => 'search.host'
]
);

$transport = (new TransportFactory())
->setHttpClient($decorator)
Expand All @@ -66,7 +73,7 @@ public function testGuzzleRequestIsSigned(): void

// Check the last request to ensure it was signed and has a host header.
$lastRequest = $mockHandler->getLastRequest();
$this->assertEquals('localhost:9200', $lastRequest->getHeader('Host')[0]);
$this->assertEquals('search.host', $lastRequest->getHeader('Host')[0]);
$this->assertNotEmpty($lastRequest->getHeader('x-amz-content-sha256'));
$this->assertNotEmpty($lastRequest->getHeader('x-amz-date'));
$this->assertNotEmpty($lastRequest->getHeader('Authorization'));
Expand Down
12 changes: 8 additions & 4 deletions tests/Aws/SigningClientDecoratorSymfonyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@ public function testSymfonyRequestIsSigned(): void
$credentials = $this->createMock(CredentialsInterface::class);
$signer = new SignatureV4('es', 'us-east-1');

$decorator = new SigningClientDecorator($symfonyPsr18Client, $credentials, $signer);
$decorator = new SigningClientDecorator(
$symfonyPsr18Client,
$credentials,
$signer,
[
'Host' => 'search.host'
]
);

$transport = (new TransportFactory())
->setHttpClient($decorator)
Expand All @@ -71,8 +78,5 @@ public function testSymfonyRequestIsSigned(): void
$this->assertArrayHasKey('x-amz-content-sha256', $requestHeaders);
$this->assertArrayHasKey('x-amz-date', $requestHeaders);
$this->assertArrayHasKey('host', $requestHeaders);

}


}
17 changes: 15 additions & 2 deletions tests/Aws/SigningClientDecoratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ public function testSendRequest()
->method('sendRequest')
->with(
$this->callback(function (Request $req): bool {
$this->assertEquals('localhost:9200', $req->getHeaderLine('Host'));
$this->assertEquals('server:443', $req->getHeaderLine('Host'));
$this->assertEquals('GET', $req->getMethod());
$this->assertTrue($req->hasHeader('Host'));
$this->assertTrue($req->hasHeader('Authorization'));
$this->assertTrue($req->hasHeader('x-amz-content-sha256'));
$this->assertTrue($req->hasHeader('x-amz-date'));
Expand All @@ -44,8 +45,20 @@ public function testSendRequest()
)
->willReturn($this->createMock(ResponseInterface::class));

$decorator = new SigningClientDecorator($client, $credentials, $signer);
$decorator = new SigningClientDecorator($client, $credentials, $signer, ['Host' => 'server:443']);
$request = new Request('GET', 'http://localhost:9200/_search');
$decorator->sendRequest($request);
}

public function testSendRequestWithEmptyHostHeader(): void
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Missing Host header.');
$client = $this->createMock(ClientInterface::class);
$credentials = $this->createMock(CredentialsInterface::class);
$signer = $this->createMock(SignatureV4::class);
$decorator = new SigningClientDecorator($client, $credentials, $signer, []);
$request = new Request('GET', 'http://localhost:9200/_search', ['Host' => '']);
$decorator->sendRequest($request);
}
}

0 comments on commit 70e71ff

Please sign in to comment.