diff --git a/CHANGELOG.md b/CHANGELOG.md index afafbe49..4723457e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ ## 1.34.0 [unreleased] +### Breaking Changes +1. [#509](https://github.com/influxdata/influxdb-client-python/pull/509): Rename `key_file` to `cert_key_file` inside the central [configuration class](https://github.com/influxdata/influxdb-client-python/blob/d011df72b528a45d305aa8accbe879b31be3280e/influxdb_client/configuration.py#L92) + ### Features 1. [#510](https://github.com/influxdata/influxdb-client-python/pull/510): Allow to use client's optional configs for initialization from file or environment properties +2. [#509](https://github.com/influxdata/influxdb-client-python/pull/509): MTLS support for the InfluxDB Python client ### Bug Fixes 1. [#512](https://github.com/influxdata/influxdb-client-python/pull/512): Exception propagation for asynchronous `QueryApi` [async/await] diff --git a/README.rst b/README.rst index 6d4358fb..88e05c16 100644 --- a/README.rst +++ b/README.rst @@ -197,6 +197,9 @@ The following options are supported: - ``timeout`` - socket timeout in ms (default value is 10000) - ``verify_ssl`` - set this to false to skip verifying SSL certificate when calling API from https server - ``ssl_ca_cert`` - set this to customize the certificate file to verify the peer +- ``cert_file`` - path to the certificate that will be used for mTLS authentication +- ``cert_key_file`` - path to the file contains private key for mTLS certificate +- ``cert_key_password`` - string or function which returns password for decrypting the mTLS private key - ``connection_pool_maxsize`` - set the number of connections to save that can be reused by urllib3 - ``auth_basic`` - enable http basic authentication when talking to a InfluxDB 1.8.x without authentication but is accessed via reverse proxy with basic authentication (defaults to false) - ``profilers`` - set the list of enabled `Flux profilers `_ @@ -226,6 +229,9 @@ Supported properties are: - ``INFLUXDB_V2_TIMEOUT`` - socket timeout in ms (default value is 10000) - ``INFLUXDB_V2_VERIFY_SSL`` - set this to false to skip verifying SSL certificate when calling API from https server - ``INFLUXDB_V2_SSL_CA_CERT`` - set this to customize the certificate file to verify the peer +- ``INFLUXDB_V2_CERT_FILE`` - path to the certificate that will be used for mTLS authentication +- ``INFLUXDB_V2_CERT_KEY_FILE`` - path to the file contains private key for mTLS certificate +- ``INFLUXDB_V2_CERT_KEY_PASSWORD`` - string or function which returns password for decrypting the mTLS private key - ``INFLUXDB_V2_CONNECTION_POOL_MAXSIZE`` - set the number of connections to save that can be reused by urllib3 - ``INFLUXDB_V2_AUTH_BASIC`` - enable http basic authentication when talking to a InfluxDB 1.8.x without authentication but is accessed via reverse proxy with basic authentication (defaults to false) - ``INFLUXDB_V2_PROFILERS`` - set the list of enabled `Flux profilers `_ diff --git a/influxdb_client/_async/rest.py b/influxdb_client/_async/rest.py index dcf678d0..8db95e91 100644 --- a/influxdb_client/_async/rest.py +++ b/influxdb_client/_async/rest.py @@ -83,15 +83,19 @@ def __init__(self, configuration, pools_size=4, maxsize=None, **kwargs): if maxsize is None: maxsize = configuration.connection_pool_maxsize - ssl_context = ssl.create_default_context(cafile=configuration.ssl_ca_cert) - if configuration.cert_file: - ssl_context.load_cert_chain( - configuration.cert_file, keyfile=configuration.key_file - ) - - if not configuration.verify_ssl: - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE + if configuration.ssl_context is None: + ssl_context = ssl.create_default_context(cafile=configuration.ssl_ca_cert) + if configuration.cert_file: + ssl_context.load_cert_chain( + certfile=configuration.cert_file, keyfile=configuration.cert_key_file, + password=configuration.cert_key_password + ) + + if not configuration.verify_ssl: + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + else: + ssl_context = configuration.ssl_context connector = aiohttp.TCPConnector( limit=maxsize, diff --git a/influxdb_client/_sync/rest.py b/influxdb_client/_sync/rest.py index d3ac55c6..bac94f9e 100644 --- a/influxdb_client/_sync/rest.py +++ b/influxdb_client/_sync/rest.py @@ -101,9 +101,11 @@ def __init__(self, configuration, pools_size=4, maxsize=None, retries=False): cert_reqs=cert_reqs, ca_certs=ca_certs, cert_file=configuration.cert_file, - key_file=configuration.key_file, + key_file=configuration.cert_key_file, + key_password=configuration.cert_key_password, proxy_url=configuration.proxy, proxy_headers=configuration.proxy_headers, + ssl_context=configuration.ssl_context, **addition_pool_args ) else: @@ -113,7 +115,9 @@ def __init__(self, configuration, pools_size=4, maxsize=None, retries=False): cert_reqs=cert_reqs, ca_certs=ca_certs, cert_file=configuration.cert_file, - key_file=configuration.key_file, + key_file=configuration.cert_key_file, + key_password=configuration.cert_key_password, + ssl_context=configuration.ssl_context, **addition_pool_args ) diff --git a/influxdb_client/client/_base.py b/influxdb_client/client/_base.py index 95aef3db..de2910da 100644 --- a/influxdb_client/client/_base.py +++ b/influxdb_client/client/_base.py @@ -60,6 +60,10 @@ def __init__(self, url, token, debug=None, timeout=10_000, enable_gzip=False, or self.conf.enable_gzip = enable_gzip self.conf.verify_ssl = kwargs.get('verify_ssl', True) self.conf.ssl_ca_cert = kwargs.get('ssl_ca_cert', None) + self.conf.cert_file = kwargs.get('cert_file', None) + self.conf.cert_key_file = kwargs.get('cert_key_file', None) + self.conf.cert_key_password = kwargs.get('cert_key_password', None) + self.conf.ssl_context = kwargs.get('ssl_context', None) self.conf.proxy = kwargs.get('proxy', None) self.conf.proxy_headers = kwargs.get('proxy_headers', None) self.conf.connection_pool_maxsize = kwargs.get('connection_pool_maxsize', self.conf.connection_pool_maxsize) @@ -142,6 +146,18 @@ def _has_section(key: str): if _has_option('ssl_ca_cert'): ssl_ca_cert = _config_value('ssl_ca_cert') + cert_file = None + if _has_option('cert_file'): + cert_file = _config_value('cert_file') + + cert_key_file = None + if _has_option('cert_key_file'): + cert_key_file = _config_value('cert_key_file') + + cert_key_password = None + if _has_option('cert_key_password'): + cert_key_password = _config_value('cert_key_password') + connection_pool_maxsize = None if _has_option('connection_pool_maxsize'): connection_pool_maxsize = _config_value('connection_pool_maxsize') @@ -168,6 +184,7 @@ def _has_section(key: str): return cls(url, token, debug=debug, timeout=_to_int(timeout), org=org, default_tags=default_tags, enable_gzip=enable_gzip, verify_ssl=_to_bool(verify_ssl), ssl_ca_cert=ssl_ca_cert, + cert_file=cert_file, cert_key_file=cert_key_file, cert_key_password=cert_key_password, connection_pool_maxsize=_to_int(connection_pool_maxsize), auth_basic=_to_bool(auth_basic), profilers=profilers, proxy=proxy, **kwargs) @@ -179,6 +196,9 @@ def _from_env_properties(cls, debug=None, enable_gzip=False, **kwargs): org = os.getenv('INFLUXDB_V2_ORG', "my-org") verify_ssl = os.getenv('INFLUXDB_V2_VERIFY_SSL', "True") ssl_ca_cert = os.getenv('INFLUXDB_V2_SSL_CA_CERT', None) + cert_file = os.getenv('INFLUXDB_V2_CERT_FILE', None) + cert_key_file = os.getenv('INFLUXDB_V2_CERT_KEY_FILE', None) + cert_key_password = os.getenv('INFLUXDB_V2_CERT_KEY_PASSWORD', None) connection_pool_maxsize = os.getenv('INFLUXDB_V2_CONNECTION_POOL_MAXSIZE', None) auth_basic = os.getenv('INFLUXDB_V2_AUTH_BASIC', "False") @@ -195,6 +215,7 @@ def _from_env_properties(cls, debug=None, enable_gzip=False, **kwargs): return cls(url, token, debug=debug, timeout=_to_int(timeout), org=org, default_tags=default_tags, enable_gzip=enable_gzip, verify_ssl=_to_bool(verify_ssl), ssl_ca_cert=ssl_ca_cert, + cert_file=cert_file, cert_key_file=cert_key_file, cert_key_password=cert_key_password, connection_pool_maxsize=_to_int(connection_pool_maxsize), auth_basic=_to_bool(auth_basic), profilers=profilers, **kwargs) diff --git a/influxdb_client/client/influxdb_client.py b/influxdb_client/client/influxdb_client.py index 042087da..91030774 100644 --- a/influxdb_client/client/influxdb_client.py +++ b/influxdb_client/client/influxdb_client.py @@ -40,6 +40,12 @@ def __init__(self, url, token: str = None, debug=None, timeout=10_000, enable_gz :param org: organization name (used as a default in Query, Write and Delete API) :key bool verify_ssl: Set this to false to skip verifying SSL certificate when calling API from https server. :key str ssl_ca_cert: Set this to customize the certificate file to verify the peer. + :key str cert_file: Path to the certificate that will be used for mTLS authentication. + :key str cert_key_file: Path to the file contains private key for mTLS certificate. + :key str cert_key_password: String or function which returns password for decrypting the mTLS private key. + :key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake. + Be aware that only delivered certificate/ key files or an SSL Context are + possible. :key str proxy: Set this to configure the http proxy to be used (ex. http://localhost:3128) :key str proxy_headers: A dictionary containing headers that will be sent to the proxy. Could be used for proxy authentication. @@ -89,6 +95,9 @@ def from_config_file(cls, config_file: str = "config.ini", debug=None, enable_gz authentication. :key urllib3.util.retry.Retry retries: Set the default retry strategy that is used for all HTTP requests except batching writes. As a default there is no one retry strategy. + :key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake. + Be aware that only delivered certificate/ key files or an SSL Context are + possible. The supported formats: - https://docs.python.org/3/library/configparser.html @@ -102,6 +111,9 @@ def from_config_file(cls, config_file: str = "config.ini", debug=None, enable_gz - timeout, - verify_ssl - ssl_ca_cert + - cert_file + - cert_key_file + - cert_key_password - connection_pool_maxsize - auth_basic - profilers @@ -177,6 +189,9 @@ def from_env_properties(cls, debug=None, enable_gzip=False, **kwargs): authentication. :key urllib3.util.retry.Retry retries: Set the default retry strategy that is used for all HTTP requests except batching writes. As a default there is no one retry strategy. + :key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake. + Be aware that only delivered certificate/ key files or an SSL Context are + possible. Supported environment properties: - INFLUXDB_V2_URL @@ -185,6 +200,9 @@ def from_env_properties(cls, debug=None, enable_gzip=False, **kwargs): - INFLUXDB_V2_TIMEOUT - INFLUXDB_V2_VERIFY_SSL - INFLUXDB_V2_SSL_CA_CERT + - INFLUXDB_V2_CERT_FILE + - INFLUXDB_V2_CERT_KEY_FILE + - INFLUXDB_V2_CERT_KEY_PASSWORD - INFLUXDB_V2_CONNECTION_POOL_MAXSIZE - INFLUXDB_V2_AUTH_BASIC - INFLUXDB_V2_PROFILERS diff --git a/influxdb_client/client/influxdb_client_async.py b/influxdb_client/client/influxdb_client_async.py index cdd8eade..96953cb3 100644 --- a/influxdb_client/client/influxdb_client_async.py +++ b/influxdb_client/client/influxdb_client_async.py @@ -32,6 +32,12 @@ def __init__(self, url, token: str = None, org: str = None, debug=None, timeout= supports the Gzip compression. :key bool verify_ssl: Set this to false to skip verifying SSL certificate when calling API from https server. :key str ssl_ca_cert: Set this to customize the certificate file to verify the peer. + :key str cert_file: Path to the certificate that will be used for mTLS authentication. + :key str cert_key_file: Path to the file contains private key for mTLS certificate. + :key str cert_key_password: String or function which returns password for decrypting the mTLS private key. + :key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake. + Be aware that only delivered certificate/ key files or an SSL Context are + possible. :key str proxy: Set this to configure the http proxy to be used (ex. http://localhost:3128) :key str proxy_headers: A dictionary containing headers that will be sent to the proxy. Could be used for proxy authentication. @@ -105,6 +111,9 @@ def from_config_file(cls, config_file: str = "config.ini", debug=None, enable_gz authentication. :key urllib3.util.retry.Retry retries: Set the default retry strategy that is used for all HTTP requests except batching writes. As a default there is no one retry strategy. + :key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake. + Be aware that only delivered certificate/ key files or an SSL Context are + possible. The supported formats: - https://docs.python.org/3/library/configparser.html @@ -118,6 +127,9 @@ def from_config_file(cls, config_file: str = "config.ini", debug=None, enable_gz - timeout, - verify_ssl - ssl_ca_cert + - cert_file + - cert_key_file + - cert_key_password - connection_pool_maxsize - auth_basic - profilers @@ -193,6 +205,10 @@ def from_env_properties(cls, debug=None, enable_gzip=False, **kwargs): authentication. :key urllib3.util.retry.Retry retries: Set the default retry strategy that is used for all HTTP requests except batching writes. As a default there is no one retry strategy. + :key ssl.SSLContext ssl_context: Specify a custom Python SSL Context for the TLS/ mTLS handshake. + Be aware that only delivered certificate/ key files or an SSL Context are + possible. + Supported environment properties: - INFLUXDB_V2_URL @@ -201,6 +217,9 @@ def from_env_properties(cls, debug=None, enable_gzip=False, **kwargs): - INFLUXDB_V2_TIMEOUT - INFLUXDB_V2_VERIFY_SSL - INFLUXDB_V2_SSL_CA_CERT + - INFLUXDB_V2_CERT_FILE + - INFLUXDB_V2_CERT_KEY_FILE + - INFLUXDB_V2_CERT_KEY_PASSWORD - INFLUXDB_V2_CONNECTION_POOL_MAXSIZE - INFLUXDB_V2_AUTH_BASIC - INFLUXDB_V2_PROFILERS diff --git a/influxdb_client/configuration.py b/influxdb_client/configuration.py index e9d95c78..3c118565 100644 --- a/influxdb_client/configuration.py +++ b/influxdb_client/configuration.py @@ -89,10 +89,15 @@ def __init__(self): # client certificate file self.cert_file = None # client key file - self.key_file = None + self.cert_key_file = None + # client key file password + self.cert_key_password = None # Set this to True/False to enable/disable SSL hostname verification. self.assert_hostname = None + # Set this to specify a custom ssl context to inject this context inside the urllib3 connection pool. + self.ssl_context = None + # urllib3 connection pool's maximum number of connections saved # per pool. urllib3 uses 1 connection as default value, but this is # not the best value when you are making a lot of possibly parallel diff --git a/tests/config-ssl-mtls-certs.ini b/tests/config-ssl-mtls-certs.ini new file mode 100644 index 00000000..0b3d6360 --- /dev/null +++ b/tests/config-ssl-mtls-certs.ini @@ -0,0 +1,14 @@ +[influx2] +url=http://localhost:8086 +org=my-org +token=my-token +timeout=6000 +ssl_ca_cert=/path/to/my/cert +cert_file=/path/to/my/cert +cert_key_file=/path/to/my/key +cert_key_password=test + +[tags] +id = 132-987-655 +customer = California Miner +data_center = ${env.data_center} \ No newline at end of file diff --git a/tests/test_InfluxDBClient.py b/tests/test_InfluxDBClient.py index c4b9b2a5..5202d7a9 100644 --- a/tests/test_InfluxDBClient.py +++ b/tests/test_InfluxDBClient.py @@ -3,6 +3,7 @@ import json import logging import os +import ssl import threading import unittest from io import StringIO @@ -57,6 +58,18 @@ def test_certificate_file(self): self.assertTrue(ping) + def test_certificate_context(self): + self._start_http_server() + + ssl_context = ssl.create_default_context(cafile=f"{os.path.dirname(__file__)}/server.pem") + + self.client = InfluxDBClient(f"https://localhost:{self.httpd.server_address[1]}", + token="my-token", verify_ssl=True, + ssl_context=ssl_context) + ping = self.client.ping() + + self.assertTrue(ping) + def test_init_from_ini_file(self): self.client = InfluxDBClient.from_config_file(f'{os.path.dirname(__file__)}/config.ini') @@ -141,6 +154,75 @@ def test_init_from_env_ssl_ca_cert(self): self.assertEqual("/my/custom/path/to/cert", self.client.api_client.configuration.ssl_ca_cert) + def test_init_from_env_ssl_cert_file(self): + os.environ["INFLUXDB_V2_CERT_FILE"] = "/my/custom/path" + self.client = InfluxDBClient.from_env_properties() + + self.assertEqual("/my/custom/path", self.client.api_client.configuration.cert_file) + + def test_init_from_file_ssl_cert_file_default(self): + self.client = InfluxDBClient.from_config_file(f'{os.path.dirname(__file__)}/config.ini') + + self.assertIsNone(self.client.api_client.configuration.cert_file) + + def test_init_from_file_ssl_cert_file(self): + self.client = InfluxDBClient.from_config_file(f'{os.path.dirname(__file__)}/config-ssl-mtls-certs.ini') + + self.assertEqual("/path/to/my/cert", self.client.api_client.configuration.cert_file) + + def test_init_from_env_ssl_cert_file_default(self): + if os.getenv("INFLUXDB_V2_CERT_FILE"): + del os.environ["INFLUXDB_V2_CERT_FILE"] + self.client = InfluxDBClient.from_env_properties() + + self.assertIsNone(self.client.api_client.configuration.cert_file) + + def test_init_from_env_ssl_cert_key(self): + os.environ["INFLUXDB_V2_CERT_KEY_FILE"] = "/my/custom/path" + self.client = InfluxDBClient.from_env_properties() + + self.assertEqual("/my/custom/path", self.client.api_client.configuration.cert_key_file) + + def test_init_from_file_ssl_cert_key_default(self): + self.client = InfluxDBClient.from_config_file(f'{os.path.dirname(__file__)}/config.ini') + + self.assertIsNone(self.client.api_client.configuration.cert_key_file) + + def test_init_from_file_ssl_cert_key(self): + self.client = InfluxDBClient.from_config_file(f'{os.path.dirname(__file__)}/config-ssl-mtls-certs.ini') + + self.assertEqual("/path/to/my/key", self.client.api_client.configuration.cert_key_file) + + def test_init_from_env_ssl_cert_key_default(self): + if os.getenv("INFLUXDB_V2_CERT_KEY_FILE"): + del os.environ["INFLUXDB_V2_CERT_KEY_FILE"] + self.client = InfluxDBClient.from_env_properties() + + self.assertIsNone(self.client.api_client.configuration.cert_key_file) + + def test_init_from_env_ssl_key_password(self): + os.environ["INFLUXDB_V2_CERT_KEY_PASSWORD"] = "test" + self.client = InfluxDBClient.from_env_properties() + + self.assertEqual("test", self.client.api_client.configuration.cert_key_password) + + def test_init_from_file_ssl_key_password_default(self): + self.client = InfluxDBClient.from_config_file(f'{os.path.dirname(__file__)}/config.ini') + + self.assertIsNone(self.client.api_client.configuration.cert_key_password) + + def test_init_from_file_ssl_key_password(self): + self.client = InfluxDBClient.from_config_file(f'{os.path.dirname(__file__)}/config-ssl-mtls-certs.ini') + + self.assertEqual("test", self.client.api_client.configuration.cert_key_password) + + def test_init_from_env_ssl_key_password_default(self): + if os.getenv("INFLUXDB_V2_CERT_KEY_PASSWORD"): + del os.environ["INFLUXDB_V2_CERT_KEY_PASSWORD"] + self.client = InfluxDBClient.from_env_properties() + + self.assertIsNone(self.client.api_client.configuration.cert_key_password) + def test_init_from_env_connection_pool_maxsize(self): os.environ["INFLUXDB_V2_CONNECTION_POOL_MAXSIZE"] = "29" self.client = InfluxDBClient.from_env_properties()