From e1849b164d2daabfe8f02a55a2aefd8bcd29913b Mon Sep 17 00:00:00 2001 From: dblock Date: Wed, 8 Jan 2025 10:24:42 -0500 Subject: [PATCH 1/5] Fix up UPGRADING.md. Signed-off-by: dblock --- UPGRADING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/UPGRADING.md b/UPGRADING.md index 33f309764..5182b0ae6 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -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. From a944379c336d01d4427cb294944f68816d1a1ad6 Mon Sep 17 00:00:00 2001 From: dblock Date: Fri, 10 Jan 2025 10:30:25 -0500 Subject: [PATCH 2/5] Added optional headers to the AWS SigningDecorator. Signed-off-by: dblock --- USER_GUIDE.md | 2 + guides/auth.md | 124 ++++++++++++++++++ guides/raw-request.md | 6 +- src/OpenSearch/Aws/SigningClientDecorator.php | 12 ++ tests/Aws/SigningClientDecoratorTest.php | 5 +- 5 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 guides/auth.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index c0c90a39e..e37a2a18c 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -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) diff --git a/guides/auth.md b/guides/auth.md new file mode 100644 index 000000000..5ad8bdb35 --- /dev/null +++ b/guides/auth.md @@ -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, + [] +); +``` \ No newline at end of file diff --git a/guides/raw-request.md b/guides/raw-request.md index 5424f8cf0..48a28b3b2 100644 --- a/guides/raw-request.md +++ b/guides/raw-request.md @@ -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` | diff --git a/src/OpenSearch/Aws/SigningClientDecorator.php b/src/OpenSearch/Aws/SigningClientDecorator.php index e54752f32..bdf2409c9 100644 --- a/src/OpenSearch/Aws/SigningClientDecorator.php +++ b/src/OpenSearch/Aws/SigningClientDecorator.php @@ -13,15 +13,27 @@ */ 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, usually `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); + } + $request = $request->withHeader('x-amz-content-sha256', hash('sha256', (string) $request->getBody())); $request = $this->signer->signRequest($request, $this->credentials); return $this->inner->sendRequest($request); diff --git a/tests/Aws/SigningClientDecoratorTest.php b/tests/Aws/SigningClientDecoratorTest.php index 582e8f3ee..617be2460 100644 --- a/tests/Aws/SigningClientDecoratorTest.php +++ b/tests/Aws/SigningClientDecoratorTest.php @@ -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')); @@ -44,7 +45,7 @@ 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); } From d41f1312fc890c8f6956d9359d445d308ff18698 Mon Sep 17 00:00:00 2001 From: dblock Date: Mon, 13 Jan 2025 09:21:12 -0500 Subject: [PATCH 3/5] Raise an error when host header is empty. Signed-off-by: dblock --- src/OpenSearch/Aws/SigningClientDecorator.php | 4 ++++ tests/Aws/SigningClientDecoratorTest.php | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/OpenSearch/Aws/SigningClientDecorator.php b/src/OpenSearch/Aws/SigningClientDecorator.php index bdf2409c9..ecf46bbfe 100644 --- a/src/OpenSearch/Aws/SigningClientDecorator.php +++ b/src/OpenSearch/Aws/SigningClientDecorator.php @@ -34,6 +34,10 @@ public function sendRequest(RequestInterface $request): ResponseInterface $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); diff --git a/tests/Aws/SigningClientDecoratorTest.php b/tests/Aws/SigningClientDecoratorTest.php index 617be2460..fee1ec3c8 100644 --- a/tests/Aws/SigningClientDecoratorTest.php +++ b/tests/Aws/SigningClientDecoratorTest.php @@ -49,4 +49,16 @@ public function testSendRequest() $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); + } } From 42d731b18ffc7e91b2917b3e9b0a6d9a8029dbf9 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Fri, 17 Jan 2025 20:25:45 -0500 Subject: [PATCH 4/5] Require Host. Co-authored-by: Kim Pepper Signed-off-by: Daniel (dB.) Doubrovkine --- src/OpenSearch/Aws/SigningClientDecorator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenSearch/Aws/SigningClientDecorator.php b/src/OpenSearch/Aws/SigningClientDecorator.php index ecf46bbfe..1240dcf1f 100644 --- a/src/OpenSearch/Aws/SigningClientDecorator.php +++ b/src/OpenSearch/Aws/SigningClientDecorator.php @@ -17,7 +17,7 @@ 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, usually `Host` is required. + * @param array $headers Additional headers to add to the request. `Host` is required. * @return void */ public function __construct( From b3e48904e399beeeacf3acf4e72cfc32e208a3ac Mon Sep 17 00:00:00 2001 From: dblock Date: Fri, 17 Jan 2025 20:33:37 -0500 Subject: [PATCH 5/5] Fix tests. Signed-off-by: dblock --- tests/Aws/SigningClientDecoratorGuzzleTest.php | 11 +++++++++-- tests/Aws/SigningClientDecoratorSymfonyTest.php | 12 ++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/Aws/SigningClientDecoratorGuzzleTest.php b/tests/Aws/SigningClientDecoratorGuzzleTest.php index ab7a64403..8f077821f 100644 --- a/tests/Aws/SigningClientDecoratorGuzzleTest.php +++ b/tests/Aws/SigningClientDecoratorGuzzleTest.php @@ -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) @@ -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')); diff --git a/tests/Aws/SigningClientDecoratorSymfonyTest.php b/tests/Aws/SigningClientDecoratorSymfonyTest.php index cc3889f62..7961c373a 100644 --- a/tests/Aws/SigningClientDecoratorSymfonyTest.php +++ b/tests/Aws/SigningClientDecoratorSymfonyTest.php @@ -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) @@ -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); - } - - }