ruby-kafka 1.4.0 → 1.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd754956b907d18ac2a17b3d8f5bc374a8851dea21826277b8941f163403161d
4
- data.tar.gz: 7a1b74d4c3d3f8cfb0772bd6521a8a5c726bc1efc5ade83f814802ce08fb0f24
3
+ metadata.gz: '037679129d871686838235c368d870493439212f9aed0e897c1254fbfd5e4751'
4
+ data.tar.gz: bf61f193e97eb326b62a47b525a865911f1a7307983b6e765c08cc95e368a262
5
5
  SHA512:
6
- metadata.gz: 45038342acd388b7b797b64261addb3bcd90f43fb27d9dfe43a5071f5cb2a5ab6e25df1557fcadd528c5c40056384183cdd427d4fdac66de5cb93c67d246d45f
7
- data.tar.gz: af738b6a58f1cc7d1ee8c0fda72edad9fc94eb21c409f8b48a935a990b563736d30060ea209ef25d11f46ef2252f0d4adae726455c295637c2ef71522473c712
6
+ metadata.gz: 8d53ee98b08cda3c8b64dd44ccb27d16fe6f9c5b65986115ce1db8c6d4fae03543fb9f83dc5765f40e4455e23986c323593fc8b9fc89e14a5dae4ec912a83981
7
+ data.tar.gz: 48229b285251618f66f55963074285e01de96c71be4f65a2340a4c34e394db494aea76c3f8254075eb5670881a43a2ed51b2874d0c188c94d9bac9d7f27d38df
data/.circleci/config.yml CHANGED
@@ -347,27 +347,29 @@ jobs:
347
347
  - image: circleci/ruby:2.5.1-node
348
348
  environment:
349
349
  LOG_LEVEL: DEBUG
350
- - image: wurstmeister/zookeeper
351
- - image: wurstmeister/kafka:2.13-2.7.0
350
+ - image: bitnami/zookeeper
352
351
  environment:
353
- KAFKA_ADVERTISED_HOST_NAME: localhost
354
- KAFKA_ADVERTISED_PORT: 9092
355
- KAFKA_PORT: 9092
356
- KAFKA_ZOOKEEPER_CONNECT: localhost:2181
352
+ ALLOW_ANONYMOUS_LOGIN: yes
353
+ - image: bitnami/kafka:2.7.0
354
+ environment:
355
+ ALLOW_PLAINTEXT_LISTENER: yes
356
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
357
+ KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
358
+ KAFKA_CFG_ZOOKEEPER_CONNECT: localhost:2181
357
359
  KAFKA_DELETE_TOPIC_ENABLE: true
358
- - image: wurstmeister/kafka:2.13-2.7.0
360
+ - image: bitnami/kafka:2.7.0
359
361
  environment:
360
- KAFKA_ADVERTISED_HOST_NAME: localhost
361
- KAFKA_ADVERTISED_PORT: 9093
362
- KAFKA_PORT: 9093
363
- KAFKA_ZOOKEEPER_CONNECT: localhost:2181
362
+ ALLOW_PLAINTEXT_LISTENER: yes
363
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9093
364
+ KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9093
365
+ KAFKA_CFG_ZOOKEEPER_CONNECT: localhost:2181
364
366
  KAFKA_DELETE_TOPIC_ENABLE: true
365
- - image: wurstmeister/kafka:2.13-2.7.0
367
+ - image: bitnami/kafka:2.7.0
366
368
  environment:
367
- KAFKA_ADVERTISED_HOST_NAME: localhost
368
- KAFKA_ADVERTISED_PORT: 9094
369
- KAFKA_PORT: 9094
370
- KAFKA_ZOOKEEPER_CONNECT: localhost:2181
369
+ ALLOW_PLAINTEXT_LISTENER: yes
370
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9094
371
+ KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9094
372
+ KAFKA_CFG_ZOOKEEPER_CONNECT: localhost:2181
371
373
  KAFKA_DELETE_TOPIC_ENABLE: true
372
374
  steps:
373
375
  - checkout
data/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ Changes and additions to the library will be listed here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 1.5.0
8
+ - Add support for AWS IAM Authentication to an MSK cluster (#907).
9
+ - Added session token to the IAM mechanism; necessary for auth via temporary credentials (#937)
10
+
7
11
  ## 1.4.0
8
12
 
9
13
  - Refresh a stale cluster's metadata if necessary on `Kafka::Client#deliver_message` (#901).
data/README.md CHANGED
@@ -1121,6 +1121,20 @@ kafka = Kafka.new(
1121
1121
  )
1122
1122
  ```
1123
1123
 
1124
+ ##### AWS MSK (IAM)
1125
+ In order to authenticate using IAM w/ an AWS MSK cluster, set your access key, secret key, and region when initializing the Kafka client:
1126
+
1127
+ ```ruby
1128
+ k = Kafka.new(
1129
+ ["kafka1:9092"],
1130
+ sasl_aws_msk_iam_access_key_id: 'iam_access_key',
1131
+ sasl_aws_msk_iam_secret_key_id: 'iam_secret_key',
1132
+ sasl_aws_msk_iam_aws_region: 'us-west-2',
1133
+ ssl_ca_certs_from_system: true,
1134
+ # ...
1135
+ )
1136
+ ```
1137
+
1124
1138
  ##### PLAIN
1125
1139
  In order to authenticate using PLAIN, you must set your username and password when initializing the Kafka client:
1126
1140
 
data/lib/kafka/client.rb CHANGED
@@ -85,6 +85,10 @@ module Kafka
85
85
  ssl_client_cert_key_password: nil, ssl_client_cert_chain: nil, sasl_gssapi_principal: nil,
86
86
  sasl_gssapi_keytab: nil, sasl_plain_authzid: '', sasl_plain_username: nil, sasl_plain_password: nil,
87
87
  sasl_scram_username: nil, sasl_scram_password: nil, sasl_scram_mechanism: nil,
88
+ sasl_aws_msk_iam_access_key_id: nil,
89
+ sasl_aws_msk_iam_secret_key_id: nil,
90
+ sasl_aws_msk_iam_aws_region: nil,
91
+ sasl_aws_msk_iam_session_token: nil,
88
92
  sasl_over_ssl: true, ssl_ca_certs_from_system: false, partitioner: nil, sasl_oauth_token_provider: nil, ssl_verify_hostname: true,
89
93
  resolve_seed_brokers: false)
90
94
  @logger = TaggedLogger.new(logger)
@@ -112,6 +116,10 @@ module Kafka
112
116
  sasl_scram_username: sasl_scram_username,
113
117
  sasl_scram_password: sasl_scram_password,
114
118
  sasl_scram_mechanism: sasl_scram_mechanism,
119
+ sasl_aws_msk_iam_access_key_id: sasl_aws_msk_iam_access_key_id,
120
+ sasl_aws_msk_iam_secret_key_id: sasl_aws_msk_iam_secret_key_id,
121
+ sasl_aws_msk_iam_aws_region: sasl_aws_msk_iam_aws_region,
122
+ sasl_aws_msk_iam_session_token: sasl_aws_msk_iam_session_token,
115
123
  sasl_oauth_token_provider: sasl_oauth_token_provider,
116
124
  logger: @logger
117
125
  )
@@ -8,7 +8,7 @@ module Kafka
8
8
 
9
9
  class SaslHandshakeRequest
10
10
 
11
- SUPPORTED_MECHANISMS = %w(GSSAPI PLAIN SCRAM-SHA-256 SCRAM-SHA-512 OAUTHBEARER)
11
+ SUPPORTED_MECHANISMS = %w(AWS_MSK_IAM GSSAPI PLAIN SCRAM-SHA-256 SCRAM-SHA-512 OAUTHBEARER)
12
12
 
13
13
  def initialize(mechanism)
14
14
  unless SUPPORTED_MECHANISMS.include?(mechanism)
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'base64'
5
+ require 'json'
6
+
7
+ module Kafka
8
+ module Sasl
9
+ class AwsMskIam
10
+ AWS_MSK_IAM = "AWS_MSK_IAM"
11
+
12
+ def initialize(aws_region:, access_key_id:, secret_key_id:, session_token: nil, logger:)
13
+ @semaphore = Mutex.new
14
+
15
+ @aws_region = aws_region
16
+ @access_key_id = access_key_id
17
+ @secret_key_id = secret_key_id
18
+ @session_token = session_token
19
+ @logger = TaggedLogger.new(logger)
20
+ end
21
+
22
+ def ident
23
+ AWS_MSK_IAM
24
+ end
25
+
26
+ def configured?
27
+ @aws_region && @access_key_id && @secret_key_id
28
+ end
29
+
30
+ def authenticate!(host, encoder, decoder)
31
+ @logger.debug "Authenticating #{@access_key_id} with SASL #{AWS_MSK_IAM}"
32
+
33
+ host_without_port = host.split(':', -1).first
34
+
35
+ time_now = Time.now.utc
36
+
37
+ msg = authentication_payload(host: host_without_port, time_now: time_now)
38
+ @logger.debug "Sending first client SASL AWS_MSK_IAM message:"
39
+ @logger.debug msg
40
+ encoder.write_bytes(msg)
41
+
42
+ begin
43
+ @server_first_message = decoder.bytes
44
+ @logger.debug "Received first server SASL AWS_MSK_IAM message: #{@server_first_message}"
45
+
46
+ raise Kafka::Error, "SASL AWS_MSK_IAM authentication failed: unknown error" unless @server_first_message
47
+ rescue Errno::ETIMEDOUT, EOFError => e
48
+ raise Kafka::Error, "SASL AWS_MSK_IAM authentication failed: #{e.message}"
49
+ end
50
+
51
+ @logger.debug "SASL #{AWS_MSK_IAM} authentication successful"
52
+ end
53
+
54
+ private
55
+
56
+ def bin_to_hex(s)
57
+ s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
58
+ end
59
+
60
+ def digest
61
+ @digest ||= OpenSSL::Digest::SHA256.new
62
+ end
63
+
64
+ def authentication_payload(host:, time_now:)
65
+ {
66
+ 'version' => "2020_10_22",
67
+ 'host' => host,
68
+ 'user-agent' => "ruby-kafka",
69
+ 'action' => "kafka-cluster:Connect",
70
+ 'x-amz-algorithm' => "AWS4-HMAC-SHA256",
71
+ 'x-amz-credential' => @access_key_id + "/" + time_now.strftime("%Y%m%d") + "/" + @aws_region + "/kafka-cluster/aws4_request",
72
+ 'x-amz-date' => time_now.strftime("%Y%m%dT%H%M%SZ"),
73
+ 'x-amz-signedheaders' => "host",
74
+ 'x-amz-expires' => "900",
75
+ 'x-amz-security-token' => @session_token,
76
+ 'x-amz-signature' => signature(host: host, time_now: time_now)
77
+ }.delete_if { |_, v| v.nil? }.to_json
78
+ end
79
+
80
+ def canonical_request(host:, time_now:)
81
+ "GET\n" +
82
+ "/\n" +
83
+ canonical_query_string(time_now: time_now) + "\n" +
84
+ canonical_headers(host: host) + "\n" +
85
+ signed_headers + "\n" +
86
+ hashed_payload
87
+ end
88
+
89
+ def canonical_query_string(time_now:)
90
+ params = {
91
+ "Action" => "kafka-cluster:Connect",
92
+ "X-Amz-Algorithm" => "AWS4-HMAC-SHA256",
93
+ "X-Amz-Credential" => @access_key_id + "/" + time_now.strftime("%Y%m%d") + "/" + @aws_region + "/kafka-cluster/aws4_request",
94
+ "X-Amz-Date" => time_now.strftime("%Y%m%dT%H%M%SZ"),
95
+ "X-Amz-Expires" => "900",
96
+ "X-Amz-Security-Token" => @session_token,
97
+ "X-Amz-SignedHeaders" => "host"
98
+ }.delete_if { |_, v| v.nil? }
99
+
100
+ URI.encode_www_form(params)
101
+ end
102
+
103
+ def canonical_headers(host:)
104
+ "host" + ":" + host + "\n"
105
+ end
106
+
107
+ def signed_headers
108
+ "host"
109
+ end
110
+
111
+ def hashed_payload
112
+ bin_to_hex(digest.digest(""))
113
+ end
114
+
115
+ def string_to_sign(host:, time_now:)
116
+ "AWS4-HMAC-SHA256" + "\n" +
117
+ time_now.strftime("%Y%m%dT%H%M%SZ") + "\n" +
118
+ time_now.strftime("%Y%m%d") + "/" + @aws_region + "/kafka-cluster/aws4_request" + "\n" +
119
+ bin_to_hex(digest.digest(canonical_request(host: host, time_now: time_now)))
120
+ end
121
+
122
+ def signature(host:, time_now:)
123
+ date_key = OpenSSL::HMAC.digest("SHA256", "AWS4" + @secret_key_id, time_now.strftime("%Y%m%d"))
124
+ date_region_key = OpenSSL::HMAC.digest("SHA256", date_key, @aws_region)
125
+ date_region_service_key = OpenSSL::HMAC.digest("SHA256", date_region_key, "kafka-cluster")
126
+ signing_key = OpenSSL::HMAC.digest("SHA256", date_region_service_key, "aws4_request")
127
+ signature = bin_to_hex(OpenSSL::HMAC.digest("SHA256", signing_key, string_to_sign(host: host, time_now: time_now)))
128
+
129
+ signature
130
+ end
131
+ end
132
+ end
133
+ end
@@ -4,13 +4,18 @@ require 'kafka/sasl/plain'
4
4
  require 'kafka/sasl/gssapi'
5
5
  require 'kafka/sasl/scram'
6
6
  require 'kafka/sasl/oauth'
7
+ require 'kafka/sasl/awsmskiam'
7
8
 
8
9
  module Kafka
9
10
  class SaslAuthenticator
10
11
  def initialize(logger:, sasl_gssapi_principal:, sasl_gssapi_keytab:,
11
12
  sasl_plain_authzid:, sasl_plain_username:, sasl_plain_password:,
12
13
  sasl_scram_username:, sasl_scram_password:, sasl_scram_mechanism:,
13
- sasl_oauth_token_provider:)
14
+ sasl_oauth_token_provider:,
15
+ sasl_aws_msk_iam_access_key_id:,
16
+ sasl_aws_msk_iam_secret_key_id:,
17
+ sasl_aws_msk_iam_aws_region:,
18
+ sasl_aws_msk_iam_session_token: nil)
14
19
  @logger = TaggedLogger.new(logger)
15
20
 
16
21
  @plain = Sasl::Plain.new(
@@ -33,12 +38,20 @@ module Kafka
33
38
  logger: @logger,
34
39
  )
35
40
 
41
+ @aws_msk_iam = Sasl::AwsMskIam.new(
42
+ access_key_id: sasl_aws_msk_iam_access_key_id,
43
+ secret_key_id: sasl_aws_msk_iam_secret_key_id,
44
+ aws_region: sasl_aws_msk_iam_aws_region,
45
+ session_token: sasl_aws_msk_iam_session_token,
46
+ logger: @logger,
47
+ )
48
+
36
49
  @oauth = Sasl::OAuth.new(
37
50
  token_provider: sasl_oauth_token_provider,
38
51
  logger: @logger,
39
52
  )
40
53
 
41
- @mechanism = [@gssapi, @plain, @scram, @oauth].find(&:configured?)
54
+ @mechanism = [@gssapi, @plain, @scram, @oauth, @aws_msk_iam].find(&:configured?)
42
55
  end
43
56
 
44
57
  def enabled?
data/lib/kafka/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kafka
4
- VERSION = "1.4.0"
4
+ VERSION = "1.5.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-kafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Schierbeck
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-25 00:00:00.000000000 Z
11
+ date: 2022-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: digest-crc
@@ -476,6 +476,7 @@ files:
476
476
  - lib/kafka/protocol/txn_offset_commit_request.rb
477
477
  - lib/kafka/protocol/txn_offset_commit_response.rb
478
478
  - lib/kafka/round_robin_assignment_strategy.rb
479
+ - lib/kafka/sasl/awsmskiam.rb
479
480
  - lib/kafka/sasl/gssapi.rb
480
481
  - lib/kafka/sasl/oauth.rb
481
482
  - lib/kafka/sasl/plain.rb