Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added optional headers to the AWS SigningDecorator. #253

Merged
merged 6 commits into from
Jan 18, 2025
Merged
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
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);
}
}
Loading