From a129b45f6c525085e29c29eaf05415ed1f9f0ea9 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Thu, 5 May 2022 11:25:28 +0200 Subject: [PATCH 1/4] feat: add possibility to authenticate by username/password --- influxdb_client/_async/api_client.py | 15 ++++++ influxdb_client/_sync/api_client.py | 15 ++++++ influxdb_client/client/_base.py | 15 ++++-- influxdb_client/client/influxdb_client.py | 2 +- .../client/influxdb_client_async.py | 3 +- influxdb_client/rest.py | 13 +++++- tests/test_InfluxDBClient.py | 7 ++- tests/test_InfluxDBClientAsync.py | 5 ++ tests/test_InfluxDBClientAuthorization.py | 46 +++++++++++++++++++ 9 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 tests/test_InfluxDBClientAuthorization.py diff --git a/influxdb_client/_async/api_client.py b/influxdb_client/_async/api_client.py index 7d1af096..7d721ff8 100644 --- a/influxdb_client/_async/api_client.py +++ b/influxdb_client/_async/api_client.py @@ -25,6 +25,9 @@ from influxdb_client.configuration import Configuration import influxdb_client.domain from influxdb_client._async import rest +from influxdb_client import SigninService +from influxdb_client import SignoutService +from influxdb_client.rest import _requires_create_user_session, _requires_expire_user_session class ApiClientAsync(object): @@ -81,6 +84,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None, async def close(self): """Dispose api client.""" + await self._signout() await self.rest_client.close() """Dispose pools.""" if self._pool: @@ -117,6 +121,7 @@ async def __call_api( _preload_content=True, _request_timeout=None, urlopen_kw=None): config = self.configuration + await self._signin(resource_path=resource_path) # header parameters header_params = header_params or {} @@ -649,3 +654,13 @@ def __deserialize_model(self, data, klass): if klass_name: instance = self.__deserialize(data, klass_name) return instance + + async def _signin(self, resource_path: str): + if _requires_create_user_session(self.configuration, self.cookie, resource_path): + http_info = await SigninService(self).post_signin_async() + self.cookie = http_info[2]['set-cookie'] + + async def _signout(self): + if _requires_expire_user_session(self.configuration, self.cookie): + await SignoutService(self).post_signout_async() + self.cookie = None diff --git a/influxdb_client/_sync/api_client.py b/influxdb_client/_sync/api_client.py index 26c1ddf3..c1a60dd9 100644 --- a/influxdb_client/_sync/api_client.py +++ b/influxdb_client/_sync/api_client.py @@ -25,6 +25,9 @@ from influxdb_client.configuration import Configuration import influxdb_client.domain from influxdb_client._sync import rest +from influxdb_client import SigninService +from influxdb_client import SignoutService +from influxdb_client.rest import _requires_create_user_session, _requires_expire_user_session class ApiClient(object): @@ -81,6 +84,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None, def __del__(self): """Dispose pools.""" + self._signout() if self._pool: self._pool.close() self._pool.join() @@ -117,6 +121,7 @@ def __call_api( _preload_content=True, _request_timeout=None, urlopen_kw=None): config = self.configuration + self._signin(resource_path=resource_path) # header parameters header_params = header_params or {} @@ -649,3 +654,13 @@ def __deserialize_model(self, data, klass): if klass_name: instance = self.__deserialize(data, klass_name) return instance + + def _signin(self, resource_path: str): + if _requires_create_user_session(self.configuration, self.cookie, resource_path): + http_info = SigninService(self).post_signin_with_http_info() + self.cookie = http_info[2]['set-cookie'] + + def _signout(self): + if _requires_expire_user_session(self.configuration, self.cookie): + SignoutService(self).post_signout() + self.cookie = None diff --git a/influxdb_client/client/_base.py b/influxdb_client/client/_base.py index e0218272..600b4095 100644 --- a/influxdb_client/client/_base.py +++ b/influxdb_client/client/_base.py @@ -70,13 +70,20 @@ def __init__(self, url, token, debug=None, timeout=10_000, enable_gzip=False, or self.conf.loggers[client_logger] = logging.getLogger(client_logger) self.conf.debug = debug - auth_token = self.token + self.conf.username = kwargs.get('username', None) + self.conf.password = kwargs.get('password', None) + # by token self.auth_header_name = "Authorization" - self.auth_header_value = "Token " + auth_token - + if self.token: + self.auth_header_value = "Token " + self.token + # by HTTP basic auth_basic = kwargs.get('auth_basic', False) if auth_basic: self.auth_header_value = "Basic " + base64.b64encode(token.encode()).decode() + # by username, password + if self.conf.username and self.conf.password: + self.auth_header_name = None + self.auth_header_value = None self.retries = kwargs.get('retries', False) @@ -459,6 +466,8 @@ class _Configuration(Configuration): def __init__(self): Configuration.__init__(self) self.enable_gzip = False + self.username = None + self.password = None def update_request_header_params(self, path: str, params: dict): super().update_request_header_params(path, params) diff --git a/influxdb_client/client/influxdb_client.py b/influxdb_client/client/influxdb_client.py index e3cce1b5..123400b1 100644 --- a/influxdb_client/client/influxdb_client.py +++ b/influxdb_client/client/influxdb_client.py @@ -24,7 +24,7 @@ class InfluxDBClient(_BaseClient): """InfluxDBClient is client for InfluxDB v2.""" - def __init__(self, url, token, debug=None, timeout=10_000, enable_gzip=False, org: str = None, + def __init__(self, url, token: str = None, debug=None, timeout=10_000, enable_gzip=False, org: str = None, default_tags: dict = None, **kwargs) -> None: """ Initialize defaults. diff --git a/influxdb_client/client/influxdb_client_async.py b/influxdb_client/client/influxdb_client_async.py index 07c53875..d17319ba 100644 --- a/influxdb_client/client/influxdb_client_async.py +++ b/influxdb_client/client/influxdb_client_async.py @@ -16,7 +16,8 @@ class InfluxDBClientAsync(_BaseClient): """InfluxDBClientAsync is client for InfluxDB v2.""" - def __init__(self, url, token, org: str = None, debug=None, timeout=10_000, enable_gzip=False, **kwargs) -> None: + def __init__(self, url, token: str = None, org: str = None, debug=None, timeout=10_000, enable_gzip=False, + **kwargs) -> None: """ Initialize defaults. diff --git a/influxdb_client/rest.py b/influxdb_client/rest.py index 9bff5a72..91e3d548 100644 --- a/influxdb_client/rest.py +++ b/influxdb_client/rest.py @@ -9,10 +9,10 @@ Generated by: https://openapi-generator.tech """ - from __future__ import absolute_import from influxdb_client.client.exceptions import InfluxDBError +from influxdb_client.configuration import Configuration _UTF_8_encoding = 'utf-8' @@ -40,7 +40,7 @@ def __init__(self, status=None, reason=None, http_resp=None): def __str__(self): """Get custom error messages for exception.""" - error_message = "({0})\n"\ + error_message = "({0})\n" \ "Reason: {1}\n".format(self.status, self.reason) if self.headers: error_message += "HTTP response headers: {0}\n".format( @@ -50,3 +50,12 @@ def __str__(self): error_message += "HTTP response body: {0}\n".format(self.body) return error_message + + +def _requires_create_user_session(configuration: Configuration, cookie: str, resource_path: str): + _unauthorized = ['/api/v2/signin', '/api/v2/signout'] + return configuration.username and configuration.password and not cookie and resource_path not in _unauthorized + + +def _requires_expire_user_session(configuration: Configuration, cookie: str): + return configuration.username and configuration.password and cookie diff --git a/tests/test_InfluxDBClient.py b/tests/test_InfluxDBClient.py index 975978a2..b8bcd766 100644 --- a/tests/test_InfluxDBClient.py +++ b/tests/test_InfluxDBClient.py @@ -216,11 +216,16 @@ def test_version(self): def test_version_not_running_instance(self): client_not_running = InfluxDBClient("http://localhost:8099", token="my-token", debug=True) - with self.assertRaises(NewConnectionError) as cm: + with self.assertRaises(NewConnectionError): client_not_running.version() client_not_running.close() + def test_username_password_authorization(self): + self.client.close() + self.client = InfluxDBClient(url=self.host, username="my-user", password="my-password", debug=True) + self.client.query_api().query("buckets()", "my-org") + def _start_proxy_server(self): import http.server import urllib.request diff --git a/tests/test_InfluxDBClientAsync.py b/tests/test_InfluxDBClientAsync.py index 662c4437..ac7c13bf 100644 --- a/tests/test_InfluxDBClientAsync.py +++ b/tests/test_InfluxDBClientAsync.py @@ -232,6 +232,11 @@ def test_initialize_out_side_async_context(self): self.assertEqual("The async client should be initialised inside async coroutine " "otherwise there can be unexpected behaviour.", e.value.message) + @async_test + async def test_username_password_authorization(self): + await self.client.close() + self.client = InfluxDBClientAsync(url="http://localhost:8086", username="my-user", password="my-password", debug=True) + await self.client.query_api().query("buckets()", "my-org") async def _prepare_data(self, measurement: str): _point1 = Point(measurement).tag("location", "Prague").field("temperature", 25.3) diff --git a/tests/test_InfluxDBClientAuthorization.py b/tests/test_InfluxDBClientAuthorization.py new file mode 100644 index 00000000..f828748d --- /dev/null +++ b/tests/test_InfluxDBClientAuthorization.py @@ -0,0 +1,46 @@ +import unittest + +import httpretty + +from influxdb_client import InfluxDBClient + + +class InfluxDBClientAuthorization(unittest.TestCase): + + def setUp(self) -> None: + httpretty.enable() + httpretty.reset() + + def tearDown(self) -> None: + if self.influxdb_client: + self.influxdb_client.close() + httpretty.disable() + + def test_session_request(self): + httpretty.reset() + self.influxdb_client = InfluxDBClient(url="http://localhost", token="my-token", + username="my-username", + password="my-password") + + # create user session + httpretty.register_uri(httpretty.POST, uri="http://localhost/api/v2/signin", + adding_headers={'Set-Cookie': 'session=xyz'}) + # authorized request + httpretty.register_uri(httpretty.GET, uri="http://localhost/ping") + # expires current session + httpretty.register_uri(httpretty.POST, uri="http://localhost/api/v2/signout") + + ping = self.influxdb_client.ping() + self.assertTrue(ping) + + self.assertEqual(2, len(httpretty.httpretty.latest_requests)) + # basic auth header + self.assertEqual('Basic bXktdXNlcm5hbWU6bXktcGFzc3dvcmQ=', httpretty.httpretty.latest_requests[0].headers['Authorization']) + # cookie header + self.assertEqual('session=xyz', httpretty.httpretty.latest_requests[1].headers['Cookie']) + self.assertIsNotNone(self.influxdb_client.api_client.cookie) + + # signout + self.influxdb_client.close() + + self.assertEqual(3, len(httpretty.httpretty.latest_requests)) From 91ec9a02714ef13b45b9798ad6523fa79be1c194 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Thu, 5 May 2022 13:44:32 +0200 Subject: [PATCH 2/4] docs: add documentation --- README.rst | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ docs/usage.rst | 26 +++++++++++++--------- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 8f30a6e9..d0c499da 100644 --- a/README.rst +++ b/README.rst @@ -1089,6 +1089,64 @@ Gzip support .. marker-gzip-end +Authenticate to the InfluxDB +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. marker-authenticate-start + +``InfluxDBClient`` supports three options how to authorize a connection: + +- `Token` +- `Username & Password` +- `HTTP Basic` + +Token +""""" + +Use the ``token`` to authenticate to the InfluxDB API. In your API requests, an `Authorization` header will be send. +The header value, provide the word `Token` followed by a space and an InfluxDB API token. The word `token`` is case-sensitive. + +.. code-block:: python + + from influxdb_client import InfluxDBClient + + with InfluxDBClient(url="http://localhost:8086", token="my-token") as client + +.. note:: Note that this is a preferred way how to authenticate to InfluxDB API. + +Username & Password +""""""""""""""""""" + +Authenticates via username and password credentials. If successful, creates a new session for the user. + +.. code-block:: python + + from influxdb_client import InfluxDBClient + + with InfluxDBClient(url="http://localhost:8086", username="my-user", password="my-password") as client + +.. warning:: + + The ``username/password`` auth is based on the HTTP "Basic" authentication. + The authorization expires when the `time-to-live (TTL) `__ + (default 60 minutes) is reached and client produces ``unauthorized exception``. + +HTTP Basic +"""""""""" + +Use this to enable basic authentication when talking to a InfluxDB 1.8.x that does not use auth-enabled +but is protected by a reverse proxy with basic authentication. + +.. code-block:: python + + from influxdb_client import InfluxDBClient + + with InfluxDBClient(url="http://localhost:8086", auth_basic=True, token="my-proxy-secret") as client + + +.. warning:: Don't use this when directly talking to InfluxDB 2. + +.. marker-authenticate-end + Proxy configuration ^^^^^^^^^^^^^^^^^^^ .. marker-proxy-start diff --git a/docs/usage.rst b/docs/usage.rst index 3d6f2ab6..cd269e74 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -16,17 +16,23 @@ Write :start-after: marker-writes-start :end-before: marker-writes-end +Delete data +^^^^^^^^^^^ +.. include:: ../README.rst + :start-after: marker-delete-start + :end-before: marker-delete-end + Pandas DataFrame ^^^^^^^^^^^^^^^^ .. include:: ../README.rst :start-after: marker-pandas-start :end-before: marker-pandas-end -Delete data -^^^^^^^^^^^ +How to use Asyncio +^^^^^^^^^^^^^^^^^^ .. include:: ../README.rst - :start-after: marker-delete-start - :end-before: marker-delete-end + :start-after: marker-asyncio-start + :end-before: marker-asyncio-end Gzip support ^^^^^^^^^^^^ @@ -40,6 +46,12 @@ Proxy configuration :start-after: marker-proxy-start :end-before: marker-proxy-end +Authentication +^^^^^^^^^^^^^^ +.. include:: ../README.rst + :start-after: marker-authenticate-start + :end-before: marker-authenticate-end + Nanosecond precision ^^^^^^^^^^^^^^^^^^^^ .. include:: ../README.rst @@ -52,12 +64,6 @@ Handling Errors :start-after: marker-handling-errors-start :end-before: marker-handling-errors-end -How to use Asyncio -^^^^^^^^^^^^^^^^^^ -.. include:: ../README.rst - :start-after: marker-asyncio-start - :end-before: marker-asyncio-end - Logging ^^^^^^^ From 99e3104b28e052b4035a2b58b6464a1d19002781 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Thu, 5 May 2022 13:49:28 +0200 Subject: [PATCH 3/4] docs: add documentation --- influxdb_client/client/influxdb_client.py | 4 +++- influxdb_client/client/influxdb_client_async.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/influxdb_client/client/influxdb_client.py b/influxdb_client/client/influxdb_client.py index 123400b1..3022f33f 100644 --- a/influxdb_client/client/influxdb_client.py +++ b/influxdb_client/client/influxdb_client.py @@ -30,7 +30,7 @@ def __init__(self, url, token: str = None, debug=None, timeout=10_000, enable_gz Initialize defaults. :param url: InfluxDB server API url (ex. http://localhost:8086). - :param token: auth token + :param token: ``token`` to authenticate to the InfluxDB API :param debug: enable verbose logging of http requests :param timeout: HTTP client timeout setting for a request specified in milliseconds. If one number provided, it will be total request timeout. @@ -50,6 +50,8 @@ def __init__(self, url, token: str = None, debug=None, timeout=10_000, enable_gz :key bool auth_basic: Set this to true to enable basic authentication when talking to a InfluxDB 1.8.x that does not use auth-enabled but is protected by a reverse proxy with basic authentication. (defaults to false, don't set to true when talking to InfluxDB 2) + :key str username: ``username`` to authenticate via username and password credentials to the InfluxDB 2.x + :key str password: ``password`` to authenticate via username and password credentials to the InfluxDB 2.x :key list[str] profilers: list of enabled Flux profilers """ super().__init__(url=url, token=token, debug=debug, timeout=timeout, enable_gzip=enable_gzip, org=org, diff --git a/influxdb_client/client/influxdb_client_async.py b/influxdb_client/client/influxdb_client_async.py index d17319ba..f622f300 100644 --- a/influxdb_client/client/influxdb_client_async.py +++ b/influxdb_client/client/influxdb_client_async.py @@ -22,7 +22,7 @@ def __init__(self, url, token: str = None, org: str = None, debug=None, timeout= Initialize defaults. :param url: InfluxDB server API url (ex. http://localhost:8086). - :param token: auth token + :param token: ``token`` to authenticate to the InfluxDB 2.x :param org: organization name (used as a default in Query, Write and Delete API) :param debug: enable verbose logging of http requests :param timeout: The maximal number of milliseconds for the whole HTTP request including @@ -40,6 +40,8 @@ def __init__(self, url, token: str = None, org: str = None, debug=None, timeout= :key bool auth_basic: Set this to true to enable basic authentication when talking to a InfluxDB 1.8.x that does not use auth-enabled but is protected by a reverse proxy with basic authentication. (defaults to false, don't set to true when talking to InfluxDB 2) + :key str username: ``username`` to authenticate via username and password credentials to the InfluxDB 2.x + :key str password: ``password`` to authenticate via username and password credentials to the InfluxDB 2.x :key bool allow_redirects: If set to ``False``, do not follow HTTP redirects. ``True`` by default. :key int max_redirects: Maximum number of HTTP redirects to follow. ``10`` by default. :key dict client_session_kwargs: Additional configuration arguments for :class:`~aiohttp.ClientSession` From 83e017cef7ca25e81a9de403e956447dac206703 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Thu, 5 May 2022 13:50:37 +0200 Subject: [PATCH 4/4] docs: update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9879099..c8c102cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 1.29.0 [unreleased] +### Features +1. [#435](https://github.com/influxdata/influxdb-client-python/pull/435): Add possibility to authenticate by `username/password` + ### Breaking Changes 1. [#433](https://github.com/influxdata/influxdb-client-python/pull/433): Rename `InvocableScripts` to `InvokableScripts`