1use std::collections::BTreeMap;
16use std::hash::Hash;
17use std::io::Write;
18use std::path::Path;
19use std::sync::{Arc, LazyLock, Weak};
20use std::time::Duration;
21
22use anyhow::{Context, anyhow};
23use async_nats::jetstream::consumer::DeliverPolicy;
24use async_nats::jetstream::{self};
25use aws_sdk_kinesis::Client as KinesisClient;
26use aws_sdk_kinesis::config::{AsyncSleep, SharedAsyncSleep, Sleep};
27use moka::future::Cache as MokaCache;
28use moka::ops::compute::Op;
29use phf::{Set, phf_set};
30use pulsar::authentication::oauth2::{OAuth2Authentication, OAuth2Params};
31use pulsar::{Authentication, OperationRetryOptions, Pulsar, TokioExecutor};
32use rdkafka::ClientConfig;
33use risingwave_common::bail;
34use rustls_pki_types::pem::PemObject;
35use rustls_pki_types::{CertificateDer, PrivatePkcs8KeyDer};
36use serde::Deserialize;
37use serde_with::json::JsonString;
38use serde_with::{DisplayFromStr, serde_as};
39use tempfile::NamedTempFile;
40use time::OffsetDateTime;
41use url::Url;
42use with_options::WithOptions;
43
44use crate::aws_utils::load_file_descriptor_from_s3;
45use crate::deserialize_duration_from_string;
46use crate::enforce_secret::EnforceSecret;
47use crate::error::ConnectorResult;
48use crate::sink::SinkError;
49use crate::source::nats::source::NatsOffset;
50
51pub const PRIVATE_LINK_BROKER_REWRITE_MAP_KEY: &str = "broker.rewrite.endpoints";
52pub const PRIVATE_LINK_TARGETS_KEY: &str = "privatelink.targets";
53
54const AWS_MSK_IAM_AUTH: &str = "AWS_MSK_IAM";
55
56pub const DISABLE_DEFAULT_CREDENTIAL: &str = "DISABLE_DEFAULT_CREDENTIAL";
59
60#[derive(Debug, Clone, Deserialize)]
61pub struct AwsPrivateLinkItem {
62 pub az_id: Option<String>,
63 pub port: u16,
64}
65
66use aws_config::default_provider::region::DefaultRegionChain;
67use aws_config::sts::AssumeRoleProvider;
68use aws_credential_types::provider::SharedCredentialsProvider;
69use aws_types::SdkConfig;
70use aws_types::region::Region;
71use risingwave_common::util::env_var::env_var_is_true;
72
73#[derive(Deserialize, Debug, Clone, WithOptions, PartialEq)]
75pub struct AwsAuthProps {
76 #[serde(rename = "aws.region", alias = "region", alias = "s3.region")]
77 pub region: Option<String>,
78
79 #[serde(
80 rename = "aws.endpoint_url",
81 alias = "endpoint_url",
82 alias = "endpoint",
83 alias = "s3.endpoint"
84 )]
85 pub endpoint: Option<String>,
86 #[serde(
87 rename = "aws.credentials.access_key_id",
88 alias = "access_key",
89 alias = "s3.access.key"
90 )]
91 pub access_key: Option<String>,
92 #[serde(
93 rename = "aws.credentials.secret_access_key",
94 alias = "secret_key",
95 alias = "s3.secret.key"
96 )]
97 pub secret_key: Option<String>,
98 #[serde(rename = "aws.credentials.session_token", alias = "session_token")]
99 pub session_token: Option<String>,
100 #[serde(rename = "aws.credentials.role.arn", alias = "arn")]
102 pub arn: Option<String>,
103 #[serde(rename = "aws.credentials.role.external_id", alias = "external_id")]
105 pub external_id: Option<String>,
106 #[serde(rename = "aws.profile", alias = "profile")]
107 pub profile: Option<String>,
108 #[serde(rename = "aws.msk.signer_timeout_sec")]
109 pub msk_signer_timeout_sec: Option<u64>,
110}
111
112impl EnforceSecret for AwsAuthProps {
113 const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
114 "access_key",
115 "aws.credentials.access_key_id",
116 "s3.access.key",
117 "secret_key",
118 "aws.credentials.secret_access_key",
119 "s3.secret.key",
120 "session_token",
121 "aws.credentials.session_token",
122 };
123}
124
125impl AwsAuthProps {
126 async fn build_region(&self) -> ConnectorResult<Region> {
127 if let Some(region_name) = &self.region {
128 Ok(Region::new(region_name.clone()))
129 } else {
130 let mut region_chain = DefaultRegionChain::builder();
131 if let Some(profile_name) = &self.profile {
132 region_chain = region_chain.profile_name(profile_name);
133 }
134
135 Ok(region_chain
136 .build()
137 .region()
138 .await
139 .context("region must be provided")?)
140 }
141 }
142
143 async fn build_credential_provider(&self) -> ConnectorResult<SharedCredentialsProvider> {
144 if let (Some(access_key), Some(secret_key)) =
145 (self.access_key.as_ref(), self.secret_key.as_ref())
146 {
147 Ok(SharedCredentialsProvider::new(
148 aws_credential_types::Credentials::from_keys(
149 access_key,
150 secret_key,
151 self.session_token.clone(),
152 ),
153 ))
154 } else if !env_var_is_true(DISABLE_DEFAULT_CREDENTIAL) {
155 Ok(SharedCredentialsProvider::new(
156 aws_config::default_provider::credentials::default_provider().await,
157 ))
158 } else {
159 bail!("Both `access_key` and `secret_key` must be provided")
160 }
161 }
162
163 async fn with_role_provider(
164 &self,
165 credential: SharedCredentialsProvider,
166 ) -> ConnectorResult<SharedCredentialsProvider> {
167 if let Some(role_name) = &self.arn {
168 let region = self.build_region().await?;
169 let mut role = AssumeRoleProvider::builder(role_name)
170 .session_name("RisingWave")
171 .region(region);
172 if let Some(id) = &self.external_id {
173 role = role.external_id(id);
174 }
175 let provider = role.build_from_provider(credential).await;
176 Ok(SharedCredentialsProvider::new(provider))
177 } else {
178 Ok(credential)
179 }
180 }
181
182 pub async fn build_config(&self) -> ConnectorResult<SdkConfig> {
183 let region = self.build_region().await?;
184 let credentials_provider = self
185 .with_role_provider(self.build_credential_provider().await?)
186 .await?;
187 let mut config_loader = aws_config::from_env()
188 .region(region)
189 .credentials_provider(credentials_provider);
190
191 if let Some(endpoint) = self.endpoint.as_ref() {
192 config_loader = config_loader.endpoint_url(endpoint);
193 }
194
195 Ok(config_loader.load().await)
196 }
197}
198
199#[serde_as]
200#[derive(Debug, Clone, Deserialize, WithOptions, PartialEq, Hash, Eq)]
201pub struct KafkaConnectionProps {
202 #[serde(rename = "properties.bootstrap.server", alias = "kafka.brokers")]
203 pub brokers: String,
204
205 #[serde(rename = "properties.security.protocol")]
208 #[with_option(allow_alter_on_fly)]
209 security_protocol: Option<String>,
210
211 #[serde(rename = "properties.ssl.endpoint.identification.algorithm")]
212 #[with_option(allow_alter_on_fly)]
213 ssl_endpoint_identification_algorithm: Option<String>,
214
215 #[serde(rename = "properties.ssl.ca.location")]
218 #[with_option(allow_alter_on_fly)]
219 ssl_ca_location: Option<String>,
220
221 #[serde(rename = "properties.ssl.ca.pem")]
223 #[with_option(allow_alter_on_fly)]
224 ssl_ca_pem: Option<String>,
225
226 #[serde(rename = "properties.ssl.certificate.location")]
228 #[with_option(allow_alter_on_fly)]
229 ssl_certificate_location: Option<String>,
230
231 #[serde(rename = "properties.ssl.certificate.pem")]
233 #[with_option(allow_alter_on_fly)]
234 ssl_certificate_pem: Option<String>,
235
236 #[serde(rename = "properties.ssl.key.location")]
238 #[with_option(allow_alter_on_fly)]
239 ssl_key_location: Option<String>,
240
241 #[serde(rename = "properties.ssl.key.pem")]
243 #[with_option(allow_alter_on_fly)]
244 ssl_key_pem: Option<String>,
245
246 #[serde(rename = "properties.ssl.key.password")]
248 #[with_option(allow_alter_on_fly)]
249 ssl_key_password: Option<String>,
250
251 #[serde(rename = "properties.sasl.mechanism")]
253 #[with_option(allow_alter_on_fly)]
254 sasl_mechanism: Option<String>,
255
256 #[serde(rename = "properties.sasl.username")]
258 #[with_option(allow_alter_on_fly)]
259 sasl_username: Option<String>,
260
261 #[serde(rename = "properties.sasl.password")]
263 #[with_option(allow_alter_on_fly)]
264 sasl_password: Option<String>,
265
266 #[serde(rename = "properties.sasl.kerberos.service.name")]
268 #[with_option(allow_alter_on_fly)]
269 sasl_kerberos_service_name: Option<String>,
270
271 #[serde(rename = "properties.sasl.kerberos.keytab")]
273 #[with_option(allow_alter_on_fly)]
274 sasl_kerberos_keytab: Option<String>,
275
276 #[serde(rename = "properties.sasl.kerberos.principal")]
278 #[with_option(allow_alter_on_fly)]
279 sasl_kerberos_principal: Option<String>,
280
281 #[serde(rename = "properties.sasl.kerberos.kinit.cmd")]
283 #[with_option(allow_alter_on_fly)]
284 sasl_kerberos_kinit_cmd: Option<String>,
285
286 #[serde(rename = "properties.sasl.kerberos.min.time.before.relogin")]
288 #[with_option(allow_alter_on_fly)]
289 sasl_kerberos_min_time_before_relogin: Option<String>,
290
291 #[serde(rename = "properties.sasl.oauthbearer.config")]
293 #[with_option(allow_alter_on_fly)]
294 sasl_oathbearer_config: Option<String>,
295
296 #[serde(rename = "properties.sasl.oauthbearer.method")]
298 #[with_option(allow_alter_on_fly)]
299 sasl_oauthbearer_method: Option<String>,
300
301 #[serde(rename = "properties.sasl.oauthbearer.client.id")]
303 #[with_option(allow_alter_on_fly)]
304 sasl_oauthbearer_client_id: Option<String>,
305
306 #[serde(rename = "properties.sasl.oauthbearer.client.secret")]
308 #[with_option(allow_alter_on_fly)]
309 sasl_oauthbearer_client_secret: Option<String>,
310
311 #[serde(rename = "properties.sasl.oauthbearer.token.endpoint.url")]
313 #[with_option(allow_alter_on_fly)]
314 sasl_oauthbearer_token_endpoint_url: Option<String>,
315
316 #[serde(rename = "properties.sasl.oauthbearer.scope")]
318 #[with_option(allow_alter_on_fly)]
319 sasl_oauthbearer_scope: Option<String>,
320
321 #[serde(rename = "properties.sasl.oauthbearer.extensions")]
323 #[with_option(allow_alter_on_fly)]
324 sasl_oauthbearer_extensions: Option<String>,
325}
326
327impl EnforceSecret for KafkaConnectionProps {
328 const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
329 "properties.ssl.key.pem",
330 "properties.ssl.key.password",
331 "properties.sasl.password",
332 "properties.sasl.oauthbearer.client.secret",
333 };
334}
335
336#[serde_as]
337#[derive(Debug, Clone, Deserialize, WithOptions)]
338pub struct KafkaCommon {
339 #[serde(rename = "topic", alias = "kafka.topic")]
341 pub topic: String,
342
343 #[serde(
344 rename = "properties.sync.call.timeout",
345 deserialize_with = "deserialize_duration_from_string",
346 default = "default_kafka_sync_call_timeout"
347 )]
348 #[with_option(allow_alter_on_fly)]
349 pub sync_call_timeout: Duration,
350}
351
352#[serde_as]
353#[derive(Debug, Clone, Deserialize, WithOptions, PartialEq, Hash, Eq)]
354pub struct KafkaPrivateLinkCommon {
355 #[serde(rename = "broker.rewrite.endpoints")]
357 #[serde_as(as = "Option<JsonString>")]
358 pub broker_rewrite_map: Option<BTreeMap<String, String>>,
359}
360
361const fn default_kafka_sync_call_timeout() -> Duration {
362 Duration::from_secs(5)
363}
364
365const fn default_socket_keepalive_enable() -> bool {
366 true
367}
368
369#[serde_as]
370#[derive(Debug, Clone, Deserialize, WithOptions)]
371pub struct RdKafkaPropertiesCommon {
372 #[serde(rename = "properties.message.max.bytes")]
377 #[serde_as(as = "Option<DisplayFromStr>")]
378 #[with_option(allow_alter_on_fly)]
379 pub message_max_bytes: Option<usize>,
380
381 #[serde(rename = "properties.receive.message.max.bytes")]
386 #[serde_as(as = "Option<DisplayFromStr>")]
387 #[with_option(allow_alter_on_fly)]
388 pub receive_message_max_bytes: Option<usize>,
389
390 #[serde(rename = "properties.statistics.interval.ms")]
391 #[serde_as(as = "Option<DisplayFromStr>")]
392 #[with_option(allow_alter_on_fly)]
393 pub statistics_interval_ms: Option<usize>,
394
395 #[serde(rename = "properties.client.id")]
397 #[serde_as(as = "Option<DisplayFromStr>")]
398 #[with_option(allow_alter_on_fly)]
399 pub client_id: Option<String>,
400
401 #[serde(rename = "properties.enable.ssl.certificate.verification")]
402 #[serde_as(as = "Option<DisplayFromStr>")]
403 #[with_option(allow_alter_on_fly)]
404 pub enable_ssl_certificate_verification: Option<bool>,
405
406 #[serde(rename = "properties.reconnect.backoff.ms")]
409 #[serde_as(as = "Option<DisplayFromStr>")]
410 #[with_option(allow_alter_on_fly)]
411 pub reconnect_backoff_ms: Option<usize>,
412
413 #[serde(rename = "properties.reconnect.backoff.max.ms")]
416 #[serde_as(as = "Option<DisplayFromStr>")]
417 #[with_option(allow_alter_on_fly)]
418 pub reconnect_backoff_max_ms: Option<usize>,
419
420 #[serde(rename = "properties.socket.connection.setup.timeout.ms")]
423 #[serde_as(as = "Option<DisplayFromStr>")]
424 #[with_option(allow_alter_on_fly)]
425 pub socket_connection_setup_timeout_ms: Option<usize>,
426
427 #[serde(
428 rename = "properties.socket.keepalive.enable",
429 default = "default_socket_keepalive_enable"
430 )]
431 #[serde_as(as = "DisplayFromStr")]
432 pub socket_keepalive_enable: bool,
433
434 #[serde(rename = "properties.retry.backoff.ms")]
436 #[serde_as(as = "Option<DisplayFromStr>")]
437 #[with_option(allow_alter_on_fly)]
438 pub retry_backoff_ms: Option<usize>,
439
440 #[serde(rename = "properties.retry.backoff.max.ms")]
442 #[serde_as(as = "Option<DisplayFromStr>")]
443 #[with_option(allow_alter_on_fly)]
444 pub retry_backoff_max_ms: Option<usize>,
445}
446
447impl RdKafkaPropertiesCommon {
448 pub(crate) fn set_client(&self, c: &mut rdkafka::ClientConfig) {
449 if let Some(v) = self.statistics_interval_ms {
450 c.set("statistics.interval.ms", v.to_string());
451 }
452 if let Some(v) = self.message_max_bytes {
453 c.set("message.max.bytes", v.to_string());
454 }
455 if let Some(v) = self.receive_message_max_bytes {
456 c.set("receive.message.max.bytes", v.to_string());
457 }
458 if let Some(v) = self.client_id.as_ref() {
459 c.set("client.id", v);
460 }
461 if let Some(v) = self.enable_ssl_certificate_verification {
462 c.set("enable.ssl.certificate.verification", v.to_string());
463 }
464 if let Some(v) = self.reconnect_backoff_ms {
465 c.set("reconnect.backoff.ms", v.to_string());
466 }
467 if let Some(v) = self.reconnect_backoff_max_ms {
468 c.set("reconnect.backoff.max.ms", v.to_string());
469 }
470 if let Some(v) = self.socket_connection_setup_timeout_ms {
471 c.set("socket.connection.setup.timeout.ms", v.to_string());
472 }
473 c.set(
474 "socket.keepalive.enable",
475 self.socket_keepalive_enable.to_string(),
476 );
477 if let Some(v) = self.retry_backoff_ms {
478 c.set("retry.backoff.ms", v.to_string());
479 }
480 if let Some(v) = self.retry_backoff_max_ms {
481 c.set("retry.backoff.max.ms", v.to_string());
482 }
483 }
484}
485
486impl KafkaConnectionProps {
487 #[cfg(test)]
488 pub fn test_default() -> Self {
489 Self {
490 brokers: "localhost:9092".to_owned(),
491 security_protocol: None,
492 ssl_ca_location: None,
493 ssl_certificate_location: None,
494 ssl_key_location: None,
495 ssl_ca_pem: None,
496 ssl_certificate_pem: None,
497 ssl_key_pem: None,
498 ssl_key_password: None,
499 ssl_endpoint_identification_algorithm: None,
500 sasl_mechanism: None,
501 sasl_username: None,
502 sasl_password: None,
503 sasl_kerberos_service_name: None,
504 sasl_kerberos_keytab: None,
505 sasl_kerberos_principal: None,
506 sasl_kerberos_kinit_cmd: None,
507 sasl_kerberos_min_time_before_relogin: None,
508 sasl_oathbearer_config: None,
509 sasl_oauthbearer_method: None,
510 sasl_oauthbearer_client_id: None,
511 sasl_oauthbearer_client_secret: None,
512 sasl_oauthbearer_token_endpoint_url: None,
513 sasl_oauthbearer_scope: None,
514 sasl_oauthbearer_extensions: None,
515 }
516 }
517
518 pub(crate) fn set_security_properties(&self, config: &mut ClientConfig) {
519 if self.is_aws_msk_iam() {
521 config.set("security.protocol", "SASL_SSL");
522 config.set("sasl.mechanism", "OAUTHBEARER");
523 return;
524 }
525
526 if let Some(security_protocol) = self.security_protocol.as_ref() {
528 config.set("security.protocol", security_protocol);
529 }
530
531 if let Some(ssl_ca_location) = self.ssl_ca_location.as_ref() {
533 config.set("ssl.ca.location", ssl_ca_location);
534 }
535 if let Some(ssl_ca_pem) = self.ssl_ca_pem.as_ref() {
536 config.set("ssl.ca.pem", ssl_ca_pem);
537 }
538 if let Some(ssl_certificate_location) = self.ssl_certificate_location.as_ref() {
539 config.set("ssl.certificate.location", ssl_certificate_location);
540 }
541 if let Some(ssl_certificate_pem) = self.ssl_certificate_pem.as_ref() {
542 config.set("ssl.certificate.pem", ssl_certificate_pem);
543 }
544 if let Some(ssl_key_location) = self.ssl_key_location.as_ref() {
545 config.set("ssl.key.location", ssl_key_location);
546 }
547 if let Some(ssl_key_pem) = self.ssl_key_pem.as_ref() {
548 config.set("ssl.key.pem", ssl_key_pem);
549 }
550 if let Some(ssl_key_password) = self.ssl_key_password.as_ref() {
551 config.set("ssl.key.password", ssl_key_password);
552 }
553 if let Some(ssl_endpoint_identification_algorithm) =
554 self.ssl_endpoint_identification_algorithm.as_ref()
555 {
556 config.set(
558 "ssl.endpoint.identification.algorithm",
559 ssl_endpoint_identification_algorithm,
560 );
561 }
562
563 if let Some(sasl_mechanism) = self.sasl_mechanism.as_ref() {
565 config.set("sasl.mechanism", sasl_mechanism);
566 }
567
568 if let Some(sasl_username) = self.sasl_username.as_ref() {
570 config.set("sasl.username", sasl_username);
571 }
572 if let Some(sasl_password) = self.sasl_password.as_ref() {
573 config.set("sasl.password", sasl_password);
574 }
575
576 if let Some(sasl_kerberos_service_name) = self.sasl_kerberos_service_name.as_ref() {
578 config.set("sasl.kerberos.service.name", sasl_kerberos_service_name);
579 }
580 if let Some(sasl_kerberos_keytab) = self.sasl_kerberos_keytab.as_ref() {
581 config.set("sasl.kerberos.keytab", sasl_kerberos_keytab);
582 }
583 if let Some(sasl_kerberos_principal) = self.sasl_kerberos_principal.as_ref() {
584 config.set("sasl.kerberos.principal", sasl_kerberos_principal);
585 }
586 if let Some(sasl_kerberos_kinit_cmd) = self.sasl_kerberos_kinit_cmd.as_ref() {
587 config.set("sasl.kerberos.kinit.cmd", sasl_kerberos_kinit_cmd);
588 }
589 if let Some(sasl_kerberos_min_time_before_relogin) =
590 self.sasl_kerberos_min_time_before_relogin.as_ref()
591 {
592 config.set(
593 "sasl.kerberos.min.time.before.relogin",
594 sasl_kerberos_min_time_before_relogin,
595 );
596 }
597
598 if let Some(sasl_oathbearer_config) = self.sasl_oathbearer_config.as_ref() {
600 config.set("sasl.oauthbearer.config", sasl_oathbearer_config);
601 }
602 if let Some(sasl_oauthbearer_method) = self.sasl_oauthbearer_method.as_ref() {
603 config.set("sasl.oauthbearer.method", sasl_oauthbearer_method);
604 }
605 if let Some(sasl_oauthbearer_client_id) = self.sasl_oauthbearer_client_id.as_ref() {
606 config.set("sasl.oauthbearer.client.id", sasl_oauthbearer_client_id);
607 }
608 if let Some(sasl_oauthbearer_client_secret) = self.sasl_oauthbearer_client_secret.as_ref() {
609 config.set(
610 "sasl.oauthbearer.client.secret",
611 sasl_oauthbearer_client_secret,
612 );
613 }
614 if let Some(sasl_oauthbearer_token_endpoint_url) =
615 self.sasl_oauthbearer_token_endpoint_url.as_ref()
616 {
617 config.set(
618 "sasl.oauthbearer.token.endpoint.url",
619 sasl_oauthbearer_token_endpoint_url,
620 );
621 }
622 if let Some(sasl_oauthbearer_scope) = self.sasl_oauthbearer_scope.as_ref() {
623 config.set("sasl.oauthbearer.scope", sasl_oauthbearer_scope);
624 }
625 if let Some(sasl_oauthbearer_extensions) = self.sasl_oauthbearer_extensions.as_ref() {
626 config.set("sasl.oauthbearer.extensions", sasl_oauthbearer_extensions);
627 }
628 if !self.is_oauthbearer_oidc() {
630 config.set("enable.sasl.oauthbearer.unsecure.jwt", "true");
631 }
632 }
633
634 pub(crate) fn is_oauthbearer_oidc(&self) -> bool {
635 self.sasl_oauthbearer_method
636 .as_deref()
637 .is_some_and(|m| m.eq_ignore_ascii_case("oidc"))
638 }
639
640 pub(crate) fn is_aws_msk_iam(&self) -> bool {
641 if let Some(sasl_mechanism) = self.sasl_mechanism.as_ref()
642 && sasl_mechanism == AWS_MSK_IAM_AUTH
643 {
644 true
645 } else {
646 false
647 }
648 }
649}
650
651#[derive(Clone, Debug, Deserialize, WithOptions)]
652pub struct PulsarCommon {
653 #[serde(rename = "topic", alias = "pulsar.topic")]
654 pub topic: String,
655
656 #[serde(rename = "service.url", alias = "pulsar.service.url")]
657 pub service_url: String,
658
659 #[serde(rename = "auth.token")]
660 pub auth_token: Option<String>,
661}
662
663impl EnforceSecret for PulsarCommon {
664 const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
665 "pulsar.auth.token",
666 };
667}
668
669#[derive(Clone, Debug, Deserialize, WithOptions)]
670pub struct PulsarOauthCommon {
671 #[serde(rename = "oauth.issuer.url")]
672 pub issuer_url: String,
673
674 #[serde(rename = "oauth.credentials.url")]
675 pub credentials_url: String,
676
677 #[serde(rename = "oauth.audience")]
678 pub audience: String,
679
680 #[serde(rename = "oauth.scope")]
681 pub scope: Option<String>,
682}
683
684fn create_credential_temp_file(credentials: &[u8]) -> std::io::Result<NamedTempFile> {
685 let mut f = NamedTempFile::new()?;
686 f.write_all(credentials)?;
687 f.as_file().sync_all()?;
688 Ok(f)
689}
690
691impl PulsarCommon {
692 pub(crate) async fn build_client(
693 &self,
694 oauth: &Option<PulsarOauthCommon>,
695 aws_auth_props: &AwsAuthProps,
696 operation_retry_options: Option<OperationRetryOptions>,
697 ) -> ConnectorResult<Pulsar<TokioExecutor>> {
698 let mut pulsar_builder = Pulsar::builder(&self.service_url, TokioExecutor);
699 let mut _temp_file = None; if let Some(oauth) = oauth.as_ref() {
702 let (credentials_url, temp_file) = self
703 .resolve_pulsar_credentials_url(oauth, aws_auth_props)
704 .await?;
705 _temp_file = temp_file;
706
707 let auth_params = OAuth2Params {
708 issuer_url: oauth.issuer_url.clone(),
709 credentials_url,
710 audience: Some(oauth.audience.clone()),
711 scope: oauth.scope.clone(),
712 };
713
714 pulsar_builder = pulsar_builder
715 .with_auth_provider(OAuth2Authentication::client_credentials(auth_params));
716 } else if let Some(auth_token) = &self.auth_token {
717 pulsar_builder = pulsar_builder.with_auth(Authentication {
718 name: "token".to_owned(),
719 data: Vec::from(auth_token.as_str()),
720 });
721 }
722
723 if let Some(operation_retry_options) = operation_retry_options {
724 tracing::info!(
725 max_retries = ?operation_retry_options.max_retries,
726 retry_delay_ms = operation_retry_options.retry_delay.as_millis(),
727 "applying Pulsar source operation retry override"
728 );
729 pulsar_builder = pulsar_builder.with_operation_retry_options(operation_retry_options);
730 }
731
732 let res = pulsar_builder.build().await.map_err(|e| anyhow!(e))?;
733 drop(_temp_file); Ok(res)
735 }
736
737 pub(crate) async fn resolve_pulsar_credentials_url(
738 &self,
739 oauth: &PulsarOauthCommon,
740 aws_auth_props: &AwsAuthProps,
741 ) -> ConnectorResult<(String, Option<NamedTempFile>)> {
742 if let Ok(url) = Url::parse(&oauth.credentials_url) {
744 return self
745 .handle_pulsar_credentials_url(&url, aws_auth_props)
746 .await;
747 }
748
749 let path = Path::new(&oauth.credentials_url);
751 if !path.is_absolute() {
752 bail!("credentials_url must be a valid URL (s3://, file://) or an absolute file path");
753 }
754
755 if !tokio::fs::try_exists(&oauth.credentials_url)
757 .await
758 .unwrap_or(false)
759 {
760 bail!("credentials file does not exist: {}", oauth.credentials_url);
761 }
762
763 Ok((format!("file://{}", oauth.credentials_url), None))
765 }
766
767 pub(crate) async fn handle_pulsar_credentials_url(
768 &self,
769 url: &Url,
770 aws_auth_props: &AwsAuthProps,
771 ) -> ConnectorResult<(String, Option<NamedTempFile>)> {
772 match url.scheme() {
773 "s3" => {
774 let credentials = load_file_descriptor_from_s3(url, aws_auth_props).await?;
775 let temp_file = create_credential_temp_file(&credentials)
776 .context("failed to create temp file for pulsar credentials")?;
777
778 let temp_path = temp_file
779 .path()
780 .to_str()
781 .context("temp file path is not valid UTF-8")?;
782
783 Ok((format!("file://{}", temp_path), Some(temp_file)))
784 }
785 "file" => Ok((url.to_string(), None)),
786 _ => bail!(
787 "invalid credentials_url scheme '{}', only file://, s3://, and absolute file paths are supported",
788 url.scheme()
789 ),
790 }
791 }
792}
793
794#[serde_as]
795#[derive(Deserialize, Debug, Clone, WithOptions)]
796pub struct KinesisCommon {
797 #[serde(rename = "stream", alias = "kinesis.stream.name")]
798 pub stream_name: String,
799 #[serde(rename = "aws.region", alias = "kinesis.stream.region")]
800 pub stream_region: String,
801 #[serde(rename = "endpoint", alias = "kinesis.endpoint")]
802 pub endpoint: Option<String>,
803 #[serde(
804 rename = "aws.credentials.access_key_id",
805 alias = "kinesis.credentials.access"
806 )]
807 pub credentials_access_key: Option<String>,
808 #[serde(
809 rename = "aws.credentials.secret_access_key",
810 alias = "kinesis.credentials.secret"
811 )]
812 pub credentials_secret_access_key: Option<String>,
813 #[serde(
814 rename = "aws.credentials.session_token",
815 alias = "kinesis.credentials.session_token"
816 )]
817 pub session_token: Option<String>,
818 #[serde(rename = "aws.credentials.role.arn", alias = "kinesis.assumerole.arn")]
819 pub assume_role_arn: Option<String>,
820 #[serde(
821 rename = "aws.credentials.role.external_id",
822 alias = "kinesis.assumerole.external_id"
823 )]
824 pub assume_role_external_id: Option<String>,
825
826 #[serde(
828 rename = "kinesis.sdk.connect_timeout_ms",
829 default = "kinesis_default_connect_timeout_ms"
830 )]
831 #[serde_as(as = "DisplayFromStr")]
832 pub sdk_connect_timeout_ms: u64,
833
834 #[serde(
835 rename = "kinesis.sdk.read_timeout_ms",
836 default = "kinesis_default_read_timeout_ms"
837 )]
838 #[serde_as(as = "DisplayFromStr")]
839 pub sdk_read_timeout_ms: u64,
840
841 #[serde(
842 rename = "kinesis.sdk.operation_timeout_ms",
843 default = "kinesis_default_operation_timeout_ms"
844 )]
845 #[serde_as(as = "DisplayFromStr")]
846 pub sdk_operation_timeout_ms: u64,
847
848 #[serde(
849 rename = "kinesis.sdk.operation_attempt_timeout_ms",
850 default = "kinesis_default_operation_attempt_timeout_ms"
851 )]
852 #[serde_as(as = "DisplayFromStr")]
853 pub sdk_operation_attempt_timeout_ms: u64,
854
855 #[serde(
856 rename = "kinesis.sdk.max_retry_limit",
857 default = "kinesis_default_max_retry_limit"
858 )]
859 #[serde_as(as = "DisplayFromStr")]
860 pub sdk_max_retry_limit: u32,
861
862 #[serde(
863 rename = "kinesis.sdk.init_backoff_ms",
864 default = "kinesis_default_init_backoff_ms"
865 )]
866 #[serde_as(as = "DisplayFromStr")]
867 pub sdk_init_backoff_ms: u64,
868
869 #[serde(
870 rename = "kinesis.sdk.max_backoff_ms",
871 default = "kinesis_default_max_backoff_ms"
872 )]
873 #[serde_as(as = "DisplayFromStr")]
874 pub sdk_max_backoff_ms: u64,
875}
876
877#[derive(Debug)]
878pub struct KinesisAsyncSleepImpl;
879
880impl AsyncSleep for KinesisAsyncSleepImpl {
881 fn sleep(&self, duration: Duration) -> Sleep {
882 Sleep::new(async move { tokio::time::sleep(duration).await })
883 }
884}
885
886const fn kinesis_default_connect_timeout_ms() -> u64 {
887 10000
888}
889
890const fn kinesis_default_read_timeout_ms() -> u64 {
891 10000
892}
893
894const fn kinesis_default_operation_timeout_ms() -> u64 {
895 10000
896}
897
898const fn kinesis_default_operation_attempt_timeout_ms() -> u64 {
899 10000
900}
901
902const fn kinesis_default_init_backoff_ms() -> u64 {
903 1000
904}
905
906const fn kinesis_default_max_backoff_ms() -> u64 {
907 20000
908}
909
910const fn kinesis_default_max_retry_limit() -> u32 {
911 3
912}
913
914impl EnforceSecret for KinesisCommon {
915 const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
916 "kinesis.credentials.access",
917 "kinesis.credentials.secret",
918 "kinesis.credentials.session_token",
919 };
920}
921
922impl KinesisCommon {
923 pub(crate) async fn build_client(&self) -> ConnectorResult<KinesisClient> {
924 let config = AwsAuthProps {
925 region: Some(self.stream_region.clone()),
926 endpoint: self.endpoint.clone(),
927 access_key: self.credentials_access_key.clone(),
928 secret_key: self.credentials_secret_access_key.clone(),
929 session_token: self.session_token.clone(),
930 arn: self.assume_role_arn.clone(),
931 external_id: self.assume_role_external_id.clone(),
932 profile: Default::default(),
933 msk_signer_timeout_sec: Default::default(),
934 };
935 let aws_config = config.build_config().await?;
936 let mut builder = aws_sdk_kinesis::config::Builder::from(&aws_config);
937 {
938 let sleep_impl = SharedAsyncSleep::new(KinesisAsyncSleepImpl);
940 builder.set_sleep_impl(Some(sleep_impl));
941 let timeout_config = aws_smithy_types::timeout::TimeoutConfig::builder()
942 .connect_timeout(Duration::from_millis(self.sdk_connect_timeout_ms))
943 .read_timeout(Duration::from_millis(self.sdk_read_timeout_ms))
944 .operation_timeout(Duration::from_millis(self.sdk_operation_timeout_ms))
945 .operation_attempt_timeout(Duration::from_millis(
946 self.sdk_operation_attempt_timeout_ms,
947 ))
948 .build();
949 builder.set_timeout_config(Some(timeout_config));
950
951 let retry_config = aws_smithy_types::retry::RetryConfig::standard()
952 .with_initial_backoff(Duration::from_millis(self.sdk_init_backoff_ms))
953 .with_max_backoff(Duration::from_millis(self.sdk_max_backoff_ms))
954 .with_max_attempts(self.sdk_max_retry_limit);
955 builder.set_retry_config(Some(retry_config));
956 }
957 if let Some(endpoint) = &config.endpoint {
958 builder = builder.endpoint_url(endpoint);
959 }
960 Ok(KinesisClient::from_conf(builder.build()))
961 }
962}
963
964#[derive(Debug, Clone, PartialEq, Eq, Hash)]
967pub struct NatsConnectionProps {
968 pub server_url: String,
969 pub connect_mode: String,
970 pub user: Option<String>,
971 pub password: Option<String>,
972 pub jwt: Option<String>,
973 pub nkey: Option<String>,
974}
975
976pub static SHARED_NATS_CLIENT: LazyLock<MokaCache<NatsConnectionProps, Weak<async_nats::Client>>> =
984 LazyLock::new(|| MokaCache::builder().build());
985
986#[serde_as]
987#[derive(Deserialize, Debug, Clone, WithOptions)]
988pub struct NatsCommon {
989 #[serde(rename = "server_url")]
990 pub server_url: String,
991 #[serde(rename = "subject")]
992 pub subject: String,
993 #[serde(rename = "connect_mode")]
994 pub connect_mode: String,
995 #[serde(rename = "username")]
996 pub user: Option<String>,
997 #[serde(rename = "password")]
998 pub password: Option<String>,
999 #[serde(rename = "jwt")]
1000 pub jwt: Option<String>,
1001 #[serde(rename = "nkey")]
1002 pub nkey: Option<String>,
1003 #[serde(rename = "max_bytes")]
1004 #[serde_as(as = "Option<DisplayFromStr>")]
1005 pub max_bytes: Option<i64>,
1006 #[serde(rename = "max_messages")]
1007 #[serde_as(as = "Option<DisplayFromStr>")]
1008 pub max_messages: Option<i64>,
1009 #[serde(rename = "max_messages_per_subject")]
1010 #[serde_as(as = "Option<DisplayFromStr>")]
1011 pub max_messages_per_subject: Option<i64>,
1012 #[serde(rename = "max_consumers")]
1013 #[serde_as(as = "Option<DisplayFromStr>")]
1014 pub max_consumers: Option<i32>,
1015 #[serde(rename = "max_message_size")]
1016 #[serde_as(as = "Option<DisplayFromStr>")]
1017 pub max_message_size: Option<i32>,
1018 #[serde(rename = "allow_create_stream", default)]
1019 #[serde_as(as = "DisplayFromStr")]
1020 pub allow_create_stream: bool,
1021}
1022
1023impl EnforceSecret for NatsCommon {
1024 const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
1025 "password",
1026 "jwt",
1027 "nkey",
1028 };
1029}
1030
1031impl NatsCommon {
1032 pub fn connection_props(&self) -> NatsConnectionProps {
1034 NatsConnectionProps {
1035 server_url: self.server_url.clone(),
1036 connect_mode: self.connect_mode.clone(),
1037 user: self.user.clone(),
1038 password: self.password.clone(),
1039 jwt: self.jwt.clone(),
1040 nkey: self.nkey.clone(),
1041 }
1042 }
1043
1044 async fn build_client_inner(&self) -> ConnectorResult<async_nats::Client> {
1046 let mut connect_options = async_nats::ConnectOptions::new();
1047 match self.connect_mode.as_str() {
1048 "user_and_password" => {
1049 if let (Some(v_user), Some(v_password)) =
1050 (self.user.as_ref(), self.password.as_ref())
1051 {
1052 connect_options =
1053 connect_options.user_and_password(v_user.into(), v_password.into())
1054 } else {
1055 bail!(
1056 "NATS connect mode `user_and_password` requires both `user` and `password`"
1057 );
1058 }
1059 }
1060
1061 "credential" => {
1062 if let (Some(v_nkey), Some(v_jwt)) = (self.nkey.as_ref(), self.jwt.as_ref()) {
1063 connect_options = connect_options
1064 .credentials(&self.create_credential(v_nkey, v_jwt)?)
1065 .expect("failed to parse static creds")
1066 } else {
1067 bail!("NATS connect mode `credential` requires both `nkey` and `jwt`");
1068 }
1069 }
1070 "plain" => {}
1071 _ => {
1072 bail!(
1073 "NATS connect mode must be one of `user_and_password`, `credential`, or `plain`"
1074 );
1075 }
1076 };
1077
1078 let servers = self.server_url.split(',').collect::<Vec<&str>>();
1079 let client = connect_options
1080 .connect(
1081 servers
1082 .iter()
1083 .map(|url| url.parse())
1084 .collect::<Result<Vec<async_nats::ServerAddr>, _>>()?,
1085 )
1086 .await
1087 .context("failed to build the NATS client")
1088 .map_err(SinkError::Nats)?;
1089 Ok(client)
1090 }
1091
1092 pub(crate) async fn build_client(&self) -> ConnectorResult<Arc<async_nats::Client>> {
1095 let connection_props = self.connection_props();
1096 let mut client: Option<Arc<async_nats::Client>> = None;
1097
1098 SHARED_NATS_CLIENT
1099 .entry_by_ref(&connection_props)
1100 .and_try_compute_with::<_, _, crate::error::ConnectorError>(|maybe_entry| async {
1101 if let Some(entry) = maybe_entry
1102 && let entry_value = entry.into_value()
1103 && let Some(existing_client) = entry_value.upgrade()
1104 {
1105 match existing_client.connection_state() {
1106 async_nats::connection::State::Connected => {
1107 tracing::info!("reusing existing NATS client for {}", self.server_url);
1108 client = Some(existing_client);
1109 return Ok(Op::Nop);
1110 }
1111 _ => {
1112 tracing::warn!(
1113 server_url = self.server_url,
1114 "existing NATS client is not connected",
1115 );
1116 }
1117 }
1118 }
1119 tracing::info!(
1120 server_url = self.server_url,
1121 "no cached NATS client was found, or the cached client disconnected; building a new client"
1122 );
1123 let new_client = Arc::new(self.build_client_inner().await?);
1124 client = Some(new_client.clone());
1125 Ok(Op::Put(Arc::downgrade(&new_client)))
1126 })
1127 .await?;
1128
1129 Ok(client.expect("client should be set"))
1130 }
1131
1132 pub(crate) async fn build_context(&self) -> ConnectorResult<jetstream::Context> {
1133 let client = self.build_client().await?;
1134 let jetstream = async_nats::jetstream::new((*client).clone());
1135 Ok(jetstream)
1136 }
1137
1138 pub(crate) fn build_context_from_client(
1140 client: &Arc<async_nats::Client>,
1141 ) -> jetstream::Context {
1142 async_nats::jetstream::new((**client).clone())
1143 }
1144
1145 pub(crate) async fn build_consumer(
1150 &self,
1151 stream: String,
1152 durable_consumer_name: String,
1153 split_id: String,
1154 start_sequence: NatsOffset,
1155 mut config: jetstream::consumer::pull::Config,
1156 existing_client: Option<Arc<async_nats::Client>>,
1157 ) -> ConnectorResult<(
1158 async_nats::jetstream::consumer::Consumer<async_nats::jetstream::consumer::pull::Config>,
1159 Arc<async_nats::Client>,
1160 )> {
1161 let client = match existing_client {
1162 Some(c) => c,
1163 None => self.build_client().await?,
1164 };
1165 let context = Self::build_context_from_client(&client);
1166 let stream = self.build_or_get_stream(context.clone(), stream).await?;
1167 let subject_name = self
1168 .subject
1169 .replace(',', "-")
1170 .replace(['.', '>', '*', ' ', '\t'], "_");
1171 let name = format!("risingwave-consumer-{}-{}", subject_name, split_id);
1172
1173 let deliver_policy = match start_sequence {
1174 NatsOffset::Earliest => DeliverPolicy::All,
1175 NatsOffset::Latest => DeliverPolicy::New,
1176 NatsOffset::SequenceNumber(v) => {
1177 let parsed = v
1179 .parse::<u64>()
1180 .context("failed to parse nats offset as sequence number")?;
1181 DeliverPolicy::ByStartSequence {
1182 start_sequence: 1 + parsed,
1183 }
1184 }
1185 NatsOffset::Timestamp(v) => DeliverPolicy::ByStartTime {
1186 start_time: OffsetDateTime::from_unix_timestamp_nanos(v as i128 * 1_000_000)
1187 .context("invalid timestamp for nats offset")?,
1188 },
1189 NatsOffset::None => DeliverPolicy::All,
1190 };
1191
1192 let consumer = match stream.get_consumer(&name).await {
1193 Ok(consumer) => consumer,
1194 _ => {
1195 stream
1196 .get_or_create_consumer(&name, {
1197 config.deliver_policy = deliver_policy;
1198 config.durable_name = Some(durable_consumer_name);
1199 config.filter_subjects =
1200 self.subject.split(',').map(|s| s.to_owned()).collect();
1201 config
1202 })
1203 .await?
1204 }
1205 };
1206 Ok((consumer, client))
1207 }
1208
1209 pub(crate) async fn build_or_get_stream(
1210 &self,
1211 jetstream: jetstream::Context,
1212 stream_str: String,
1213 ) -> ConnectorResult<jetstream::stream::Stream> {
1214 let subjects: Vec<String> = self.subject.split(',').map(|s| s.to_owned()).collect();
1215
1216 if let Ok(mut stream_instance) = jetstream.get_stream(&stream_str).await {
1219 tracing::info!(
1220 "load existing nats stream ({:?}) with config {:?}",
1221 stream_str,
1222 stream_instance.info().await?
1223 );
1224 return Ok(stream_instance);
1225 }
1226
1227 if !self.allow_create_stream {
1228 return Err(anyhow!(
1229 "stream {} not found, set `allow_create_stream` to true to create a stream",
1230 stream_str
1231 )
1232 .into());
1233 }
1234
1235 let mut config = jetstream::stream::Config {
1236 name: stream_str.clone(),
1237 max_bytes: 1000000,
1238 subjects,
1239 ..Default::default()
1240 };
1241 if let Some(v) = self.max_bytes {
1242 config.max_bytes = v;
1243 }
1244 if let Some(v) = self.max_messages {
1245 config.max_messages = v;
1246 }
1247 if let Some(v) = self.max_messages_per_subject {
1248 config.max_messages_per_subject = v;
1249 }
1250 if let Some(v) = self.max_consumers {
1251 config.max_consumers = v;
1252 }
1253 if let Some(v) = self.max_message_size {
1254 config.max_message_size = v;
1255 }
1256 tracing::info!(
1257 "create nats stream ({:?}) with config {:?}",
1258 &stream_str,
1259 config
1260 );
1261 let stream = jetstream.get_or_create_stream(config).await?;
1262 Ok(stream)
1263 }
1264
1265 pub(crate) fn create_credential(&self, seed: &str, jwt: &str) -> ConnectorResult<String> {
1266 let creds = format!(
1267 "-----BEGIN NATS USER JWT-----\n{}\n------END NATS USER JWT------\n\n\
1268 ************************* IMPORTANT *************************\n\
1269 NKEY Seed printed below can be used to sign and prove identity.\n\
1270 NKEYs are sensitive and should be treated as secrets.\n\n\
1271 -----BEGIN USER NKEY SEED-----\n{}\n------END USER NKEY SEED------\n\n\
1272 *************************************************************",
1273 jwt, seed
1274 );
1275 Ok(creds)
1276 }
1277}
1278
1279pub(crate) fn load_certs(
1280 certificates: &str,
1281) -> ConnectorResult<Vec<rustls_pki_types::CertificateDer<'static>>> {
1282 let cert_bytes = if let Some(path) = certificates.strip_prefix("fs://") {
1283 std::fs::read_to_string(path).map(|cert| cert.as_bytes().to_owned())?
1284 } else {
1285 certificates.as_bytes().to_owned()
1286 };
1287
1288 CertificateDer::pem_slice_iter(&cert_bytes)
1289 .collect::<Result<Vec<_>, _>>()
1290 .context("failed to parse certificates")
1291 .map_err(Into::into)
1292}
1293
1294pub(crate) fn load_private_key(
1295 certificate: &str,
1296) -> ConnectorResult<rustls_pki_types::PrivateKeyDer<'static>> {
1297 let cert_bytes = if let Some(path) = certificate.strip_prefix("fs://") {
1298 std::fs::read_to_string(path).map(|cert| cert.as_bytes().to_owned())?
1299 } else {
1300 certificate.as_bytes().to_owned()
1301 };
1302
1303 let cert = PrivatePkcs8KeyDer::pem_slice_iter(&cert_bytes)
1304 .next()
1305 .ok_or_else(|| anyhow!("No private key found"))?
1306 .context("failed to parse the private key")?;
1307 Ok(cert.into())
1308}
1309
1310#[serde_as]
1311#[derive(Deserialize, Debug, Clone, WithOptions)]
1312pub struct MongodbCommon {
1313 #[serde(rename = "mongodb.url")]
1315 pub connect_uri: String,
1316 #[serde(rename = "collection.name")]
1320 pub collection_name: String,
1321}
1322
1323impl EnforceSecret for MongodbCommon {
1324 const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
1325 "mongodb.url"
1326 };
1327}
1328
1329impl MongodbCommon {
1330 pub(crate) async fn build_client(&self) -> ConnectorResult<mongodb::Client> {
1331 let client = mongodb::Client::with_uri_str(&self.connect_uri).await?;
1332
1333 Ok(client)
1334 }
1335}
1336
1337#[serde_as]
1339#[derive(Debug, Clone, Deserialize, WithOptions)]
1340pub struct TcpKeepaliveConfig {
1341 #[serde(rename = "tcp.keepalive.idle", default = "default_tcp_keepalive_idle")]
1342 #[serde_as(as = "DisplayFromStr")]
1343 pub tcp_keepalive_idle: u32,
1344 #[serde(
1345 rename = "tcp.keepalive.interval",
1346 default = "default_tcp_keepalive_interval"
1347 )]
1348 #[serde_as(as = "DisplayFromStr")]
1349 pub tcp_keepalive_interval: u32,
1350 #[serde(
1351 rename = "tcp.keepalive.count",
1352 default = "default_tcp_keepalive_count"
1353 )]
1354 #[serde_as(as = "DisplayFromStr")]
1355 pub tcp_keepalive_count: u32,
1356}
1357
1358const fn default_tcp_keepalive_idle() -> u32 {
1359 10 * 60
1360}
1361
1362const fn default_tcp_keepalive_interval() -> u32 {
1363 10
1364}
1365
1366const fn default_tcp_keepalive_count() -> u32 {
1367 3
1368}
1369
1370#[cfg(all(test, not(madsim)))]
1371mod tests {
1372 use super::*;
1373
1374 #[test]
1375 fn test_oauthbearer_oidc_does_not_set_unsecure_jwt() {
1376 let mut props = KafkaConnectionProps::test_default();
1377 props.sasl_mechanism = Some("OAUTHBEARER".to_owned());
1378 props.sasl_oauthbearer_method = Some("oidc".to_owned());
1379 props.sasl_oauthbearer_client_id = Some("my-client".to_owned());
1380 props.sasl_oauthbearer_client_secret = Some("my-secret".to_owned());
1381 props.sasl_oauthbearer_token_endpoint_url =
1382 Some("https://idp.example.com/token".to_owned());
1383 props.sasl_oauthbearer_scope = Some("kafka".to_owned());
1384
1385 let mut config = rdkafka::ClientConfig::new();
1386 props.set_security_properties(&mut config);
1387
1388 let map = config.config_map();
1389 assert_eq!(map.get("sasl.mechanism").unwrap(), "OAUTHBEARER");
1390 assert_eq!(map.get("sasl.oauthbearer.method").unwrap(), "oidc");
1391 assert_eq!(map.get("sasl.oauthbearer.client.id").unwrap(), "my-client");
1392 assert_eq!(
1393 map.get("sasl.oauthbearer.client.secret").unwrap(),
1394 "my-secret"
1395 );
1396 assert_eq!(
1397 map.get("sasl.oauthbearer.token.endpoint.url").unwrap(),
1398 "https://idp.example.com/token"
1399 );
1400 assert_eq!(map.get("sasl.oauthbearer.scope").unwrap(), "kafka");
1401 assert!(
1402 !map.contains_key("enable.sasl.oauthbearer.unsecure.jwt"),
1403 "unsecure JWT must not be set when method=oidc"
1404 );
1405 }
1406
1407 #[test]
1408 fn test_default_oauthbearer_sets_unsecure_jwt() {
1409 let mut props = KafkaConnectionProps::test_default();
1410 props.sasl_mechanism = Some("OAUTHBEARER".to_owned());
1411 props.sasl_oathbearer_config = Some("principal=user".to_owned());
1412
1413 let mut config = rdkafka::ClientConfig::new();
1414 props.set_security_properties(&mut config);
1415
1416 let map = config.config_map();
1417 assert_eq!(map.get("sasl.mechanism").unwrap(), "OAUTHBEARER");
1418 assert_eq!(
1419 map.get("enable.sasl.oauthbearer.unsecure.jwt").unwrap(),
1420 "true"
1421 );
1422 assert!(!map.contains_key("sasl.oauthbearer.method"));
1423 }
1424
1425 #[test]
1426 fn test_aws_msk_iam_unaffected_by_oidc() {
1427 let mut props = KafkaConnectionProps::test_default();
1428 props.sasl_mechanism = Some("AWS_MSK_IAM".to_owned());
1429
1430 let mut config = rdkafka::ClientConfig::new();
1431 props.set_security_properties(&mut config);
1432
1433 let map = config.config_map();
1434 assert_eq!(map.get("security.protocol").unwrap(), "SASL_SSL");
1436 assert_eq!(map.get("sasl.mechanism").unwrap(), "OAUTHBEARER");
1437 assert!(!map.contains_key("sasl.oauthbearer.method"));
1439 assert!(!map.contains_key("enable.sasl.oauthbearer.unsecure.jwt"));
1440 }
1441
1442 #[test]
1443 fn test_oauthbearer_oidc_with_extensions() {
1444 let mut props = KafkaConnectionProps::test_default();
1445 props.sasl_mechanism = Some("OAUTHBEARER".to_owned());
1446 props.sasl_oauthbearer_method = Some("oidc".to_owned());
1447 props.sasl_oauthbearer_client_id = Some("client".to_owned());
1448 props.sasl_oauthbearer_client_secret = Some("".to_owned());
1449 props.sasl_oauthbearer_token_endpoint_url =
1450 Some("https://idp.example.com/token".to_owned());
1451 props.sasl_oauthbearer_extensions =
1452 Some("logicalCluster=lkc-abc,identityPoolId=pool-xyz".to_owned());
1453
1454 let mut config = rdkafka::ClientConfig::new();
1455 props.set_security_properties(&mut config);
1456
1457 let map = config.config_map();
1458 assert_eq!(
1459 map.get("sasl.oauthbearer.extensions").unwrap(),
1460 "logicalCluster=lkc-abc,identityPoolId=pool-xyz"
1461 );
1462 }
1463}