-
Notifications
You must be signed in to change notification settings - Fork 190
fix(kafka): support SASL_PLAINTEXT in watermill publisher #4633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -80,32 +80,39 @@ func (o *BrokerOptions) createKafkaConfig(role string) (*sarama.Config, error) { | |
| loggerFunc: logger.Debug, | ||
| } | ||
|
|
||
| if o.KafkaConfig.SecurityProtocol == "SASL_SSL" { | ||
| // Both SASL_SSL and SASL_PLAINTEXT authenticate via SASL. They differ only in | ||
| // whether the connection is wrapped in TLS: SASL_SSL uses TLS, SASL_PLAINTEXT | ||
| // sends the SASL exchange (including credentials) over an unencrypted socket. | ||
| // PLAINTEXT and SSL (no SASL) are left untouched and need no SASL/credentials setup. | ||
| switch o.KafkaConfig.SecurityProtocol { | ||
| case "SASL_SSL", "SASL_PLAINTEXT": | ||
|
Comment on lines
+87
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Prompt To Fix With AIThis is a comment left during a code review.
Path: openmeter/watermill/driver/kafka/broker.go
Line: 87-88
Comment:
**SASL Config Allows Empty Mechanism**
When `securityProtocol` is `SASL_PLAINTEXT` but `saslMechanisms` or credentials are left at their default empty values, this new branch enables Sarama SASL with empty auth fields. That config passes the existing validation and only fails later during broker connection, so a bad SASL_PLAINTEXT config reports as a runtime Kafka auth/connection failure instead of a clear startup validation error.
How can I resolve this? If you propose a fix, please make it concise. |
||
| config.Net.SASL.Enable = true | ||
| config.Net.SASL.Handshake = true | ||
|
|
||
| config.Net.TLS.Enable = true | ||
| // Sarama has issues with min version TLS 1.3, so let's use the defaults for now | ||
| // remote error: tls: protocol version not supported | ||
| config.Net.TLS.Config = &tls.Config{} // nosemgrep | ||
|
|
||
| config.Net.SASL.User = o.KafkaConfig.SaslUsername | ||
| config.Net.SASL.Password = o.KafkaConfig.SaslPassword | ||
| // We rely on sarama to validate the SASL mechanism | ||
| config.Net.SASL.Mechanism = sarama.SASLMechanism(o.KafkaConfig.SaslMechanisms) | ||
|
|
||
| switch o.KafkaConfig.SaslMechanisms { | ||
| case sarama.SASLTypeSCRAMSHA256: | ||
| config.Net.SASL.Mechanism = sarama.SASLMechanism(o.KafkaConfig.SaslMechanisms) | ||
| config.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { | ||
| return &XDGSCRAMClient{HashGeneratorFcn: SHA256} | ||
| } | ||
| case sarama.SASLTypeSCRAMSHA512: | ||
| config.Net.SASL.Mechanism = sarama.SASLMechanism(o.KafkaConfig.SaslMechanisms) | ||
| config.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { | ||
| return &XDGSCRAMClient{HashGeneratorFcn: SHA512} | ||
| } | ||
| } | ||
|
|
||
| // Only SASL_SSL wraps the SASL exchange in TLS. SASL_PLAINTEXT authenticates | ||
| // over an unencrypted socket, so we must NOT enable TLS for it. | ||
| if o.KafkaConfig.SecurityProtocol == "SASL_SSL" { | ||
| config.Net.TLS.Enable = true | ||
| // Sarama has issues with min version TLS 1.3, so let's use the defaults for now | ||
| // remote error: tls: protocol version not supported | ||
| config.Net.TLS.Config = &tls.Config{} // nosemgrep | ||
| } | ||
| } | ||
|
|
||
| config.Producer.Retry.Max = 10 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| package kafka | ||
|
|
||
| import ( | ||
| "log/slog" | ||
| "testing" | ||
|
|
||
| "github.com/IBM/sarama" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| "go.opentelemetry.io/otel/metric/noop" | ||
|
|
||
| "github.com/openmeterio/openmeter/app/config" | ||
| ) | ||
|
|
||
| func TestBrokerOptions_createKafkaConfig_SASL(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| kafkaConfig config.KafkaConfiguration | ||
| expectSASLEnabled bool | ||
| expectTLSEnabled bool | ||
| expectSCRAMGenerator bool | ||
| }{ | ||
| { | ||
| name: "PLAINTEXT does not enable SASL or TLS", | ||
| kafkaConfig: config.KafkaConfiguration{ | ||
| Broker: "localhost:9092", | ||
| }, | ||
| expectSASLEnabled: false, | ||
| expectTLSEnabled: false, | ||
| }, | ||
| { | ||
| name: "SASL_PLAINTEXT enables SASL without TLS", | ||
| kafkaConfig: config.KafkaConfiguration{ | ||
| Broker: "localhost:9092", | ||
| SecurityProtocol: "SASL_PLAINTEXT", | ||
| SaslMechanisms: sarama.SASLTypePlaintext, | ||
| SaslUsername: "user", | ||
| SaslPassword: "pass", | ||
| }, | ||
| expectSASLEnabled: true, | ||
| expectTLSEnabled: false, | ||
| }, | ||
| { | ||
| name: "SASL_SSL enables both SASL and TLS", | ||
| kafkaConfig: config.KafkaConfiguration{ | ||
| Broker: "localhost:9092", | ||
| SecurityProtocol: "SASL_SSL", | ||
| SaslMechanisms: sarama.SASLTypePlaintext, | ||
| SaslUsername: "user", | ||
| SaslPassword: "pass", | ||
| }, | ||
| expectSASLEnabled: true, | ||
| expectTLSEnabled: true, | ||
| }, | ||
| { | ||
| name: "SASL_PLAINTEXT with SCRAM-SHA-512 wires up a SCRAM client generator", | ||
| kafkaConfig: config.KafkaConfiguration{ | ||
| Broker: "localhost:9092", | ||
| SecurityProtocol: "SASL_PLAINTEXT", | ||
| SaslMechanisms: sarama.SASLTypeSCRAMSHA512, | ||
| SaslUsername: "user", | ||
| SaslPassword: "pass", | ||
| }, | ||
| expectSASLEnabled: true, | ||
| expectTLSEnabled: false, | ||
| expectSCRAMGenerator: true, | ||
| }, | ||
| { | ||
| name: "SASL_PLAINTEXT with SCRAM-SHA-256 wires up a SCRAM client generator", | ||
| kafkaConfig: config.KafkaConfiguration{ | ||
| Broker: "localhost:9092", | ||
| SecurityProtocol: "SASL_PLAINTEXT", | ||
| SaslMechanisms: sarama.SASLTypeSCRAMSHA256, | ||
| SaslUsername: "user", | ||
| SaslPassword: "pass", | ||
| }, | ||
| expectSASLEnabled: true, | ||
| expectTLSEnabled: false, | ||
| expectSCRAMGenerator: true, | ||
| }, | ||
| { | ||
| name: "SASL_SSL with SCRAM wires up a SCRAM client generator and enables TLS", | ||
| kafkaConfig: config.KafkaConfiguration{ | ||
| Broker: "localhost:9092", | ||
| SecurityProtocol: "SASL_SSL", | ||
| SaslMechanisms: sarama.SASLTypeSCRAMSHA512, | ||
| SaslUsername: "user", | ||
| SaslPassword: "pass", | ||
| }, | ||
| expectSASLEnabled: true, | ||
| expectTLSEnabled: true, | ||
| expectSCRAMGenerator: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| o := &BrokerOptions{ | ||
| KafkaConfig: tt.kafkaConfig, | ||
| ClientID: "test-client", | ||
| Logger: slog.Default(), | ||
| MetricMeter: noop.NewMeterProvider().Meter("test"), | ||
| } | ||
|
|
||
| cfg, err := o.createKafkaConfig("publisher") | ||
| require.NoError(t, err) | ||
|
|
||
| assert.Equal(t, tt.expectSASLEnabled, cfg.Net.SASL.Enable) | ||
| assert.Equal(t, tt.expectTLSEnabled, cfg.Net.TLS.Enable) | ||
|
|
||
| if tt.expectSASLEnabled { | ||
| assert.Equal(t, tt.kafkaConfig.SaslUsername, cfg.Net.SASL.User) | ||
| assert.Equal(t, tt.kafkaConfig.SaslPassword, cfg.Net.SASL.Password) | ||
| assert.Equal(t, sarama.SASLMechanism(tt.kafkaConfig.SaslMechanisms), cfg.Net.SASL.Mechanism) | ||
| } | ||
|
|
||
| if tt.expectSCRAMGenerator { | ||
| assert.NotNil(t, cfg.Net.SASL.SCRAMClientGeneratorFunc) | ||
| } else { | ||
| assert.Nil(t, cfg.Net.SASL.SCRAMClientGeneratorFunc) | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check only runs for the ingest
KafkaConfiguration, but the sink Kafka config uses the same SASL fields through the shared Kafka config type and still does not reject empty mechanism, username, or password. When the sink is configured withSASL_SSLorSASL_PLAINTEXTand the SASL fields are left empty, startup validation can still pass and Kafka authentication fails later with the same opaque broker connection error. Please enforce this SASL-field invariant in the shared Kafka validation path used by the sink as well.Prompt To Fix With AI