kafka-python 3.0.0__py3-none-any.whl
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.
- kafka/__init__.py +34 -0
- kafka/__main__.py +5 -0
- kafka/admin/__init__.py +29 -0
- kafka/admin/__main__.py +5 -0
- kafka/admin/_acls.py +355 -0
- kafka/admin/_cluster.py +359 -0
- kafka/admin/_configs.py +479 -0
- kafka/admin/_groups.py +754 -0
- kafka/admin/_partitions.py +595 -0
- kafka/admin/_topics.py +281 -0
- kafka/admin/_transactions.py +450 -0
- kafka/admin/_users.py +194 -0
- kafka/admin/client.py +373 -0
- kafka/benchmarks/__init__.py +0 -0
- kafka/benchmarks/consumer_performance.py +138 -0
- kafka/benchmarks/load_example.py +109 -0
- kafka/benchmarks/producer_encode_path.py +201 -0
- kafka/benchmarks/producer_performance.py +161 -0
- kafka/benchmarks/profile_protocol.py +138 -0
- kafka/benchmarks/protocol_old_vs_new.py +447 -0
- kafka/benchmarks/record_batch_compose.py +77 -0
- kafka/benchmarks/record_batch_read.py +82 -0
- kafka/benchmarks/varint_speed.py +426 -0
- kafka/cli/__init__.py +36 -0
- kafka/cli/admin/__init__.py +117 -0
- kafka/cli/admin/acls/__init__.py +9 -0
- kafka/cli/admin/acls/common.py +76 -0
- kafka/cli/admin/acls/create.py +19 -0
- kafka/cli/admin/acls/delete.py +23 -0
- kafka/cli/admin/acls/describe.py +16 -0
- kafka/cli/admin/cluster/__init__.py +14 -0
- kafka/cli/admin/cluster/describe.py +11 -0
- kafka/cli/admin/cluster/describe_quorum.py +11 -0
- kafka/cli/admin/cluster/features.py +52 -0
- kafka/cli/admin/cluster/log_dirs.py +43 -0
- kafka/cli/admin/cluster/versions.py +33 -0
- kafka/cli/admin/configs/__init__.py +10 -0
- kafka/cli/admin/configs/alter.py +43 -0
- kafka/cli/admin/configs/common.py +17 -0
- kafka/cli/admin/configs/describe.py +30 -0
- kafka/cli/admin/configs/list.py +16 -0
- kafka/cli/admin/configs/reset.py +20 -0
- kafka/cli/admin/groups/__init__.py +16 -0
- kafka/cli/admin/groups/alter_offsets.py +30 -0
- kafka/cli/admin/groups/delete.py +11 -0
- kafka/cli/admin/groups/delete_offsets.py +29 -0
- kafka/cli/admin/groups/describe.py +11 -0
- kafka/cli/admin/groups/list.py +28 -0
- kafka/cli/admin/groups/list_offsets.py +29 -0
- kafka/cli/admin/groups/remove_members.py +40 -0
- kafka/cli/admin/groups/reset_offsets.py +139 -0
- kafka/cli/admin/partitions/__init__.py +21 -0
- kafka/cli/admin/partitions/alter_reassignments.py +37 -0
- kafka/cli/admin/partitions/create.py +27 -0
- kafka/cli/admin/partitions/delete_records.py +31 -0
- kafka/cli/admin/partitions/describe.py +36 -0
- kafka/cli/admin/partitions/elect_leaders.py +53 -0
- kafka/cli/admin/partitions/list_offsets.py +88 -0
- kafka/cli/admin/partitions/list_reassignments.py +35 -0
- kafka/cli/admin/topics/__init__.py +10 -0
- kafka/cli/admin/topics/create.py +13 -0
- kafka/cli/admin/topics/delete.py +19 -0
- kafka/cli/admin/topics/describe.py +18 -0
- kafka/cli/admin/topics/list.py +11 -0
- kafka/cli/admin/transactions/__init__.py +17 -0
- kafka/cli/admin/transactions/abort.py +38 -0
- kafka/cli/admin/transactions/describe.py +24 -0
- kafka/cli/admin/transactions/describe_producers.py +29 -0
- kafka/cli/admin/transactions/find_hanging.py +26 -0
- kafka/cli/admin/transactions/list.py +37 -0
- kafka/cli/admin/users/__init__.py +8 -0
- kafka/cli/admin/users/alter_user_scram_credentials.py +34 -0
- kafka/cli/admin/users/describe_user_scram_credentials.py +15 -0
- kafka/cli/common.py +95 -0
- kafka/cli/consumer/__init__.py +63 -0
- kafka/cli/producer/__init__.py +57 -0
- kafka/cluster.py +824 -0
- kafka/codec.py +325 -0
- kafka/consumer/__init__.py +5 -0
- kafka/consumer/__main__.py +5 -0
- kafka/consumer/fetcher.py +2012 -0
- kafka/consumer/group.py +1347 -0
- kafka/consumer/subscription_state.py +897 -0
- kafka/coordinator/__init__.py +0 -0
- kafka/coordinator/assignors/__init__.py +0 -0
- kafka/coordinator/assignors/abstract.py +90 -0
- kafka/coordinator/assignors/cooperative_sticky.py +167 -0
- kafka/coordinator/assignors/range.py +81 -0
- kafka/coordinator/assignors/roundrobin.py +101 -0
- kafka/coordinator/assignors/sticky/StickyAssignorUserData.json +37 -0
- kafka/coordinator/assignors/sticky/__init__.py +0 -0
- kafka/coordinator/assignors/sticky/partition_movements.py +149 -0
- kafka/coordinator/assignors/sticky/sorted_set.py +63 -0
- kafka/coordinator/assignors/sticky/sticky_assignor.py +665 -0
- kafka/coordinator/assignors/sticky/user_data.py +8 -0
- kafka/coordinator/base.py +1215 -0
- kafka/coordinator/consumer.py +1224 -0
- kafka/coordinator/heartbeat.py +82 -0
- kafka/coordinator/subscription.py +34 -0
- kafka/errors.py +1004 -0
- kafka/future.py +166 -0
- kafka/metrics/__init__.py +13 -0
- kafka/metrics/compound_stat.py +33 -0
- kafka/metrics/dict_reporter.py +81 -0
- kafka/metrics/kafka_metric.py +36 -0
- kafka/metrics/measurable.py +27 -0
- kafka/metrics/measurable_stat.py +13 -0
- kafka/metrics/metric_config.py +33 -0
- kafka/metrics/metric_name.py +105 -0
- kafka/metrics/metrics.py +261 -0
- kafka/metrics/metrics_reporter.py +53 -0
- kafka/metrics/quota.py +41 -0
- kafka/metrics/stat.py +19 -0
- kafka/metrics/stats/__init__.py +15 -0
- kafka/metrics/stats/avg.py +24 -0
- kafka/metrics/stats/count.py +17 -0
- kafka/metrics/stats/histogram.py +99 -0
- kafka/metrics/stats/max_stat.py +17 -0
- kafka/metrics/stats/min_stat.py +19 -0
- kafka/metrics/stats/percentile.py +14 -0
- kafka/metrics/stats/percentiles.py +75 -0
- kafka/metrics/stats/rate.py +118 -0
- kafka/metrics/stats/sampled_stat.py +99 -0
- kafka/metrics/stats/sensor.py +136 -0
- kafka/metrics/stats/total.py +15 -0
- kafka/net/__init__.py +19 -0
- kafka/net/compat.py +165 -0
- kafka/net/connection.py +593 -0
- kafka/net/http_connect.py +144 -0
- kafka/net/inet.py +122 -0
- kafka/net/manager.py +451 -0
- kafka/net/metrics.py +149 -0
- kafka/net/sasl/__init__.py +32 -0
- kafka/net/sasl/abc.py +28 -0
- kafka/net/sasl/gssapi.py +95 -0
- kafka/net/sasl/msk.py +245 -0
- kafka/net/sasl/oauth.py +98 -0
- kafka/net/sasl/plain.py +42 -0
- kafka/net/sasl/scram.py +135 -0
- kafka/net/sasl/sspi.py +111 -0
- kafka/net/selector.py +644 -0
- kafka/net/socks5.py +262 -0
- kafka/net/transport.py +415 -0
- kafka/net/wakeup_notifier.py +72 -0
- kafka/partitioner/__init__.py +8 -0
- kafka/partitioner/abc.py +8 -0
- kafka/partitioner/default.py +89 -0
- kafka/partitioner/sticky.py +109 -0
- kafka/producer/__init__.py +5 -0
- kafka/producer/__main__.py +5 -0
- kafka/producer/future.py +101 -0
- kafka/producer/kafka.py +1123 -0
- kafka/producer/producer_batch.py +192 -0
- kafka/producer/record_accumulator.py +647 -0
- kafka/producer/sender.py +884 -0
- kafka/producer/transaction_manager.py +1326 -0
- kafka/protocol/__init__.py +0 -0
- kafka/protocol/admin/__init__.py +29 -0
- kafka/protocol/admin/acl.py +83 -0
- kafka/protocol/admin/acl.pyi +375 -0
- kafka/protocol/admin/client_quotas.py +14 -0
- kafka/protocol/admin/client_quotas.pyi +265 -0
- kafka/protocol/admin/cluster.py +31 -0
- kafka/protocol/admin/cluster.pyi +620 -0
- kafka/protocol/admin/configs.py +22 -0
- kafka/protocol/admin/configs.pyi +437 -0
- kafka/protocol/admin/groups.py +24 -0
- kafka/protocol/admin/groups.pyi +261 -0
- kafka/protocol/admin/topics.py +53 -0
- kafka/protocol/admin/topics.pyi +982 -0
- kafka/protocol/admin/transactions.py +18 -0
- kafka/protocol/admin/transactions.pyi +311 -0
- kafka/protocol/admin/users.py +14 -0
- kafka/protocol/admin/users.pyi +223 -0
- kafka/protocol/api_data.py +125 -0
- kafka/protocol/api_header.py +55 -0
- kafka/protocol/api_key.py +97 -0
- kafka/protocol/api_message.py +277 -0
- kafka/protocol/broker_version_data.py +246 -0
- kafka/protocol/consumer/__init__.py +13 -0
- kafka/protocol/consumer/fetch.py +16 -0
- kafka/protocol/consumer/fetch.pyi +298 -0
- kafka/protocol/consumer/group.py +38 -0
- kafka/protocol/consumer/group.pyi +824 -0
- kafka/protocol/consumer/metadata.py +30 -0
- kafka/protocol/consumer/metadata.pyi +89 -0
- kafka/protocol/consumer/offsets.py +75 -0
- kafka/protocol/consumer/offsets.pyi +288 -0
- kafka/protocol/data_container.py +166 -0
- kafka/protocol/frame.py +30 -0
- kafka/protocol/generate_stubs.py +468 -0
- kafka/protocol/metadata/__init__.py +10 -0
- kafka/protocol/metadata/api_versions.py +41 -0
- kafka/protocol/metadata/api_versions.pyi +128 -0
- kafka/protocol/metadata/find_coordinator.py +19 -0
- kafka/protocol/metadata/find_coordinator.pyi +105 -0
- kafka/protocol/metadata/metadata.py +34 -0
- kafka/protocol/metadata/metadata.pyi +160 -0
- kafka/protocol/old/__init__.py +0 -0
- kafka/protocol/old/abstract.py +17 -0
- kafka/protocol/old/add_offsets_to_txn.py +54 -0
- kafka/protocol/old/add_partitions_to_txn.py +71 -0
- kafka/protocol/old/admin.py +1086 -0
- kafka/protocol/old/api.py +205 -0
- kafka/protocol/old/api_versions.py +133 -0
- kafka/protocol/old/commit.py +355 -0
- kafka/protocol/old/consumer_protocol.py +36 -0
- kafka/protocol/old/end_txn.py +53 -0
- kafka/protocol/old/fetch.py +408 -0
- kafka/protocol/old/find_coordinator.py +72 -0
- kafka/protocol/old/group.py +451 -0
- kafka/protocol/old/init_producer_id.py +42 -0
- kafka/protocol/old/list_offsets.py +186 -0
- kafka/protocol/old/metadata.py +290 -0
- kafka/protocol/old/offset_for_leader_epoch.py +133 -0
- kafka/protocol/old/produce.py +247 -0
- kafka/protocol/old/sasl_authenticate.py +38 -0
- kafka/protocol/old/sasl_handshake.py +39 -0
- kafka/protocol/old/struct.py +87 -0
- kafka/protocol/old/txn_offset_commit.py +73 -0
- kafka/protocol/old/types.py +440 -0
- kafka/protocol/parser.py +191 -0
- kafka/protocol/producer/__init__.py +7 -0
- kafka/protocol/producer/produce.py +17 -0
- kafka/protocol/producer/produce.pyi +197 -0
- kafka/protocol/producer/transaction.py +30 -0
- kafka/protocol/producer/transaction.pyi +663 -0
- kafka/protocol/sasl.py +52 -0
- kafka/protocol/sasl.pyi +126 -0
- kafka/protocol/schemas/__init__.py +7 -0
- kafka/protocol/schemas/fields/__init__.py +7 -0
- kafka/protocol/schemas/fields/array.py +127 -0
- kafka/protocol/schemas/fields/base.py +156 -0
- kafka/protocol/schemas/fields/codecs/__init__.py +12 -0
- kafka/protocol/schemas/fields/codecs/encode_buffer.py +82 -0
- kafka/protocol/schemas/fields/codecs/tagged_fields.py +109 -0
- kafka/protocol/schemas/fields/codecs/types.py +505 -0
- kafka/protocol/schemas/fields/codegen.py +40 -0
- kafka/protocol/schemas/fields/simple.py +127 -0
- kafka/protocol/schemas/fields/struct.py +357 -0
- kafka/protocol/schemas/fields/struct_array.py +142 -0
- kafka/protocol/schemas/load_json.py +42 -0
- kafka/protocol/schemas/resources/AddOffsetsToTxnRequest.json +40 -0
- kafka/protocol/schemas/resources/AddOffsetsToTxnResponse.json +35 -0
- kafka/protocol/schemas/resources/AddPartitionsToTxnRequest.json +65 -0
- kafka/protocol/schemas/resources/AddPartitionsToTxnResponse.json +60 -0
- kafka/protocol/schemas/resources/AlterClientQuotasRequest.json +47 -0
- kafka/protocol/schemas/resources/AlterClientQuotasResponse.json +41 -0
- kafka/protocol/schemas/resources/AlterConfigsRequest.json +43 -0
- kafka/protocol/schemas/resources/AlterConfigsResponse.json +39 -0
- kafka/protocol/schemas/resources/AlterPartitionReassignmentsRequest.json +42 -0
- kafka/protocol/schemas/resources/AlterPartitionReassignmentsResponse.json +47 -0
- kafka/protocol/schemas/resources/AlterReplicaLogDirsRequest.json +41 -0
- kafka/protocol/schemas/resources/AlterReplicaLogDirsResponse.json +41 -0
- kafka/protocol/schemas/resources/AlterUserScramCredentialsRequest.json +45 -0
- kafka/protocol/schemas/resources/AlterUserScramCredentialsResponse.json +35 -0
- kafka/protocol/schemas/resources/ApiVersionsRequest.json +34 -0
- kafka/protocol/schemas/resources/ApiVersionsResponse.json +79 -0
- kafka/protocol/schemas/resources/ConsumerProtocolAssignment.json +42 -0
- kafka/protocol/schemas/resources/ConsumerProtocolSubscription.json +49 -0
- kafka/protocol/schemas/resources/CreateAclsRequest.json +46 -0
- kafka/protocol/schemas/resources/CreateAclsResponse.json +37 -0
- kafka/protocol/schemas/resources/CreatePartitionsRequest.json +47 -0
- kafka/protocol/schemas/resources/CreatePartitionsResponse.json +41 -0
- kafka/protocol/schemas/resources/CreateTopicsRequest.json +65 -0
- kafka/protocol/schemas/resources/CreateTopicsResponse.json +72 -0
- kafka/protocol/schemas/resources/DeleteAclsRequest.json +46 -0
- kafka/protocol/schemas/resources/DeleteAclsResponse.json +59 -0
- kafka/protocol/schemas/resources/DeleteGroupsRequest.json +30 -0
- kafka/protocol/schemas/resources/DeleteGroupsResponse.json +36 -0
- kafka/protocol/schemas/resources/DeleteRecordsRequest.json +42 -0
- kafka/protocol/schemas/resources/DeleteRecordsResponse.json +43 -0
- kafka/protocol/schemas/resources/DeleteTopicsRequest.json +43 -0
- kafka/protocol/schemas/resources/DeleteTopicsResponse.json +52 -0
- kafka/protocol/schemas/resources/DescribeAclsRequest.json +43 -0
- kafka/protocol/schemas/resources/DescribeAclsResponse.json +55 -0
- kafka/protocol/schemas/resources/DescribeClientQuotasRequest.json +37 -0
- kafka/protocol/schemas/resources/DescribeClientQuotasResponse.json +47 -0
- kafka/protocol/schemas/resources/DescribeClusterRequest.json +35 -0
- kafka/protocol/schemas/resources/DescribeClusterResponse.json +56 -0
- kafka/protocol/schemas/resources/DescribeConfigsRequest.json +42 -0
- kafka/protocol/schemas/resources/DescribeConfigsResponse.json +69 -0
- kafka/protocol/schemas/resources/DescribeGroupsRequest.json +38 -0
- kafka/protocol/schemas/resources/DescribeGroupsResponse.json +74 -0
- kafka/protocol/schemas/resources/DescribeLogDirsRequest.json +38 -0
- kafka/protocol/schemas/resources/DescribeLogDirsResponse.json +65 -0
- kafka/protocol/schemas/resources/DescribeProducersRequest.json +32 -0
- kafka/protocol/schemas/resources/DescribeProducersResponse.json +55 -0
- kafka/protocol/schemas/resources/DescribeQuorumRequest.json +39 -0
- kafka/protocol/schemas/resources/DescribeQuorumResponse.json +82 -0
- kafka/protocol/schemas/resources/DescribeTopicPartitionsRequest.json +40 -0
- kafka/protocol/schemas/resources/DescribeTopicPartitionsResponse.json +66 -0
- kafka/protocol/schemas/resources/DescribeTransactionsRequest.json +27 -0
- kafka/protocol/schemas/resources/DescribeTransactionsResponse.json +52 -0
- kafka/protocol/schemas/resources/DescribeUserScramCredentialsRequest.json +30 -0
- kafka/protocol/schemas/resources/DescribeUserScramCredentialsResponse.json +45 -0
- kafka/protocol/schemas/resources/ElectLeadersRequest.json +41 -0
- kafka/protocol/schemas/resources/ElectLeadersResponse.json +45 -0
- kafka/protocol/schemas/resources/EndTxnRequest.json +43 -0
- kafka/protocol/schemas/resources/EndTxnResponse.json +41 -0
- kafka/protocol/schemas/resources/FetchRequest.json +125 -0
- kafka/protocol/schemas/resources/FetchResponse.json +124 -0
- kafka/protocol/schemas/resources/FindCoordinatorRequest.json +43 -0
- kafka/protocol/schemas/resources/FindCoordinatorResponse.json +58 -0
- kafka/protocol/schemas/resources/HeartbeatRequest.json +39 -0
- kafka/protocol/schemas/resources/HeartbeatResponse.json +35 -0
- kafka/protocol/schemas/resources/IncrementalAlterConfigsRequest.json +44 -0
- kafka/protocol/schemas/resources/IncrementalAlterConfigsResponse.json +38 -0
- kafka/protocol/schemas/resources/InitProducerIdRequest.json +50 -0
- kafka/protocol/schemas/resources/InitProducerIdResponse.json +47 -0
- kafka/protocol/schemas/resources/JoinGroupRequest.json +63 -0
- kafka/protocol/schemas/resources/JoinGroupResponse.json +69 -0
- kafka/protocol/schemas/resources/LeaveGroupRequest.json +47 -0
- kafka/protocol/schemas/resources/LeaveGroupResponse.json +47 -0
- kafka/protocol/schemas/resources/ListConfigResourcesRequest.json +31 -0
- kafka/protocol/schemas/resources/ListConfigResourcesResponse.json +37 -0
- kafka/protocol/schemas/resources/ListGroupsRequest.json +36 -0
- kafka/protocol/schemas/resources/ListGroupsResponse.json +49 -0
- kafka/protocol/schemas/resources/ListOffsetsRequest.json +72 -0
- kafka/protocol/schemas/resources/ListOffsetsResponse.json +71 -0
- kafka/protocol/schemas/resources/ListPartitionReassignmentsRequest.json +34 -0
- kafka/protocol/schemas/resources/ListPartitionReassignmentsResponse.json +46 -0
- kafka/protocol/schemas/resources/ListTransactionsRequest.json +40 -0
- kafka/protocol/schemas/resources/ListTransactionsResponse.json +42 -0
- kafka/protocol/schemas/resources/MetadataRequest.json +56 -0
- kafka/protocol/schemas/resources/MetadataResponse.json +101 -0
- kafka/protocol/schemas/resources/OffsetCommitRequest.json +76 -0
- kafka/protocol/schemas/resources/OffsetCommitResponse.json +71 -0
- kafka/protocol/schemas/resources/OffsetDeleteRequest.json +39 -0
- kafka/protocol/schemas/resources/OffsetDeleteResponse.json +42 -0
- kafka/protocol/schemas/resources/OffsetFetchRequest.json +76 -0
- kafka/protocol/schemas/resources/OffsetFetchResponse.json +107 -0
- kafka/protocol/schemas/resources/OffsetForLeaderEpochRequest.json +52 -0
- kafka/protocol/schemas/resources/OffsetForLeaderEpochResponse.json +51 -0
- kafka/protocol/schemas/resources/ProduceRequest.json +73 -0
- kafka/protocol/schemas/resources/ProduceResponse.json +96 -0
- kafka/protocol/schemas/resources/RequestHeader.json +44 -0
- kafka/protocol/schemas/resources/ResponseHeader.json +26 -0
- kafka/protocol/schemas/resources/SaslAuthenticateRequest.json +29 -0
- kafka/protocol/schemas/resources/SaslAuthenticateResponse.json +34 -0
- kafka/protocol/schemas/resources/SaslHandshakeRequest.json +31 -0
- kafka/protocol/schemas/resources/SaslHandshakeResponse.json +32 -0
- kafka/protocol/schemas/resources/SyncGroupRequest.json +56 -0
- kafka/protocol/schemas/resources/SyncGroupResponse.json +46 -0
- kafka/protocol/schemas/resources/TxnOffsetCommitRequest.json +68 -0
- kafka/protocol/schemas/resources/TxnOffsetCommitResponse.json +47 -0
- kafka/protocol/schemas/resources/UpdateFeaturesRequest.json +43 -0
- kafka/protocol/schemas/resources/UpdateFeaturesResponse.json +39 -0
- kafka/protocol/schemas/resources/WriteTxnMarkersRequest.json +49 -0
- kafka/protocol/schemas/resources/WriteTxnMarkersResponse.json +45 -0
- kafka/protocol/schemas/resources/__init__.py +0 -0
- kafka/record/__init__.py +3 -0
- kafka/record/_crc32c.py +161 -0
- kafka/record/abc.py +144 -0
- kafka/record/default_records.py +782 -0
- kafka/record/legacy_records.py +587 -0
- kafka/record/memory_records.py +255 -0
- kafka/record/util.py +135 -0
- kafka/serializer/__init__.py +4 -0
- kafka/serializer/abstract.py +20 -0
- kafka/serializer/default.py +16 -0
- kafka/serializer/json.py +17 -0
- kafka/serializer/wrapper.py +21 -0
- kafka/structs.py +69 -0
- kafka/util.py +159 -0
- kafka/vendor/__init__.py +0 -0
- kafka/version.py +1 -0
- kafka_python-3.0.0.dist-info/METADATA +319 -0
- kafka_python-3.0.0.dist-info/RECORD +373 -0
- kafka_python-3.0.0.dist-info/WHEEL +5 -0
- kafka_python-3.0.0.dist-info/entry_points.txt +2 -0
- kafka_python-3.0.0.dist-info/licenses/LICENSE +202 -0
- kafka_python-3.0.0.dist-info/top_level.txt +1 -0
kafka/net/sasl/gssapi.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
|
|
3
|
+
# needed for SASL_GSSAPI authentication:
|
|
4
|
+
try:
|
|
5
|
+
import gssapi
|
|
6
|
+
from gssapi.raw.misc import GSSError
|
|
7
|
+
except (ImportError, OSError):
|
|
8
|
+
#no gssapi available, will disable gssapi mechanism
|
|
9
|
+
gssapi = None
|
|
10
|
+
GSSError = None
|
|
11
|
+
|
|
12
|
+
from .abc import SaslMechanism
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SaslMechanismGSSAPI(SaslMechanism):
|
|
16
|
+
# Establish security context and negotiate protection level
|
|
17
|
+
# For reference RFC 2222, section 7.2.1
|
|
18
|
+
|
|
19
|
+
SASL_QOP_AUTH = 1
|
|
20
|
+
SASL_QOP_AUTH_INT = 2
|
|
21
|
+
SASL_QOP_AUTH_CONF = 4
|
|
22
|
+
|
|
23
|
+
def __init__(self, **config):
|
|
24
|
+
if gssapi is None:
|
|
25
|
+
raise RuntimeError('GSSAPI lib not found!')
|
|
26
|
+
if 'sasl_kerberos_name' not in config and 'sasl_kerberos_service_name' not in config:
|
|
27
|
+
raise ValueError('sasl_kerberos_service_name or sasl_kerberos_name required for GSSAPI sasl configuration')
|
|
28
|
+
self._is_done = False
|
|
29
|
+
self._is_authenticated = False
|
|
30
|
+
self.gssapi_name = None
|
|
31
|
+
if config.get('sasl_kerberos_name', None) is not None:
|
|
32
|
+
self.auth_id = str(config['sasl_kerberos_name'])
|
|
33
|
+
if isinstance(config['sasl_kerberos_name'], gssapi.Name):
|
|
34
|
+
self.gssapi_name = config['sasl_kerberos_name']
|
|
35
|
+
else:
|
|
36
|
+
kerberos_domain_name = config.get('sasl_kerberos_domain_name', '') or config.get('host', '')
|
|
37
|
+
self.auth_id = config['sasl_kerberos_service_name'] + '@' + kerberos_domain_name
|
|
38
|
+
if self.gssapi_name is None:
|
|
39
|
+
self.gssapi_name = gssapi.Name(self.auth_id, name_type=gssapi.NameType.hostbased_service).canonicalize(gssapi.MechType.kerberos)
|
|
40
|
+
self._client_ctx = gssapi.SecurityContext(name=self.gssapi_name, usage='initiate')
|
|
41
|
+
self._next_token = self._client_ctx.step(None)
|
|
42
|
+
|
|
43
|
+
def auth_bytes(self):
|
|
44
|
+
# GSSAPI Auth does not have a final broker->client message
|
|
45
|
+
# so mark is_done after the final auth_bytes are provided
|
|
46
|
+
# in practice we'll still receive a response when using SaslAuthenticate
|
|
47
|
+
# but not when using the prior unframed approach.
|
|
48
|
+
if self._is_authenticated:
|
|
49
|
+
self._is_done = True
|
|
50
|
+
return self._next_token or b''
|
|
51
|
+
|
|
52
|
+
def receive(self, auth_bytes):
|
|
53
|
+
if not self._client_ctx.complete:
|
|
54
|
+
# The server will send a token back. Processing of this token either
|
|
55
|
+
# establishes a security context, or it needs further token exchange.
|
|
56
|
+
# The gssapi will be able to identify the needed next step.
|
|
57
|
+
self._next_token = self._client_ctx.step(auth_bytes)
|
|
58
|
+
elif self._is_done:
|
|
59
|
+
# The final step of gssapi is send, so we do not expect any additional bytes
|
|
60
|
+
# however, allow an empty message to support SaslAuthenticate response
|
|
61
|
+
if auth_bytes != b'':
|
|
62
|
+
raise ValueError("Unexpected receive auth_bytes after sasl/gssapi completion")
|
|
63
|
+
else:
|
|
64
|
+
# unwraps message containing supported protection levels and msg size
|
|
65
|
+
msg = self._client_ctx.unwrap(auth_bytes).message
|
|
66
|
+
# Kafka currently doesn't support integrity or confidentiality security layers, so we
|
|
67
|
+
# simply set QoP to 'auth' only (first octet). We reuse the max message size proposed
|
|
68
|
+
# by the server
|
|
69
|
+
client_flags = self.SASL_QOP_AUTH
|
|
70
|
+
server_flags = struct.Struct('>b').unpack(msg[0:1])[0]
|
|
71
|
+
message_parts = [
|
|
72
|
+
struct.Struct('>b').pack(client_flags & server_flags),
|
|
73
|
+
msg[1:], # always agree to max message size from server
|
|
74
|
+
self.auth_id.encode('utf-8'),
|
|
75
|
+
]
|
|
76
|
+
# add authorization identity to the response, and GSS-wrap
|
|
77
|
+
self._next_token = self._client_ctx.wrap(b''.join(message_parts), False).message
|
|
78
|
+
# We need to identify the last token in auth_bytes();
|
|
79
|
+
# we can't rely on client_ctx.complete because it becomes True after generating
|
|
80
|
+
# the second-to-last token (after calling .step(auth_bytes) for the final time)
|
|
81
|
+
# We could introduce an additional state variable (i.e., self._final_token),
|
|
82
|
+
# but instead we just set _is_authenticated. Since the plugin interface does
|
|
83
|
+
# not read is_authenticated() until after is_done() is True, this should be fine.
|
|
84
|
+
self._is_authenticated = True
|
|
85
|
+
|
|
86
|
+
def is_done(self):
|
|
87
|
+
return self._is_done
|
|
88
|
+
|
|
89
|
+
def is_authenticated(self):
|
|
90
|
+
return self._is_authenticated
|
|
91
|
+
|
|
92
|
+
def auth_details(self):
|
|
93
|
+
if not self.is_authenticated:
|
|
94
|
+
raise RuntimeError('Not authenticated yet!')
|
|
95
|
+
return 'Authenticated as %s to %s via SASL / GSSAPI' % (self._client_ctx.initiator_name, self._client_ctx.target_name)
|
kafka/net/sasl/msk.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import hashlib
|
|
3
|
+
import hmac
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import string
|
|
7
|
+
import urllib
|
|
8
|
+
|
|
9
|
+
# needed for AWS_MSK_IAM authentication:
|
|
10
|
+
try:
|
|
11
|
+
from botocore.session import Session as BotoSession
|
|
12
|
+
except ImportError:
|
|
13
|
+
# no botocore available, will disable AWS_MSK_IAM mechanism
|
|
14
|
+
BotoSession = None
|
|
15
|
+
|
|
16
|
+
from .abc import SaslMechanism
|
|
17
|
+
from kafka.errors import KafkaConfigurationError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SaslMechanismAwsMskIam(SaslMechanism):
|
|
24
|
+
def __init__(self, **config):
|
|
25
|
+
if BotoSession is None:
|
|
26
|
+
raise RuntimeError('AWS_MSK_IAM requires the "botocore" package')
|
|
27
|
+
if config.get('security_protocol', '') != 'SASL_SSL':
|
|
28
|
+
raise KafkaConfigurationError('AWS_MSK_IAM requires SASL_SSL')
|
|
29
|
+
if 'host' not in config:
|
|
30
|
+
raise KafkaConfigurationError('AWS_MSK_IAM requires host configuration')
|
|
31
|
+
self.host = config['host']
|
|
32
|
+
self._auth = None
|
|
33
|
+
self._is_done = False
|
|
34
|
+
self._is_authenticated = False
|
|
35
|
+
|
|
36
|
+
def _build_client(self):
|
|
37
|
+
session = BotoSession()
|
|
38
|
+
credentials = session.get_credentials().get_frozen_credentials()
|
|
39
|
+
if not session.get_config_variable('region'):
|
|
40
|
+
raise KafkaConfigurationError('Unable to determine region for AWS MSK cluster. Is AWS_DEFAULT_REGION set?')
|
|
41
|
+
return AwsMskIamClient(
|
|
42
|
+
host=self.host,
|
|
43
|
+
access_key=credentials.access_key,
|
|
44
|
+
secret_key=credentials.secret_key,
|
|
45
|
+
region=session.get_config_variable('region'),
|
|
46
|
+
token=credentials.token,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def auth_bytes(self):
|
|
50
|
+
client = self._build_client()
|
|
51
|
+
log.debug("Generating auth token for MSK scope: %s", client._scope)
|
|
52
|
+
return client.first_message()
|
|
53
|
+
|
|
54
|
+
def receive(self, auth_bytes):
|
|
55
|
+
self._is_done = True
|
|
56
|
+
self._is_authenticated = auth_bytes != b''
|
|
57
|
+
self._auth = auth_bytes.decode('utf-8')
|
|
58
|
+
|
|
59
|
+
def is_done(self):
|
|
60
|
+
return self._is_done
|
|
61
|
+
|
|
62
|
+
def is_authenticated(self):
|
|
63
|
+
return self._is_authenticated
|
|
64
|
+
|
|
65
|
+
def auth_details(self):
|
|
66
|
+
if not self.is_authenticated:
|
|
67
|
+
raise RuntimeError('Not authenticated yet!')
|
|
68
|
+
return 'Authenticated via SASL / AWS_MSK_IAM %s' % (self._auth,)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AwsMskIamClient:
|
|
72
|
+
UNRESERVED_CHARS = string.ascii_letters + string.digits + '-._~'
|
|
73
|
+
|
|
74
|
+
def __init__(self, host, access_key, secret_key, region, token=None):
|
|
75
|
+
"""
|
|
76
|
+
Arguments:
|
|
77
|
+
host (str): The hostname of the broker.
|
|
78
|
+
access_key (str): An AWS_ACCESS_KEY_ID.
|
|
79
|
+
secret_key (str): An AWS_SECRET_ACCESS_KEY.
|
|
80
|
+
region (str): An AWS_REGION.
|
|
81
|
+
token (Optional[str]): An AWS_SESSION_TOKEN if using temporary
|
|
82
|
+
credentials.
|
|
83
|
+
"""
|
|
84
|
+
self.algorithm = 'AWS4-HMAC-SHA256'
|
|
85
|
+
self.expires = '900'
|
|
86
|
+
self.hashfunc = hashlib.sha256
|
|
87
|
+
self.headers = [
|
|
88
|
+
('host', host)
|
|
89
|
+
]
|
|
90
|
+
self.version = '2020_10_22'
|
|
91
|
+
|
|
92
|
+
self.service = 'kafka-cluster'
|
|
93
|
+
self.action = '{}:Connect'.format(self.service)
|
|
94
|
+
|
|
95
|
+
now = datetime.datetime.utcnow()
|
|
96
|
+
self.datestamp = now.strftime('%Y%m%d')
|
|
97
|
+
self.timestamp = now.strftime('%Y%m%dT%H%M%SZ')
|
|
98
|
+
|
|
99
|
+
self.host = host
|
|
100
|
+
self.access_key = access_key
|
|
101
|
+
self.secret_key = secret_key
|
|
102
|
+
self.region = region
|
|
103
|
+
self.token = token
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def _credential(self):
|
|
107
|
+
return '{0.access_key}/{0._scope}'.format(self)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def _scope(self):
|
|
111
|
+
return '{0.datestamp}/{0.region}/{0.service}/aws4_request'.format(self)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def _signed_headers(self):
|
|
115
|
+
"""
|
|
116
|
+
Returns (str):
|
|
117
|
+
An alphabetically sorted, semicolon-delimited list of lowercase
|
|
118
|
+
request header names.
|
|
119
|
+
"""
|
|
120
|
+
return ';'.join(sorted(k.lower() for k, _ in self.headers))
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def _canonical_headers(self):
|
|
124
|
+
"""
|
|
125
|
+
Returns (str):
|
|
126
|
+
A newline-delited list of header names and values.
|
|
127
|
+
Header names are lowercased.
|
|
128
|
+
"""
|
|
129
|
+
return '\n'.join(map(':'.join, self.headers)) + '\n'
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def _canonical_request(self):
|
|
133
|
+
"""
|
|
134
|
+
Returns (str):
|
|
135
|
+
An AWS Signature Version 4 canonical request in the format:
|
|
136
|
+
<Method>\n
|
|
137
|
+
<Path>\n
|
|
138
|
+
<CanonicalQueryString>\n
|
|
139
|
+
<CanonicalHeaders>\n
|
|
140
|
+
<SignedHeaders>\n
|
|
141
|
+
<HashedPayload>
|
|
142
|
+
"""
|
|
143
|
+
# The hashed_payload is always an empty string for MSK.
|
|
144
|
+
hashed_payload = self.hashfunc(b'').hexdigest()
|
|
145
|
+
return '\n'.join((
|
|
146
|
+
'GET',
|
|
147
|
+
'/',
|
|
148
|
+
self._canonical_querystring,
|
|
149
|
+
self._canonical_headers,
|
|
150
|
+
self._signed_headers,
|
|
151
|
+
hashed_payload,
|
|
152
|
+
))
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def _canonical_querystring(self):
|
|
156
|
+
"""
|
|
157
|
+
Returns (str):
|
|
158
|
+
A '&'-separated list of URI-encoded key/value pairs.
|
|
159
|
+
"""
|
|
160
|
+
params = []
|
|
161
|
+
params.append(('Action', self.action))
|
|
162
|
+
params.append(('X-Amz-Algorithm', self.algorithm))
|
|
163
|
+
params.append(('X-Amz-Credential', self._credential))
|
|
164
|
+
params.append(('X-Amz-Date', self.timestamp))
|
|
165
|
+
params.append(('X-Amz-Expires', self.expires))
|
|
166
|
+
if self.token:
|
|
167
|
+
params.append(('X-Amz-Security-Token', self.token))
|
|
168
|
+
params.append(('X-Amz-SignedHeaders', self._signed_headers))
|
|
169
|
+
|
|
170
|
+
return '&'.join(self._uriencode(k) + '=' + self._uriencode(v) for k, v in params)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def _signing_key(self):
|
|
174
|
+
"""
|
|
175
|
+
Returns (bytes):
|
|
176
|
+
An AWS Signature V4 signing key generated from the secret_key, date,
|
|
177
|
+
region, service, and request type.
|
|
178
|
+
"""
|
|
179
|
+
key = self._hmac(('AWS4' + self.secret_key).encode('utf-8'), self.datestamp)
|
|
180
|
+
key = self._hmac(key, self.region)
|
|
181
|
+
key = self._hmac(key, self.service)
|
|
182
|
+
key = self._hmac(key, 'aws4_request')
|
|
183
|
+
return key
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def _signing_str(self):
|
|
187
|
+
"""
|
|
188
|
+
Returns (str):
|
|
189
|
+
A string used to sign the AWS Signature V4 payload in the format:
|
|
190
|
+
<Algorithm>\n
|
|
191
|
+
<Timestamp>\n
|
|
192
|
+
<Scope>\n
|
|
193
|
+
<CanonicalRequestHash>
|
|
194
|
+
"""
|
|
195
|
+
canonical_request_hash = self.hashfunc(self._canonical_request.encode('utf-8')).hexdigest()
|
|
196
|
+
return '\n'.join((self.algorithm, self.timestamp, self._scope, canonical_request_hash))
|
|
197
|
+
|
|
198
|
+
def _uriencode(self, msg):
|
|
199
|
+
"""
|
|
200
|
+
Arguments:
|
|
201
|
+
msg (str): A string to URI-encode.
|
|
202
|
+
|
|
203
|
+
Returns (str):
|
|
204
|
+
The URI-encoded version of the provided msg, following the encoding
|
|
205
|
+
rules specified: https://github.com/aws/aws-msk-iam-auth#uriencode
|
|
206
|
+
"""
|
|
207
|
+
return urllib.parse.quote(msg, safe=self.UNRESERVED_CHARS)
|
|
208
|
+
|
|
209
|
+
def _hmac(self, key, msg):
|
|
210
|
+
"""
|
|
211
|
+
Arguments:
|
|
212
|
+
key (bytes): A key to use for the HMAC digest.
|
|
213
|
+
msg (str): A value to include in the HMAC digest.
|
|
214
|
+
Returns (bytes):
|
|
215
|
+
An HMAC digest of the given key and msg.
|
|
216
|
+
"""
|
|
217
|
+
return hmac.new(key, msg.encode('utf-8'), digestmod=self.hashfunc).digest()
|
|
218
|
+
|
|
219
|
+
def first_message(self):
|
|
220
|
+
"""
|
|
221
|
+
Returns (bytes):
|
|
222
|
+
An encoded JSON authentication payload that can be sent to the
|
|
223
|
+
broker.
|
|
224
|
+
"""
|
|
225
|
+
signature = hmac.new(
|
|
226
|
+
self._signing_key,
|
|
227
|
+
self._signing_str.encode('utf-8'),
|
|
228
|
+
digestmod=self.hashfunc,
|
|
229
|
+
).hexdigest()
|
|
230
|
+
msg = {
|
|
231
|
+
'version': self.version,
|
|
232
|
+
'host': self.host,
|
|
233
|
+
'user-agent': 'kafka-python',
|
|
234
|
+
'action': self.action,
|
|
235
|
+
'x-amz-algorithm': self.algorithm,
|
|
236
|
+
'x-amz-credential': self._credential,
|
|
237
|
+
'x-amz-date': self.timestamp,
|
|
238
|
+
'x-amz-signedheaders': self._signed_headers,
|
|
239
|
+
'x-amz-expires': self.expires,
|
|
240
|
+
'x-amz-signature': signature,
|
|
241
|
+
}
|
|
242
|
+
if self.token:
|
|
243
|
+
msg['x-amz-security-token'] = self.token
|
|
244
|
+
|
|
245
|
+
return json.dumps(msg, separators=(',', ':')).encode('utf-8')
|
kafka/net/sasl/oauth.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from .abc import SaslMechanism
|
|
5
|
+
from kafka.errors import KafkaConfigurationError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SaslMechanismOAuth(SaslMechanism):
|
|
12
|
+
|
|
13
|
+
def __init__(self, **config):
|
|
14
|
+
if 'sasl_oauth_token_provider' not in config:
|
|
15
|
+
raise KafkaConfigurationError('sasl_oauth_token_provider required for OAUTHBEARER sasl')
|
|
16
|
+
if not isinstance(config['sasl_oauth_token_provider'], AbstractTokenProvider):
|
|
17
|
+
raise KafkaConfigurationError('sasl_oauth_token_provider must implement kafka.net.sasl.oauth.AbstractTokenProvider')
|
|
18
|
+
self.token_provider = config['sasl_oauth_token_provider']
|
|
19
|
+
self._error = None
|
|
20
|
+
self._is_done = False
|
|
21
|
+
self._is_authenticated = False
|
|
22
|
+
|
|
23
|
+
def auth_bytes(self):
|
|
24
|
+
if self._error:
|
|
25
|
+
# Server should respond to this with SaslAuthenticate failure, which ends the auth process
|
|
26
|
+
return self._error
|
|
27
|
+
token = self.token_provider.token()
|
|
28
|
+
extensions = self._token_extensions()
|
|
29
|
+
return "n,,\x01auth=Bearer {}{}\x01\x01".format(token, extensions).encode('utf-8')
|
|
30
|
+
|
|
31
|
+
def receive(self, auth_bytes):
|
|
32
|
+
if auth_bytes != b'':
|
|
33
|
+
error = auth_bytes.decode('utf-8')
|
|
34
|
+
log.debug("Sending x01 response to server after receiving SASL OAuth error: %s", error)
|
|
35
|
+
self._error = b'\x01'
|
|
36
|
+
else:
|
|
37
|
+
self._is_done = True
|
|
38
|
+
self._is_authenticated = True
|
|
39
|
+
|
|
40
|
+
def is_done(self):
|
|
41
|
+
return self._is_done
|
|
42
|
+
|
|
43
|
+
def is_authenticated(self):
|
|
44
|
+
return self._is_authenticated
|
|
45
|
+
|
|
46
|
+
def _token_extensions(self):
|
|
47
|
+
"""
|
|
48
|
+
Return a string representation of the OPTIONAL key-value pairs that can be sent with an OAUTHBEARER
|
|
49
|
+
initial request.
|
|
50
|
+
"""
|
|
51
|
+
# Builds up a string separated by \x01 via a dict of key value pairs
|
|
52
|
+
extensions = self.token_provider.extensions()
|
|
53
|
+
msg = '\x01'.join(['{}={}'.format(k, v) for k, v in extensions.items()])
|
|
54
|
+
return '\x01' + msg if msg else ''
|
|
55
|
+
|
|
56
|
+
def auth_details(self):
|
|
57
|
+
if not self.is_authenticated:
|
|
58
|
+
raise RuntimeError('Not authenticated yet!')
|
|
59
|
+
return 'Authenticated via SASL / OAuth'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AbstractTokenProvider(ABC):
|
|
63
|
+
"""
|
|
64
|
+
A Token Provider must be used for the SASL OAuthBearer protocol.
|
|
65
|
+
|
|
66
|
+
The implementation should ensure token reuse so that multiple
|
|
67
|
+
calls at connect time do not create multiple tokens. The implementation
|
|
68
|
+
should also periodically refresh the token in order to guarantee
|
|
69
|
+
that each call returns an unexpired token. A timeout error should
|
|
70
|
+
be returned after a short period of inactivity so that the
|
|
71
|
+
broker can log debugging info and retry.
|
|
72
|
+
|
|
73
|
+
Token Providers MUST implement the token() method
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, **config):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def token(self):
|
|
81
|
+
"""
|
|
82
|
+
Returns a (str) ID/Access Token to be sent to the Kafka
|
|
83
|
+
client.
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def extensions(self):
|
|
88
|
+
"""
|
|
89
|
+
This is an OPTIONAL method that may be implemented.
|
|
90
|
+
|
|
91
|
+
Returns a map of key-value pairs that can
|
|
92
|
+
be sent with the SASL/OAUTHBEARER initial client request. If
|
|
93
|
+
not implemented, the values are ignored. This feature is only available
|
|
94
|
+
in Kafka >= 2.1.0.
|
|
95
|
+
|
|
96
|
+
All returned keys and values should be type str
|
|
97
|
+
"""
|
|
98
|
+
return {}
|
kafka/net/sasl/plain.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .abc import SaslMechanism
|
|
4
|
+
from kafka.errors import KafkaConfigurationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
log = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SaslMechanismPlain(SaslMechanism):
|
|
11
|
+
|
|
12
|
+
def __init__(self, **config):
|
|
13
|
+
if config.get('security_protocol', '') == 'SASL_PLAINTEXT':
|
|
14
|
+
log.warning('Sending username and password in the clear')
|
|
15
|
+
if 'sasl_plain_username' not in config:
|
|
16
|
+
raise KafkaConfigurationError('sasl_plain_username required for PLAIN sasl')
|
|
17
|
+
if 'sasl_plain_password' not in config:
|
|
18
|
+
raise KafkaConfigurationError('sasl_plain_password required for PLAIN sasl')
|
|
19
|
+
|
|
20
|
+
self.username = config['sasl_plain_username']
|
|
21
|
+
self.password = config['sasl_plain_password']
|
|
22
|
+
self._is_done = False
|
|
23
|
+
self._is_authenticated = False
|
|
24
|
+
|
|
25
|
+
def auth_bytes(self):
|
|
26
|
+
# Send PLAIN credentials per RFC-4616
|
|
27
|
+
return bytes('\0'.join([self.username, self.username, self.password]).encode('utf-8'))
|
|
28
|
+
|
|
29
|
+
def receive(self, auth_bytes):
|
|
30
|
+
self._is_done = True
|
|
31
|
+
self._is_authenticated = auth_bytes == b''
|
|
32
|
+
|
|
33
|
+
def is_done(self):
|
|
34
|
+
return self._is_done
|
|
35
|
+
|
|
36
|
+
def is_authenticated(self):
|
|
37
|
+
return self._is_authenticated
|
|
38
|
+
|
|
39
|
+
def auth_details(self):
|
|
40
|
+
if not self.is_authenticated:
|
|
41
|
+
raise RuntimeError('Not authenticated yet!')
|
|
42
|
+
return 'Authenticated as %s via SASL / Plain' % self.username
|
kafka/net/sasl/scram.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import hmac
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from .abc import SaslMechanism
|
|
9
|
+
from kafka.errors import KafkaConfigurationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def xor_bytes(left, right):
|
|
16
|
+
return bytes(lb ^ rb for lb, rb in zip(left, right))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SaslMechanismScram(SaslMechanism):
|
|
20
|
+
def __init__(self, **config):
|
|
21
|
+
if not config.get('sasl_plain_username', ''):
|
|
22
|
+
KafkaConfigurationError('sasl_plain_username required for SCRAM sasl')
|
|
23
|
+
if not config.get('sasl_plain_password', ''):
|
|
24
|
+
KafkaConfigurationError('sasl_plain_password required for SCRAM sasl')
|
|
25
|
+
if config.get('sasl_mechanism', '') not in ScramClient.MECHANISMS:
|
|
26
|
+
KafkaConfigurationError('Unrecognized SCRAM mechanism')
|
|
27
|
+
if config.get('security_protocol', '') == 'SASL_PLAINTEXT':
|
|
28
|
+
log.warning('Exchanging credentials in the clear during Sasl Authentication')
|
|
29
|
+
|
|
30
|
+
self.username = config['sasl_plain_username']
|
|
31
|
+
self.mechanism = config['sasl_mechanism']
|
|
32
|
+
self._scram_client = ScramClient(
|
|
33
|
+
config['sasl_plain_username'],
|
|
34
|
+
config['sasl_plain_password'],
|
|
35
|
+
config['sasl_mechanism']
|
|
36
|
+
)
|
|
37
|
+
self._state = 0
|
|
38
|
+
|
|
39
|
+
def auth_bytes(self):
|
|
40
|
+
if self._state == 0:
|
|
41
|
+
return self._scram_client.first_message()
|
|
42
|
+
elif self._state == 1:
|
|
43
|
+
return self._scram_client.final_message()
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError('No auth_bytes for state: %s' % self._state)
|
|
46
|
+
|
|
47
|
+
def receive(self, auth_bytes):
|
|
48
|
+
if self._state == 0:
|
|
49
|
+
self._scram_client.process_server_first_message(auth_bytes)
|
|
50
|
+
elif self._state == 1:
|
|
51
|
+
self._scram_client.process_server_final_message(auth_bytes)
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError('Cannot receive bytes in state: %s' % self._state)
|
|
54
|
+
self._state += 1
|
|
55
|
+
return self.is_done()
|
|
56
|
+
|
|
57
|
+
def is_done(self):
|
|
58
|
+
return self._state == 2
|
|
59
|
+
|
|
60
|
+
def is_authenticated(self):
|
|
61
|
+
# receive raises if authentication fails...?
|
|
62
|
+
return self._state == 2
|
|
63
|
+
|
|
64
|
+
def auth_details(self):
|
|
65
|
+
if not self.is_authenticated:
|
|
66
|
+
raise RuntimeError('Not authenticated yet!')
|
|
67
|
+
return 'Authenticated as %s via SASL / %s' % (self.username, self.mechanism)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ScramClient:
|
|
71
|
+
MECHANISMS = {
|
|
72
|
+
'SCRAM-SHA-256': hashlib.sha256,
|
|
73
|
+
'SCRAM-SHA-512': hashlib.sha512
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def __init__(self, user, password, mechanism):
|
|
77
|
+
self.nonce = str(uuid.uuid4()).replace('-', '').encode('utf-8')
|
|
78
|
+
self.auth_message = b''
|
|
79
|
+
self.salted_password = None
|
|
80
|
+
self.user = user.encode('utf-8')
|
|
81
|
+
self.password = password.encode('utf-8')
|
|
82
|
+
self.hashfunc = self.MECHANISMS[mechanism]
|
|
83
|
+
self.hashname = ''.join(mechanism.lower().split('-')[1:3])
|
|
84
|
+
self.stored_key = None
|
|
85
|
+
self.client_key = None
|
|
86
|
+
self.client_signature = None
|
|
87
|
+
self.client_proof = None
|
|
88
|
+
self.server_key = None
|
|
89
|
+
self.server_signature = None
|
|
90
|
+
|
|
91
|
+
def first_message(self):
|
|
92
|
+
client_first_bare = b'n=' + self.user + b',r=' + self.nonce
|
|
93
|
+
self.auth_message += client_first_bare
|
|
94
|
+
return b'n,,' + client_first_bare
|
|
95
|
+
|
|
96
|
+
def process_server_first_message(self, server_first_message):
|
|
97
|
+
self.auth_message += b',' + server_first_message
|
|
98
|
+
params = dict(pair.split('=', 1) for pair in server_first_message.decode('utf-8').split(','))
|
|
99
|
+
server_nonce = params['r'].encode('utf-8')
|
|
100
|
+
if not server_nonce.startswith(self.nonce):
|
|
101
|
+
raise ValueError("Server nonce, did not start with client nonce!")
|
|
102
|
+
self.nonce = server_nonce
|
|
103
|
+
self.auth_message += b',c=biws,r=' + self.nonce
|
|
104
|
+
|
|
105
|
+
salt = base64.b64decode(params['s'].encode('utf-8'))
|
|
106
|
+
try:
|
|
107
|
+
iterations = int(params['i'])
|
|
108
|
+
if iterations > 1000000:
|
|
109
|
+
raise ValueError('too many iterations')
|
|
110
|
+
except (TypeError, ValueError):
|
|
111
|
+
raise ValueError('Invalid value (not integer or too large) for Iteration count in server-first-message')
|
|
112
|
+
self.create_salted_password(salt, iterations)
|
|
113
|
+
|
|
114
|
+
self.client_key = self.hmac(self.salted_password, b'Client Key')
|
|
115
|
+
self.stored_key = self.hashfunc(self.client_key).digest()
|
|
116
|
+
self.client_signature = self.hmac(self.stored_key, self.auth_message)
|
|
117
|
+
self.client_proof = xor_bytes(self.client_key, self.client_signature)
|
|
118
|
+
self.server_key = self.hmac(self.salted_password, b'Server Key')
|
|
119
|
+
self.server_signature = self.hmac(self.server_key, self.auth_message)
|
|
120
|
+
|
|
121
|
+
def hmac(self, key, msg):
|
|
122
|
+
return hmac.new(key, msg, digestmod=self.hashfunc).digest()
|
|
123
|
+
|
|
124
|
+
def create_salted_password(self, salt, iterations):
|
|
125
|
+
self.salted_password = hashlib.pbkdf2_hmac(
|
|
126
|
+
self.hashname, self.password, salt, iterations
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def final_message(self):
|
|
130
|
+
return b'c=biws,r=' + self.nonce + b',p=' + base64.b64encode(self.client_proof)
|
|
131
|
+
|
|
132
|
+
def process_server_final_message(self, server_final_message):
|
|
133
|
+
params = dict(pair.split('=', 1) for pair in server_final_message.decode('utf-8').split(','))
|
|
134
|
+
if self.server_signature != base64.b64decode(params['v'].encode('utf-8')):
|
|
135
|
+
raise ValueError("Server sent wrong signature!")
|