From c4bc2e420d9d76b9b63c8e5901bf0889b0ffb684 Mon Sep 17 00:00:00 2001 From: Philip Krauss <35487337+philkra@users.noreply.github.com> Date: Fri, 20 Sep 2019 15:01:31 +0200 Subject: [PATCH] API key Authentication (#1010) --- docs/index.rst | 22 +++++++++++++++++ elasticsearch/connection/base.py | 12 +++++++++ elasticsearch/connection/http_requests.py | 4 +++ elasticsearch/connection/http_urllib3.py | 4 +++ test_elasticsearch/test_connection.py | 30 +++++++++++++++++++++++ 5 files changed, 72 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index f05d60c0..f2d17d6d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -230,6 +230,28 @@ description of the options. .. _certifi: http://certifiio.readthedocs.io/en/latest/ +APIKey Authentication +~~~~~~~~~~~~~~~~~~~~~~ + +You can configure the client to use Elasticsearch's `API Key`_ for connecting to your cluster. +Please note this authentication method has been introduced with release of Elasticsearch ``6.7.0``. + + from elasticsearch import Elasticsearch + + # you can use the api key tuple + es = Elasticsearch( + ['node-1', 'node-2', 'node-3'], + api_key=('id', 'api_key'), + ) + + # or you pass the base 64 encoded token + es = Elasticsearch( + ['node-1', 'node-2', 'node-3'], + api_key='base64encoded tuple', + ) + +.. _API Key: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html + Logging ~~~~~~~ diff --git a/elasticsearch/connection/base.py b/elasticsearch/connection/base.py index 45e338df..8bb93a4b 100644 --- a/elasticsearch/connection/base.py +++ b/elasticsearch/connection/base.py @@ -1,4 +1,5 @@ import logging +import base64 from platform import python_version @@ -183,3 +184,14 @@ class Connection(object): def _get_default_user_agent(self): return "elasticsearch-py/%s (Python %s)" % (__versionstr__, python_version()) + + def _get_api_key_header_val(self, api_key): + """ + Check the type of the passed api_key and return the correct header value + for the `API Key authentication ` + :arg api_key, either a tuple or a base64 encoded string + """ + if isinstance(api_key, (tuple, list)): + s = "{0}:{1}".format(api_key[0], api_key[1]).encode('utf-8') + return "ApiKey " + base64.b64encode(s).decode('utf-8') + return "ApiKey " + api_key diff --git a/elasticsearch/connection/http_requests.py b/elasticsearch/connection/http_requests.py index 81282107..e8fc2224 100644 --- a/elasticsearch/connection/http_requests.py +++ b/elasticsearch/connection/http_requests.py @@ -36,6 +36,7 @@ class RequestsHttpConnection(Connection): separate cert and key files (client_cert will contain only the cert) :arg headers: any custom http headers to be add to requests :arg cloud_id: The Cloud ID from ElasticCloud. Convient way to connect to cloud instances. + :arg api_key: optional API Key authentication as either base64 encoded string or a tuple. Other host connection params will be ignored. """ @@ -52,6 +53,7 @@ class RequestsHttpConnection(Connection): client_key=None, headers=None, cloud_id=None, + api_key=None, **kwargs ): if not REQUESTS_AVAILABLE: @@ -80,6 +82,8 @@ class RequestsHttpConnection(Connection): elif isinstance(http_auth, string_types): http_auth = tuple(http_auth.split(":", 1)) self.session.auth = http_auth + if api_key is not None: + self.session.headers['authorization'] = self._get_api_key_header_val(api_key) self.base_url = "http%s://%s:%d%s" % ( "s" if self.use_ssl else "", host, diff --git a/elasticsearch/connection/http_urllib3.py b/elasticsearch/connection/http_urllib3.py index 91078ad3..41f625ab 100644 --- a/elasticsearch/connection/http_urllib3.py +++ b/elasticsearch/connection/http_urllib3.py @@ -74,6 +74,7 @@ class Urllib3HttpConnection(Connection): :arg headers: any custom http headers to be add to requests :arg http_compress: Use gzip compression :arg cloud_id: The Cloud ID from ElasticCloud. Convient way to connect to cloud instances. + :arg api_key: optional API Key authentication as either base64 encoded string or a tuple. Other host connection params will be ignored. """ @@ -96,6 +97,7 @@ class Urllib3HttpConnection(Connection): ssl_context=None, http_compress=False, cloud_id=None, + api_key=None, **kwargs ): @@ -128,6 +130,8 @@ class Urllib3HttpConnection(Connection): self.headers.setdefault("content-type", "application/json") self.headers.setdefault("user-agent", self._get_default_user_agent()) + if api_key is not None: + self.headers.setdefault('authorization', self._get_api_key_header_val(api_key)) pool_class = urllib3.HTTPConnectionPool kw = {} diff --git a/test_elasticsearch/test_connection.py b/test_elasticsearch/test_connection.py index 1ee2e41f..073ce6bd 100644 --- a/test_elasticsearch/test_connection.py +++ b/test_elasticsearch/test_connection.py @@ -44,6 +44,21 @@ class TestUrllib3Connection(TestCase): con.host, "https://0fd50f62320ed6539f6cb48e1b68.example.cloud.com:9243" ) + def test_api_key_auth(self): + # test with tuple + con = Urllib3HttpConnection( + cloud_id="foobar:ZXhhbXBsZS5jbG91ZC5jb20kMGZkNTBmNjIzMjBlZDY1MzlmNmNiNDhlMWI2OCRhYzUzOTVhODgz\nNDU2NmM5ZjE1Y2Q4ZTQ5MGE=\n", + api_key=("elastic", "changeme1"), + ) + self.assertEquals(con.headers["authorization"], "ApiKey ZWxhc3RpYzpjaGFuZ2VtZTE=") + + # test with base64 encoded string + con = Urllib3HttpConnection( + cloud_id="foobar:ZXhhbXBsZS5jbG91ZC5jb20kMGZkNTBmNjIzMjBlZDY1MzlmNmNiNDhlMWI2OCRhYzUzOTVhODgz\nNDU2NmM5ZjE1Y2Q4ZTQ5MGE=\n", + api_key="ZWxhc3RpYzpjaGFuZ2VtZTI=", + ) + self.assertEquals(con.headers["authorization"], "ApiKey ZWxhc3RpYzpjaGFuZ2VtZTI=") + def test_http_compression(self): con = Urllib3HttpConnection(http_compress=True) self.assertTrue(con.http_compress) @@ -181,6 +196,21 @@ class TestRequestsConnection(TestCase): con.host, "https://0fd50f62320ed6539f6cb48e1b68.example.cloud.com:9243" ) + def test_api_key_auth(self): + # test with tuple + con = RequestsHttpConnection( + cloud_id="foobar:ZXhhbXBsZS5jbG91ZC5jb20kMGZkNTBmNjIzMjBlZDY1MzlmNmNiNDhlMWI2OCRhYzUzOTVhODgz\nNDU2NmM5ZjE1Y2Q4ZTQ5MGE=\n", + api_key=("elastic", "changeme1"), + ) + self.assertEquals(con.session.headers["authorization"], "ApiKey ZWxhc3RpYzpjaGFuZ2VtZTE=") + + # test with base64 encoded string + con = RequestsHttpConnection( + cloud_id="foobar:ZXhhbXBsZS5jbG91ZC5jb20kMGZkNTBmNjIzMjBlZDY1MzlmNmNiNDhlMWI2OCRhYzUzOTVhODgz\nNDU2NmM5ZjE1Y2Q4ZTQ5MGE=\n", + api_key="ZWxhc3RpYzpjaGFuZ2VtZTI=", + ) + self.assertEquals(con.session.headers["authorization"], "ApiKey ZWxhc3RpYzpjaGFuZ2VtZTI=") + def test_uses_https_if_verify_certs_is_off(self): with warnings.catch_warnings(record=True) as w: con = self._get_mock_connection(