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

Support for Elastic Cloud ID #923

Merged
merged 17 commits into from Aug 12, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/configuration.asciidoc
Expand Up @@ -88,6 +88,29 @@ $client = ClientBuilder::create() // Instantiate a new ClientBuilder
Only the `host` parameter is required for each configured host. If not provided, the default port is `9200`. The default
scheme is `http`.

=== Connect to Elastic Cloud

If you want to connect to Elastic Cloud, you can use your Cloud ID and use basic authentication or ApiKey authentication to connect to it.

[source,php]
----
$client = ClientBuilder::create()
->setElasticCloudId('<elastic-cloud-id>', '<username>', '<secure-password>') <1>, <2>
->build();
----
<1> Your Cloud ID provided by the Elastic Cloud platform
<2> Your basic authentication credentials

[source,php]
----
$client = ClientBuilder::create()
->setElasticCloudId('<elastic-cloud-id>') <1>
->setApiKeyPairAuthentication('<id>', '<api_key>') <2>
->build();
----
<1> Your Cloud ID provided by the Elastic Cloud platform
<2> Your ApiKey pair as described https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/security.html[here]

=== Authorization and Encryption

For details about HTTP Authorization and SSL encryption, see
Expand Down
21 changes: 21 additions & 0 deletions docs/security.asciidoc
Expand Up @@ -24,6 +24,27 @@ $client = ClientBuilder::create()
Credentials are provided per-host, which allows each host to have their own set of credentials. All requests sent to the
cluster will use the appropriate credentials depending on the node being talked to.

=== ApiKey Authentication

If your Elasticsearch cluster is secured by API keys as described https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html[here], you have two options to provide the values to the client.
Either use the `id` and `api_key` pair from the create API key API response or use the encoded token as described https://www.elastic.co/guide/en/elasticsearch/reference/7.1/security-api-create-api-key.html#_examples_83[here] in the last code snippet.

[source,php]
----
$client = ClientBuilder::create()
->setApiKeyPairAuthentication('id', 'api_key') <1>
->build();
----
<1> ApiKey pair of `id` and `api_key` from the create API key response.

[source,php]
----
$client = ClientBuilder::create()
->setApiKeyAuthentication('encoded-api-key') <1>
->build();
----
<1> Encoded ApiKey pair

=== SSL Encryption

Configuring SSL is a little more complex. You need to identify if your certificate has been signed by a public
Expand Down
76 changes: 76 additions & 0 deletions src/Elasticsearch/ClientBuilder.php
Expand Up @@ -17,6 +17,7 @@
use Elasticsearch\Serializers\SerializerInterface;
use Elasticsearch\ConnectionPool\Selectors;
use Elasticsearch\Serializers\SmartSerializer;
use Elasticsearch\Helper\ElasticCloudIdParser;
use GuzzleHttp\Ring\Client\CurlHandler;
use GuzzleHttp\Ring\Client\CurlMultiHandler;
use GuzzleHttp\Ring\Client\Middleware;
Expand Down Expand Up @@ -126,6 +127,11 @@ class ClientBuilder
*/
private $sslVerification = null;

/**
* @var null|string
*/
private $apiKey = null;

public static function create(): ClientBuilder
{
return new static();
Expand Down Expand Up @@ -330,13 +336,82 @@ public function setHosts(array $hosts): ClientBuilder
return $this;
}

/**
* Set the APIKey for Authenication
*
* @link https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html
*
* @param string $apiKey
*/
public function setApiKeyAuthentication(string $apiKey)
{
$this->connectionParams['client']['headers']['Authorization'] = ['ApiKey ' . $apiKey];

return $this;
}

/**
* Set the APIKey Pair, consiting of the API Id and the ApiKey of the Response from /_security/api_key
*
* @link https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html
*
* @param string $id
* @param string $apiKey
*/
public function setApiKeyPairAuthentication(string $id, string $apiKey)
{
$this->setApiKeyAuthentication(base64_encode($id . ':' . $apiKey));

return $this;
}

public function setConnectionParams(array $params): ClientBuilder
{
$this->connectionParams = $params;

return $this;
}

/**
* Set Elastic Cloud ID to connect to Elastic Cloud
*
* <b>No authentication is provided</b>
*
* - set Hostname
* - set best practices for the connection
*
* @param string $cloudId
* @param string $username, optional if using Basic Authentication
* @param string $password, optional if using Basic Authentication
*/
public function setElasticCloudId(string $cloudId, ?string $username = null, ?string $password = null)
{
$cloud = new ElasticCloudIdParser($cloudId);
$hosts = [
[
'host' => $cloud->getClusterDns(),
'port' => '',
'scheme' => 'https',
'user' => $username,
'pass' => $password,
]
];

// Register the Hosts array
$this->setHosts($hosts);

// Merge best practices for the connection
$this->setConnectionParams([
'client' => [
'curl' => [
CURLOPT_ENCODING => 1,
],
]
]);

return $this;
}

public function setRetries(int $retries): ClientBuilder
{
$this->retries = $retries;
Expand Down Expand Up @@ -565,6 +640,7 @@ private function buildConnectionsFromHosts(array $hosts): array
$this->logger->error("Could not parse host: ".print_r($host, true));
throw new RuntimeException("Could not parse host: ".print_r($host, true));
}

$connections[] = $this->connectionFactory->create($host);
}

Expand Down
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace Elasticsearch\Common\Exceptions;

/**
* RuntimeException
*
* @category Elasticsearch
* @package Elasticsearch\Common\Exceptions
* @author Philip Krauss <philip.krauss@elastic.co>
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache2
* @link http://elastic.co
*/
class ElasticCloudIdParseException extends \RuntimeException implements ElasticsearchException
{
}
97 changes: 97 additions & 0 deletions src/Elasticsearch/Helper/ElasticCloudIdParser.php
@@ -0,0 +1,97 @@
<?php declare(strict_types = 1);

// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace Elasticsearch\Helper;

use Elasticsearch\Common\Exceptions\ElasticCloudIdParseException;

/**
* Class ElasticCloudIdParser
*
* @category Elasticsearch
* @package Elasticsearch\Helper
* @author Philip Krauss <philip.krauss@elastic.co>
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache2
* @link http://elastic.co
*/
class ElasticCloudIdParser
{

/**
* @var string
*/
private $cloudId;

/**
* @var string
*/
private $clusterName;

/**
* @var string
*/
private $clusterDns;

/**
* @param string $cloudId
*/
public function __construct(string $cloudId)
{
$this->cloudId = $cloudId;
$this->parse();
}

/**
* Get the Elastic Cloud Id
*
* @return string
*/
public function getCloudId(): string
{
return $this->cloudId;
}

/**
* Get the Name of the Elastic Cloud Cluster
*
* @return string
*/
public function getClusterName(): string
{
return $this->clusterName;
}

/**
* Get the DNS of the Elasticsearch Cluster
*
* @return string
*/
public function getClusterDns(): string
{
return $this->clusterDns;
}

/**
* Parse the Elastic Cloud Params from the CloudId
*
* @return void
*
* @throws ElasticCloudIdParseException
*/
private function parse(): void
{
try {
list($name, $encoded) = explode(':', $this->cloudId);
list($uri, $uuids) = explode('$', base64_decode($encoded));
list($es,) = explode(':', $uuids);

$this->clusterName = $name;
$this->clusterDns = $es . '.' . $uri;
} catch (\Throwable $t) {
throw new ElasticCloudIdParseException('could not parse the Cloud ID:' . $this->cloudId);
}
}
}
92 changes: 92 additions & 0 deletions tests/Elasticsearch/Tests/Helper/ElasticCloudIdParserTest.php
@@ -0,0 +1,92 @@
<?php declare(strict_types = 1);

// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace Elasticsearch\Tests\Helper;

use Elasticsearch\Helper\ElasticCloudIdParser;
use Elasticsearch\Common\Exceptions\ElasticCloudIdParseException;

/**
* Class ElasticCloudIdParserTest
*
* @package Elasticsearch\Tests\Helper
* @author Philip Krauss <philip.krauss@elastic.co>
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache2
* @link https://elastic.co
*/
class ElasticCloudIdParserTest extends \PHPUnit\Framework\TestCase
{

/**
* @return array
*/
public function cloudIdsProvider()
{
return [
['name' => 'my-cluster-001', 'domain' => 'd-001.com', 'uuids' => 'elasticsearch'],
['name' => 'my-cluster-002', 'domain' => 'd-002.net', 'uuids' => 'elasticsearch:kibana'],
['name' => 'my-cluster-003', 'domain' => 'd-003.org', 'uuids' => 'elasticsearch:kibana:apm'],
];
}

/**
* @dataProvider cloudIdsProvider
*
* @covers ElasticCloudIdParser::parse
* @covers ElasticCloudIdParser::getCloudId
* @covers ElasticCloudIdParser::getClusterName
* @covers ElasticCloudIdParser::getClusterDns
*/
public function testElasticCloudParserAndAccessors($name, $domain, $uuids)
{
$cloudId = sprintf('%s:%s', $name, base64_encode(sprintf('%s$%s', $domain, $uuids)));
$dns = sprintf('elasticsearch.%s', $domain);

$cloud = new ElasticCloudIdParser($cloudId);

$this->assertEquals($cloud->getCloudId(), $cloudId);
$this->assertEquals($cloud->getClusterName(), $name);
$this->assertEquals($cloud->getClusterDns(), $dns);
}

/**
* @covers ElasticCloudIdParser::parse
*/
public function testCloudIdParseExceptionWithMissingCloudId()
{
$this->expectException(ElasticCloudIdParseException::class);
$cloud = new ElasticCloudIdParser('');
}

/**
* @covers ElasticCloudIdParser::parse
*/
public function testCloudIdParseExceptionWithMissingHostPart()
{
$this->expectException(ElasticCloudIdParseException::class);
$cloud = new ElasticCloudIdParser('foo:');
}

/**
* @covers ElasticCloudIdParser::parse
*/
public function testCloudIdParseExceptionWithMissingClusterName()
{
$this->expectException(ElasticCloudIdParseException::class);
$cloud = new ElasticCloudIdParser(':bar');
}

/**
* @covers ElasticCloudIdParser::parse
*/
public function testCloudIdParseExceptionWithInvalidHostPart()
{
$cloudId = sprintf('name:%s', base64_encode('invalid-format'));

$this->expectException(ElasticCloudIdParseException::class);
$cloud = new ElasticCloudIdParser($cloudId);
}
}