adc-streaming 2.6.0__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/.github/workflows/build.yml +1 -1
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/Makefile +1 -1
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/PKG-INFO +1 -1
- adc_streaming-2.7.0/adc/auth.py +147 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/kafka.py +2 -2
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/PKG-INFO +1 -1
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/SOURCES.txt +2 -0
- adc_streaming-2.7.0/adc_streaming.egg-info/scm_file_list.json +32 -0
- adc_streaming-2.7.0/adc_streaming.egg-info/scm_version.json +8 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/tests/test_auth.py +10 -1
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/tests/test_kafka_integration.py +6 -1
- adc_streaming-2.6.0/adc/auth.py +0 -87
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/.gitignore +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/LICENSE +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/README.md +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/__init__.py +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/consumer.py +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/errors.py +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/io.py +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc/producer.py +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/dependency_links.txt +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/not-zip-safe +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/requires.txt +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/adc_streaming.egg-info/top_level.txt +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/Makefile +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/_static/css/my_theme.css +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/_templates/layout.html +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/api/api.rst +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/api/streaming.rst +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/conf.py +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/index.rst +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/user/installation.rst +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/doc/user/quickstart.rst +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/pyproject.toml +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/recipe/meta.yaml +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/setup.cfg +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/setup.py +0 -0
- {adc_streaming-2.6.0 → adc_streaming-2.7.0}/tests/test_kafka.py +0 -0
|
@@ -32,7 +32,7 @@ lint :
|
|
|
32
32
|
# stop the build if there are Python syntax errors or undefined names
|
|
33
33
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
34
34
|
# exit-zero treats all errors as warnings
|
|
35
|
-
flake8 . --count --exit-zero --max-complexity=
|
|
35
|
+
flake8 . --count --exit-zero --max-complexity=20 --max-line-length=100 --statistics
|
|
36
36
|
|
|
37
37
|
.PHONY: format
|
|
38
38
|
format :
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from enum import Enum
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
import certifi
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("adc-streaming.auth")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SASLMethod(Enum):
|
|
16
|
+
"""SASL method to use for authentication.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
PLAIN = 1
|
|
20
|
+
SCRAM_SHA_256 = 2
|
|
21
|
+
SCRAM_SHA_512 = 3
|
|
22
|
+
OAUTHBEARER = 4
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return self.name.replace("_", "-")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SASLAuth(object):
|
|
29
|
+
"""Attach SASL-based authentication to a client.
|
|
30
|
+
|
|
31
|
+
Returns client-based auth options when called.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
user : `str`
|
|
36
|
+
Username to authenticate with.
|
|
37
|
+
password : `str`
|
|
38
|
+
Password to authenticate with.
|
|
39
|
+
ssl : `bool`, optional
|
|
40
|
+
Whether to enable SSL (enabled by default).
|
|
41
|
+
method : `SASLMethod`, optional
|
|
42
|
+
The SASL method to authenticate. The default is SASLMethod.OAUTHBEARER
|
|
43
|
+
if token_endpoint is provided, or SASLMethod.PLAIN otherwise.
|
|
44
|
+
See valid SASL methods in SASLMethod.
|
|
45
|
+
ssl_ca_location : `str`, optional
|
|
46
|
+
If using SSL via a self-signed cert, a path/location
|
|
47
|
+
to the certificate.
|
|
48
|
+
ssl_endpoint_identification_algorithm : `str`, optional
|
|
49
|
+
If using SSL, the algorithm used to verify that certificate is valid for the endpoint.
|
|
50
|
+
token_endpoint : `str`, optional
|
|
51
|
+
The OpenID Connect token endpoint URL.
|
|
52
|
+
Required for OAUTHBEARER / OpenID Connect, otherwise ignored.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, user, password, ssl=True, method=None, token_endpoint=None, **kwargs):
|
|
57
|
+
if method is None:
|
|
58
|
+
if token_endpoint is not None or "token_command" in kwargs:
|
|
59
|
+
method = SASLMethod.OAUTHBEARER
|
|
60
|
+
else:
|
|
61
|
+
method = SASLMethod.PLAIN
|
|
62
|
+
|
|
63
|
+
self._method = method
|
|
64
|
+
|
|
65
|
+
# set up SSL options
|
|
66
|
+
if ssl:
|
|
67
|
+
if "ssl_ca_location" in kwargs:
|
|
68
|
+
ssl_cert = kwargs["ssl_ca_location"]
|
|
69
|
+
else:
|
|
70
|
+
ssl_cert = certifi.where()
|
|
71
|
+
|
|
72
|
+
self._config = {
|
|
73
|
+
"security.protocol": "SASL_SSL",
|
|
74
|
+
"ssl.ca.location": ssl_cert,
|
|
75
|
+
"https.ca.location": ssl_cert,
|
|
76
|
+
}
|
|
77
|
+
if "ssl_endpoint_identification_algorithm" in kwargs:
|
|
78
|
+
self._config["ssl.endpoint.identification.algorithm"] = \
|
|
79
|
+
kwargs["ssl_endpoint_identification_algorithm"]
|
|
80
|
+
else:
|
|
81
|
+
self._config = {"security.protocol": "SASL_PLAINTEXT"}
|
|
82
|
+
|
|
83
|
+
# set up SASL options
|
|
84
|
+
self._config["sasl.mechanism"] = str(self._method)
|
|
85
|
+
if method == SASLMethod.OAUTHBEARER:
|
|
86
|
+
if token_endpoint is not None:
|
|
87
|
+
self._config["sasl.oauthbearer.client.id"] = user
|
|
88
|
+
self._config["sasl.oauthbearer.client.secret"] = password
|
|
89
|
+
self._config["sasl.oauthbearer.method"] = "oidc"
|
|
90
|
+
self._config["sasl.oauthbearer.token.endpoint.url"] = token_endpoint
|
|
91
|
+
elif "token_command" in kwargs:
|
|
92
|
+
token_command = kwargs["token_command"]
|
|
93
|
+
self._config["sasl.oauthbearer.method"] = "default"
|
|
94
|
+
self._config["oauth_cb"] = lambda c: SASLAuth.external_token_callback(token_command)
|
|
95
|
+
else:
|
|
96
|
+
self._config["sasl.username"] = user
|
|
97
|
+
self._config["sasl.password"] = password
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def external_token_callback(command):
|
|
101
|
+
try:
|
|
102
|
+
presult = subprocess.run(command, shell=True, capture_output=True)
|
|
103
|
+
if presult.returncode != 0:
|
|
104
|
+
raise RuntimeError("Token callback command failed: " + presult.stderr)
|
|
105
|
+
rawdata = presult.stdout
|
|
106
|
+
# need to parse the resulting JWT enough to extract the expiration time
|
|
107
|
+
# we do no other validation, since downstream systems should be responsible
|
|
108
|
+
# for that already
|
|
109
|
+
sections = rawdata.split(b'.')
|
|
110
|
+
if len(sections) != 3:
|
|
111
|
+
raise RuntimeError("Token callback output does not appear to be a valid JWT")
|
|
112
|
+
try:
|
|
113
|
+
# The JWT spec mandates that base 64 padding be omitted, but
|
|
114
|
+
# base64.urlsafe_b64decode requires it, so we must put it back.
|
|
115
|
+
m = len(sections[1]) % 4
|
|
116
|
+
if m == 2:
|
|
117
|
+
sections[1] += b"=="
|
|
118
|
+
elif m == 3:
|
|
119
|
+
sections[1] += b"="
|
|
120
|
+
elif m == 1:
|
|
121
|
+
raise RuntimeError("Token callback output is not valid base64 data")
|
|
122
|
+
claims = json.loads(base64.urlsafe_b64decode(sections[1]).decode("utf-8"))
|
|
123
|
+
except UnicodeDecodeError:
|
|
124
|
+
raise RuntimeError("Token callback output is not valid UTF-8 after base64 decoding")
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
raise RuntimeError("Token callback output is not valid JSON after base64 and "
|
|
127
|
+
"UTF-8 decoding")
|
|
128
|
+
if not isinstance(claims, Mapping):
|
|
129
|
+
raise RuntimeError("Token callback output does not contain valid claims")
|
|
130
|
+
if "sub" not in claims:
|
|
131
|
+
raise RuntimeError("Token callback output does not contain an subject claim")
|
|
132
|
+
if "exp" not in claims:
|
|
133
|
+
raise RuntimeError("Token callback output does not contain an expiration claim")
|
|
134
|
+
exp_value = claims["exp"]
|
|
135
|
+
if not isinstance(exp_value, int) and not isinstance(exp_value, float):
|
|
136
|
+
raise RuntimeError("Token expiration value is not a number")
|
|
137
|
+
exp_value = float(exp_value)
|
|
138
|
+
try:
|
|
139
|
+
return (rawdata.decode("utf-8").strip(), exp_value, claims["sub"], {})
|
|
140
|
+
except UnicodeDecodeError:
|
|
141
|
+
raise RuntimeError("Token callback output is not valid UTF-8 data")
|
|
142
|
+
except Exception as ex:
|
|
143
|
+
logger.error(f"Token callback error: {ex}")
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
def __call__(self):
|
|
147
|
+
return self._config
|
|
@@ -14,12 +14,12 @@ def parse_kafka_url(val):
|
|
|
14
14
|
if parsed.scheme != "kafka":
|
|
15
15
|
raise ValueError("invalid kafka URL: must start with 'kafka://'")
|
|
16
16
|
|
|
17
|
+
group_id = parsed.username
|
|
18
|
+
# intentionally ignore passwords embedded in URLs to discourage their use
|
|
17
19
|
split_netloc = parsed.netloc.split("@", maxsplit=1)
|
|
18
20
|
if len(split_netloc) == 2:
|
|
19
|
-
group_id = split_netloc[0]
|
|
20
21
|
broker_addresses = split_netloc[1].split(",")
|
|
21
22
|
else:
|
|
22
|
-
group_id = None
|
|
23
23
|
broker_addresses = split_netloc[0].split(",")
|
|
24
24
|
|
|
25
25
|
topics = parsed.path.lstrip("/")
|
|
@@ -18,6 +18,8 @@ adc_streaming.egg-info/SOURCES.txt
|
|
|
18
18
|
adc_streaming.egg-info/dependency_links.txt
|
|
19
19
|
adc_streaming.egg-info/not-zip-safe
|
|
20
20
|
adc_streaming.egg-info/requires.txt
|
|
21
|
+
adc_streaming.egg-info/scm_file_list.json
|
|
22
|
+
adc_streaming.egg-info/scm_version.json
|
|
21
23
|
adc_streaming.egg-info/top_level.txt
|
|
22
24
|
doc/Makefile
|
|
23
25
|
doc/conf.py
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
".github/workflows/build.yml",
|
|
4
|
+
".gitignore",
|
|
5
|
+
"LICENSE",
|
|
6
|
+
"Makefile",
|
|
7
|
+
"README.md",
|
|
8
|
+
"adc/__init__.py",
|
|
9
|
+
"adc/auth.py",
|
|
10
|
+
"adc/consumer.py",
|
|
11
|
+
"adc/errors.py",
|
|
12
|
+
"adc/io.py",
|
|
13
|
+
"adc/kafka.py",
|
|
14
|
+
"adc/producer.py",
|
|
15
|
+
"doc/Makefile",
|
|
16
|
+
"doc/_static/css/my_theme.css",
|
|
17
|
+
"doc/_templates/layout.html",
|
|
18
|
+
"doc/api/api.rst",
|
|
19
|
+
"doc/api/streaming.rst",
|
|
20
|
+
"doc/conf.py",
|
|
21
|
+
"doc/index.rst",
|
|
22
|
+
"doc/user/installation.rst",
|
|
23
|
+
"doc/user/quickstart.rst",
|
|
24
|
+
"pyproject.toml",
|
|
25
|
+
"recipe/meta.yaml",
|
|
26
|
+
"setup.cfg",
|
|
27
|
+
"setup.py",
|
|
28
|
+
"tests/test_auth.py",
|
|
29
|
+
"tests/test_kafka.py",
|
|
30
|
+
"tests/test_kafka_integration.py"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -28,7 +28,16 @@ from adc.auth import SASLAuth
|
|
|
28
28
|
'sasl.password': 'test-pass',
|
|
29
29
|
'security.protocol': 'SASL_SSL'
|
|
30
30
|
}
|
|
31
|
-
]
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
SASLAuth(
|
|
34
|
+
'test', '', token_command="generate_token"
|
|
35
|
+
),
|
|
36
|
+
{
|
|
37
|
+
'sasl.mechanism': 'OAUTHBEARER',
|
|
38
|
+
'sasl.oauthbearer.method': 'default',
|
|
39
|
+
}
|
|
40
|
+
],
|
|
32
41
|
])
|
|
33
42
|
def test_auth(auth, expected_config):
|
|
34
43
|
# Check that the key/value pairs in expected_config are a subset
|
|
@@ -5,7 +5,11 @@ import unittest
|
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
6
|
from typing import List, Optional
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
try:
|
|
9
|
+
import docker
|
|
10
|
+
have_docker = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
have_docker = False
|
|
9
13
|
import pytest
|
|
10
14
|
|
|
11
15
|
import adc.consumer
|
|
@@ -17,6 +21,7 @@ logging.getLogger("adc-streaming").setLevel(logging.DEBUG)
|
|
|
17
21
|
logger = logging.getLogger("adc-streaming.tests")
|
|
18
22
|
|
|
19
23
|
|
|
24
|
+
@pytest.mark.skipif(not have_docker, reason="requires docker")
|
|
20
25
|
@pytest.mark.integration_test
|
|
21
26
|
class KafkaIntegrationTestCase(unittest.TestCase):
|
|
22
27
|
"""This test runs a Kafka broker in a Docker container, and makes sure that
|
adc_streaming-2.6.0/adc/auth.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
|
|
3
|
-
from enum import Enum
|
|
4
|
-
|
|
5
|
-
import certifi
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class SASLMethod(Enum):
|
|
9
|
-
"""SASL method to use for authentication.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
PLAIN = 1
|
|
13
|
-
SCRAM_SHA_256 = 2
|
|
14
|
-
SCRAM_SHA_512 = 3
|
|
15
|
-
OAUTHBEARER = 4
|
|
16
|
-
|
|
17
|
-
def __str__(self):
|
|
18
|
-
return self.name.replace("_", "-")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class SASLAuth(object):
|
|
22
|
-
"""Attach SASL-based authentication to a client.
|
|
23
|
-
|
|
24
|
-
Returns client-based auth options when called.
|
|
25
|
-
|
|
26
|
-
Parameters
|
|
27
|
-
----------
|
|
28
|
-
user : `str`
|
|
29
|
-
Username to authenticate with.
|
|
30
|
-
password : `str`
|
|
31
|
-
Password to authenticate with.
|
|
32
|
-
ssl : `bool`, optional
|
|
33
|
-
Whether to enable SSL (enabled by default).
|
|
34
|
-
method : `SASLMethod`, optional
|
|
35
|
-
The SASL method to authenticate. The default is SASLMethod.OAUTHBEARER
|
|
36
|
-
if token_endpoint is provided, or SASLMethod.PLAIN otherwise.
|
|
37
|
-
See valid SASL methods in SASLMethod.
|
|
38
|
-
ssl_ca_location : `str`, optional
|
|
39
|
-
If using SSL via a self-signed cert, a path/location
|
|
40
|
-
to the certificate.
|
|
41
|
-
ssl_endpoint_identification_algorithm : `str`, optional
|
|
42
|
-
If using SSL, the algorithm used to verify that certificate is valid for the endpoint.
|
|
43
|
-
token_endpoint : `str`, optional
|
|
44
|
-
The OpenID Connect token endpoint URL.
|
|
45
|
-
Required for OAUTHBEARER / OpenID Connect, otherwise ignored.
|
|
46
|
-
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
def __init__(self, user, password, ssl=True, method=None, token_endpoint=None, **kwargs):
|
|
50
|
-
if method is None:
|
|
51
|
-
if token_endpoint is None:
|
|
52
|
-
method = SASLMethod.PLAIN
|
|
53
|
-
else:
|
|
54
|
-
method = SASLMethod.OAUTHBEARER
|
|
55
|
-
self._method = method
|
|
56
|
-
|
|
57
|
-
# set up SSL options
|
|
58
|
-
if ssl:
|
|
59
|
-
if "ssl_ca_location" in kwargs:
|
|
60
|
-
ssl_cert = kwargs["ssl_ca_location"]
|
|
61
|
-
else:
|
|
62
|
-
ssl_cert = certifi.where()
|
|
63
|
-
|
|
64
|
-
self._config = {
|
|
65
|
-
"security.protocol": "SASL_SSL",
|
|
66
|
-
"ssl.ca.location": ssl_cert,
|
|
67
|
-
"https.ca.location": ssl_cert,
|
|
68
|
-
}
|
|
69
|
-
if "ssl_endpoint_identification_algorithm" in kwargs:
|
|
70
|
-
self._config["ssl.endpoint.identification.algorithm"] = \
|
|
71
|
-
kwargs["ssl_endpoint_identification_algorithm"]
|
|
72
|
-
else:
|
|
73
|
-
self._config = {"security.protocol": "SASL_PLAINTEXT"}
|
|
74
|
-
|
|
75
|
-
# set up SASL options
|
|
76
|
-
self._config["sasl.mechanism"] = str(self._method)
|
|
77
|
-
if token_endpoint:
|
|
78
|
-
self._config["sasl.oauthbearer.client.id"] = user
|
|
79
|
-
self._config["sasl.oauthbearer.client.secret"] = password
|
|
80
|
-
self._config["sasl.oauthbearer.method"] = "oidc"
|
|
81
|
-
self._config["sasl.oauthbearer.token.endpoint.url"] = token_endpoint
|
|
82
|
-
else:
|
|
83
|
-
self._config["sasl.username"] = user
|
|
84
|
-
self._config["sasl.password"] = password
|
|
85
|
-
|
|
86
|
-
def __call__(self):
|
|
87
|
-
return self._config
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|