Skip to content

feat: add possibility to authenticate by username/password #435

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

Merged
merged 4 commits into from
May 6, 2022
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`

Expand Down
58 changes: 58 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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) <https://docs.influxdata.com/influxdb/latest/reference/config-options/#session-length>`__
(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
Expand Down
26 changes: 16 additions & 10 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^
Expand All @@ -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
Expand All @@ -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
^^^^^^^

Expand Down
15 changes: 15 additions & 0 deletions influxdb_client/_async/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 {}
Expand Down Expand Up @@ -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
15 changes: 15 additions & 0 deletions influxdb_client/_sync/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 {}
Expand Down Expand Up @@ -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
15 changes: 12 additions & 3 deletions influxdb_client/client/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions influxdb_client/client/influxdb_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@
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.

: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.
Expand All @@ -50,6 +50,8 @@ def __init__(self, url, token, debug=None, timeout=10_000, enable_gzip=False, or
: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,
Expand Down
7 changes: 5 additions & 2 deletions influxdb_client/client/influxdb_client_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
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.

: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
Expand All @@ -39,6 +40,8 @@ def __init__(self, url, token, org: str = None, debug=None, timeout=10_000, enab
: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`
Expand Down
13 changes: 11 additions & 2 deletions influxdb_client/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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(
Expand All @@ -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
7 changes: 6 additions & 1 deletion tests/test_InfluxDBClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions tests/test_InfluxDBClientAsync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading