Skip to content

Commit 4ed0da8

Browse files
insightfulsk-raina
authored andcommitted
Support key password for schema registry client. (#2033)
* Support key password for schema registry client. * Remove unused import.
1 parent b9ae7e6 commit 4ed0da8

File tree

5 files changed

+115
-36
lines changed

5 files changed

+115
-36
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
attrs>=21.2.0
22
cachetools>=5.5.0
3+
certifi
34
httpx>=0.26
45
authlib>=1.0.0

src/confluent_kafka/schema_registry/_async/schema_registry_client.py

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
# limitations under the License.
1717
#
1818
import asyncio
19+
import certifi
1920
import json
2021
import logging
22+
import os
23+
import ssl
2124
import time
2225
import urllib
2326
from urllib.parse import unquote, urlparse
2427

2528
import httpx
26-
from typing import List, Dict, Optional, Union, Any, Tuple, Callable
29+
from typing import List, Dict, Optional, Union, Any, Callable
2730

2831
from cachetools import TTLCache, LRUCache
2932
from httpx import Response
@@ -148,24 +151,43 @@ def __init__(self, conf: dict):
148151
raise ValueError("Missing required configuration property url")
149152
self.base_urls = base_urls
150153

151-
self.verify = True
152-
ca = conf_copy.pop('ssl.ca.location', None)
153-
if ca is not None:
154-
self.verify = ca
155-
154+
ca: Union[str, bool, None] = conf_copy.pop('ssl.ca.location', None)
156155
key: Optional[str] = conf_copy.pop('ssl.key.location', None)
156+
key_password: Optional[str] = conf_copy.pop('ssl.key.password', None)
157157
client_cert: Optional[str] = conf_copy.pop('ssl.certificate.location', None)
158-
self.cert: Union[str, Tuple[str, str], None] = None
159-
160-
if client_cert is not None and key is not None:
161-
self.cert = (client_cert, key)
162158

163-
if client_cert is not None and key is None:
164-
self.cert = client_cert
159+
# this mimicks legacy, deprecated behaviour of httpx
160+
# self.verify is always set to an ssl.SSLContext in case we need to load_cert_chain
161+
if ca is False:
162+
self.verify = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
163+
self.verify.check_hostname = False
164+
self.verify.verify_mode = ssl.CERT_NONE
165+
elif isinstance(ca, str):
166+
if os.path.isdir(ca):
167+
self.verify = ssl.create_default_context(capath=ca)
168+
else:
169+
self.verify = ssl.create_default_context(cafile=ca)
170+
else:
171+
if os.environ.get("SSL_CERT_FILE"):
172+
self.verify = ssl.create_default_context(cafile=os.environ["SSL_CERT_FILE"])
173+
elif os.environ.get("SSL_CERT_DIR"):
174+
self.verify = ssl.create_default_context(capath=os.environ["SSL_CERT_DIR"])
175+
else:
176+
self.verify = ssl.create_default_context(cafile=certifi.where())
177+
178+
if client_cert is not None:
179+
if key is not None and key_password is not None:
180+
self.verify.load_cert_chain(certfile=client_cert, keyfile=key, password=key_password)
181+
elif key is not None:
182+
self.verify.load_cert_chain(certfile=client_cert, keyfile=key)
183+
elif key_password is not None:
184+
self.verify.load_cert_chain(certfile=client_cert, password=key_password)
185+
else:
186+
self.verify.load_cert_chain(certfile=client_cert)
165187

166-
if key is not None and client_cert is None:
188+
if (key is not None or key_password is not None) and client_cert is None:
167189
raise ValueError("ssl.certificate.location required when"
168-
" configuring ssl.key.location")
190+
" configuring ssl.key.location or ssl.key.password")
169191

170192
parsed = urlparse(self.base_urls[0])
171193
try:
@@ -351,7 +373,6 @@ def __init__(self, conf: dict):
351373

352374
self.session = httpx.AsyncClient(
353375
verify=self.verify,
354-
cert=self.cert,
355376
auth=self.auth,
356377
proxy=self.proxy,
357378
timeout=self.timeout
@@ -516,10 +537,18 @@ class AsyncSchemaRegistryClient(object):
516537
| ``ssl.key.location`` | str | |
517538
| | | ``ssl.certificate.location`` must also be set. |
518539
+------------------------------+------+-------------------------------------------------+
519-
| | | Path to client's public key (PEM) used for |
540+
| | | Password to use to decrypt the client's private |
541+
| | | key. |
542+
| | | |
543+
| ``ssl.key.password`` | str | The private key may be provided using |
544+
| | | ``ssl.key.location``, or bundled with the |
545+
| | | certificate in ``ssl.certificate.location``. |
546+
| | | Password is optional (key may be unencrypted). |
547+
+------------------------------+------+-------------------------------------------------+
548+
| | | Path to client's certificate (PEM) used for |
520549
| | | authentication. |
521550
| ``ssl.certificate.location`` | str | |
522-
| | | May be set without ssl.key.location if the |
551+
| | | May be set without ``ssl.key.location`` if the |
523552
| | | private key is stored within the PEM as well. |
524553
+------------------------------+------+-------------------------------------------------+
525554
| | | Client HTTP credentials in the form of |

src/confluent_kafka/schema_registry/_sync/schema_registry_client.py

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
# limitations under the License.
1717
#
1818

19+
import certifi
1920
import json
2021
import logging
22+
import os
23+
import ssl
2124
import time
2225
import urllib
2326
from urllib.parse import unquote, urlparse
2427

2528
import httpx
26-
from typing import List, Dict, Optional, Union, Any, Tuple, Callable
29+
from typing import List, Dict, Optional, Union, Any, Callable
2730

2831
from cachetools import TTLCache, LRUCache
2932
from httpx import Response
@@ -148,24 +151,43 @@ def __init__(self, conf: dict):
148151
raise ValueError("Missing required configuration property url")
149152
self.base_urls = base_urls
150153

151-
self.verify = True
152-
ca = conf_copy.pop('ssl.ca.location', None)
153-
if ca is not None:
154-
self.verify = ca
155-
154+
ca: Union[str, bool, None] = conf_copy.pop('ssl.ca.location', None)
156155
key: Optional[str] = conf_copy.pop('ssl.key.location', None)
156+
key_password: Optional[str] = conf_copy.pop('ssl.key.password', None)
157157
client_cert: Optional[str] = conf_copy.pop('ssl.certificate.location', None)
158-
self.cert: Union[str, Tuple[str, str], None] = None
159-
160-
if client_cert is not None and key is not None:
161-
self.cert = (client_cert, key)
162158

163-
if client_cert is not None and key is None:
164-
self.cert = client_cert
159+
# this mimicks legacy, deprecated behaviour of httpx
160+
# self.verify is always set to an ssl.SSLContext in case we need to load_cert_chain
161+
if ca is False:
162+
self.verify = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
163+
self.verify.check_hostname = False
164+
self.verify.verify_mode = ssl.CERT_NONE
165+
elif isinstance(ca, str):
166+
if os.path.isdir(ca):
167+
self.verify = ssl.create_default_context(capath=ca)
168+
else:
169+
self.verify = ssl.create_default_context(cafile=ca)
170+
else:
171+
if os.environ.get("SSL_CERT_FILE"):
172+
self.verify = ssl.create_default_context(cafile=os.environ["SSL_CERT_FILE"])
173+
elif os.environ.get("SSL_CERT_DIR"):
174+
self.verify = ssl.create_default_context(capath=os.environ["SSL_CERT_DIR"])
175+
else:
176+
self.verify = ssl.create_default_context(cafile=certifi.where())
177+
178+
if client_cert is not None:
179+
if key is not None and key_password is not None:
180+
self.verify.load_cert_chain(certfile=client_cert, keyfile=key, password=key_password)
181+
elif key is not None:
182+
self.verify.load_cert_chain(certfile=client_cert, keyfile=key)
183+
elif key_password is not None:
184+
self.verify.load_cert_chain(certfile=client_cert, password=key_password)
185+
else:
186+
self.verify.load_cert_chain(certfile=client_cert)
165187

166-
if key is not None and client_cert is None:
188+
if (key is not None or key_password is not None) and client_cert is None:
167189
raise ValueError("ssl.certificate.location required when"
168-
" configuring ssl.key.location")
190+
" configuring ssl.key.location or ssl.key.password")
169191

170192
parsed = urlparse(self.base_urls[0])
171193
try:
@@ -351,7 +373,6 @@ def __init__(self, conf: dict):
351373

352374
self.session = httpx.Client(
353375
verify=self.verify,
354-
cert=self.cert,
355376
auth=self.auth,
356377
proxy=self.proxy,
357378
timeout=self.timeout
@@ -516,10 +537,18 @@ class SchemaRegistryClient(object):
516537
| ``ssl.key.location`` | str | |
517538
| | | ``ssl.certificate.location`` must also be set. |
518539
+------------------------------+------+-------------------------------------------------+
519-
| | | Path to client's public key (PEM) used for |
540+
| | | Password to use to decrypt the client's private |
541+
| | | key. |
542+
| | | |
543+
| ``ssl.key.password`` | str | The private key may be provided using |
544+
| | | ``ssl.key.location``, or bundled with the |
545+
| | | certificate in ``ssl.certificate.location``. |
546+
| | | Password is optional (key may be unencrypted). |
547+
+------------------------------+------+-------------------------------------------------+
548+
| | | Path to client's certificate (PEM) used for |
520549
| | | authentication. |
521550
| ``ssl.certificate.location`` | str | |
522-
| | | May be set without ssl.key.location if the |
551+
| | | May be set without ``ssl.key.location`` if the |
523552
| | | private key is stored within the PEM as well. |
524553
+------------------------------+------+-------------------------------------------------+
525554
| | | Client HTTP credentials in the form of |

tests/schema_registry/_async/test_config.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,17 @@ def test_config_ssl_key_no_certificate():
6868
conf = {'url': TEST_URL,
6969
'ssl.key.location': '/ssl/keys/client'}
7070
with pytest.raises(ValueError, match="ssl.certificate.location required"
71-
" when configuring ssl.key.location"):
71+
" when configuring ssl.key.location"
72+
" or ssl.key.password"):
73+
AsyncSchemaRegistryClient(conf)
74+
75+
76+
def test_config_ssl_password_no_certificate():
77+
conf = {'url': TEST_URL,
78+
'ssl.key.password': 'sesame'}
79+
with pytest.raises(ValueError, match="ssl.certificate.location required"
80+
" when configuring ssl.key.location"
81+
" or ssl.key.password"):
7282
AsyncSchemaRegistryClient(conf)
7383

7484

tests/schema_registry/_sync/test_config.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,17 @@ def test_config_ssl_key_no_certificate():
6868
conf = {'url': TEST_URL,
6969
'ssl.key.location': '/ssl/keys/client'}
7070
with pytest.raises(ValueError, match="ssl.certificate.location required"
71-
" when configuring ssl.key.location"):
71+
" when configuring ssl.key.location"
72+
" or ssl.key.password"):
73+
SchemaRegistryClient(conf)
74+
75+
76+
def test_config_ssl_password_no_certificate():
77+
conf = {'url': TEST_URL,
78+
'ssl.key.password': 'sesame'}
79+
with pytest.raises(ValueError, match="ssl.certificate.location required"
80+
" when configuring ssl.key.location"
81+
" or ssl.key.password"):
7282
SchemaRegistryClient(conf)
7383

7484

0 commit comments

Comments
 (0)