1use std::collections::{BTreeMap, BTreeSet};
13
14use anyhow::Context;
15use array_concat::concat_arrays;
16use itertools::Itertools;
17use maplit::btreemap;
18use mz_ore::num::NonNeg;
19use mz_ore::str::StrExt;
20use mz_repr::CatalogItemId;
21use mz_sql_parser::ast::ConnectionOptionName::*;
22use mz_sql_parser::ast::display::AstDisplay;
23use mz_sql_parser::ast::{
24 ConnectionDefaultAwsPrivatelink, ConnectionOption, ConnectionOptionName, CreateConnectionType,
25 KafkaBroker, KafkaBrokerAwsPrivatelinkOption, KafkaBrokerAwsPrivatelinkOptionName,
26 KafkaBrokerTunnel, KafkaMatchingBrokerRule,
27};
28use mz_ssh_util::keys::SshKeyPair;
29use mz_storage_types::connections::aws::{
30 AwsAssumeRole, AwsAuth, AwsConnection, AwsConnectionReference, AwsCredentials,
31};
32use mz_storage_types::connections::gcp::{GcpConnection, GcpConnectionReference};
33use mz_storage_types::connections::inline::ReferencedConnection;
34use mz_storage_types::connections::string_or_secret::StringOrSecret;
35use mz_storage_types::connections::{
36 AwsPrivatelink, AwsPrivatelinkConnection, AwsPrivatelinkRule, CsrConnection,
37 CsrConnectionHttpAuth, GlueSchemaRegistryConnection, IcebergCatalogAuth,
38 IcebergCatalogConnection, IcebergCatalogImpl, IcebergCatalogType, KafkaConnection,
39 KafkaSaslConfig, KafkaTlsConfig, KafkaTopicOptions, MySqlConnection, MySqlSslMode,
40 PostgresConnection, RestIcebergCatalog, S3TablesRestIcebergCatalog, SqlServerConnectionDetails,
41 SshConnection, SshTunnel, TlsIdentity, Tunnel,
42};
43
44use crate::names::Aug;
45use crate::plan::statement::{Connection, ResolvedItemName};
46use crate::plan::with_options::{self};
47use crate::plan::{ConnectionDetails, PlanError, SshKey, StatementContext};
48use crate::session::vars;
49
50generate_extracted_config!(
51 ConnectionOption,
52 (AccessKeyId, StringOrSecret),
53 (AssumeRoleArn, String),
54 (AssumeRoleSessionName, String),
55 (AvailabilityZones, Vec<String>),
56 (AwsConnection, with_options::Object),
57 (AwsPrivatelink, ConnectionDefaultAwsPrivatelink<Aug>),
58 (Broker, Vec<KafkaBroker<Aug>>),
59 (Brokers, with_options::BrokersList),
60 (Credential, StringOrSecret),
61 (Database, String),
62 (Endpoint, String),
63 (GcpConnection, with_options::Object),
64 (Host, String),
65 (Password, with_options::Secret),
66 (Port, u16),
67 (ProgressTopic, String),
68 (ProgressTopicReplicationFactor, i32),
69 (PublicKey1, String),
70 (PublicKey2, String),
71 (Region, String),
72 (Registry, String),
73 (SaslMechanisms, String),
74 (SaslPassword, with_options::Secret),
75 (SaslUsername, StringOrSecret),
76 (Scope, String),
77 (SecretAccessKey, with_options::Secret),
78 (SecurityProtocol, String),
79 (ServiceAccountKey, with_options::Secret),
80 (ServiceName, String),
81 (SshTunnel, with_options::Object),
82 (SslCertificate, StringOrSecret),
83 (SslCertificateAuthority, StringOrSecret),
84 (SslKey, with_options::Secret),
85 (SslMode, String),
86 (SessionToken, StringOrSecret),
87 (CatalogType, IcebergCatalogType),
88 (Url, String),
89 (User, StringOrSecret),
90 (Warehouse, String)
91);
92
93generate_extracted_config!(
94 KafkaBrokerAwsPrivatelinkOption,
95 (AvailabilityZone, String),
96 (Port, u16)
97);
98
99pub(crate) const INALTERABLE_OPTIONS: &[ConnectionOptionName] =
101 &[ProgressTopic, ProgressTopicReplicationFactor];
102
103pub(crate) const MUTUALLY_EXCLUSIVE_SETS: &[&[ConnectionOptionName]] = &[&[Broker, Brokers]];
105
106pub(super) fn validate_options_per_connection_type(
107 t: CreateConnectionType,
108 mut options: BTreeSet<ConnectionOptionName>,
109) -> Result<(), PlanError> {
110 use mz_sql_parser::ast::ConnectionOptionName::*;
111 let permitted_options = match t {
112 CreateConnectionType::Aws => [
113 AccessKeyId,
114 SecretAccessKey,
115 SessionToken,
116 Endpoint,
117 Region,
118 AssumeRoleArn,
119 AssumeRoleSessionName,
120 ]
121 .as_slice(),
122 CreateConnectionType::AwsPrivatelink => &[AvailabilityZones, Port, ServiceName],
123 CreateConnectionType::GlueSchemaRegistry => &[AwsConnection, Registry],
124 CreateConnectionType::Gcp => &[ServiceAccountKey],
125 CreateConnectionType::Csr => &[
126 AwsPrivatelink,
127 Password,
128 Port,
129 SshTunnel,
130 SslCertificate,
131 SslCertificateAuthority,
132 SslKey,
133 Url,
134 User,
135 ],
136 CreateConnectionType::Kafka => &[
137 AwsConnection,
138 Broker,
139 Brokers,
140 ProgressTopic,
141 ProgressTopicReplicationFactor,
142 AwsPrivatelink,
143 SshTunnel,
144 SslKey,
145 SslCertificate,
146 SslCertificateAuthority,
147 SaslMechanisms,
148 SaslUsername,
149 SaslPassword,
150 SecurityProtocol,
151 ],
152 CreateConnectionType::Postgres => &[
153 AwsPrivatelink,
154 Database,
155 Host,
156 Password,
157 Port,
158 SshTunnel,
159 SslCertificate,
160 SslCertificateAuthority,
161 SslKey,
162 SslMode,
163 User,
164 ],
165 CreateConnectionType::Ssh => &[Host, Port, User, PublicKey1, PublicKey2],
166 CreateConnectionType::MySql => &[
167 AwsPrivatelink,
168 Host,
169 Password,
170 Port,
171 SshTunnel,
172 SslCertificate,
173 SslCertificateAuthority,
174 SslKey,
175 SslMode,
176 User,
177 AwsConnection,
178 ],
179 CreateConnectionType::SqlServer => &[
180 AwsPrivatelink,
181 Database,
182 Host,
183 Password,
184 Port,
185 SshTunnel,
186 SslCertificate,
187 SslCertificateAuthority,
188 SslKey,
189 SslMode,
190 User,
191 ],
192 CreateConnectionType::IcebergCatalog => &[
193 AwsConnection,
194 CatalogType,
195 Credential,
196 GcpConnection,
197 Scope,
198 Url,
199 Warehouse,
200 ],
201 };
202
203 for o in permitted_options {
204 options.remove(o);
205 }
206
207 if !options.is_empty() {
208 sql_bail!(
209 "{} connections do not support {} values",
210 t,
211 options.iter().join(", ")
212 )
213 }
214
215 Ok(())
216}
217
218impl ConnectionOptionExtracted {
219 pub(super) fn ensure_only_valid_options(
220 &self,
221 t: CreateConnectionType,
222 ) -> Result<(), PlanError> {
223 validate_options_per_connection_type(t, self.seen.clone())
224 }
225
226 pub fn try_into_connection_details(
227 self,
228 scx: &StatementContext,
229 connection_type: CreateConnectionType,
230 ) -> Result<ConnectionDetails, PlanError> {
231 self.ensure_only_valid_options(connection_type)?;
232
233 let connection: ConnectionDetails = match connection_type {
234 CreateConnectionType::Aws => {
235 let credentials = match (
236 self.access_key_id,
237 self.secret_access_key,
238 self.session_token,
239 ) {
240 (Some(access_key_id), Some(secret_access_key), session_token) => {
241 Some(AwsCredentials {
242 access_key_id,
243 secret_access_key: secret_access_key.into(),
244 session_token,
245 })
246 }
247 (None, None, None) => None,
248 _ => {
249 sql_bail!(
250 "must specify both ACCESS KEY ID and SECRET ACCESS KEY with optional SESSION TOKEN"
251 );
252 }
253 };
254
255 let assume_role = match (self.assume_role_arn, self.assume_role_session_name) {
256 (Some(arn), session_name) => Some(AwsAssumeRole { arn, session_name }),
257 (None, Some(_)) => {
258 sql_bail!(
259 "must specify ASSUME ROLE ARN with optional ASSUME ROLE SESSION NAME"
260 );
261 }
262 _ => None,
263 };
264
265 let auth = match (credentials, assume_role) {
266 (None, None) => sql_bail!(
267 "must specify either ASSUME ROLE ARN or ACCESS KEY ID and SECRET ACCESS KEY"
268 ),
269 (Some(credentials), None) => AwsAuth::Credentials(credentials),
270 (None, Some(assume_role)) => AwsAuth::AssumeRole(assume_role),
271 (Some(_), Some(_)) => {
272 sql_bail!("cannot specify both ACCESS KEY ID and ASSUME ROLE ARN");
273 }
274 };
275
276 ConnectionDetails::Aws(AwsConnection {
277 auth,
278 endpoint: match self.endpoint {
279 Some(endpoint) if !endpoint.is_empty() => Some(endpoint),
284 _ => None,
285 },
286 region: self.region,
287 })
288 }
289 CreateConnectionType::AwsPrivatelink => {
290 let connection = AwsPrivatelinkConnection {
291 service_name: self
292 .service_name
293 .ok_or_else(|| sql_err!("SERVICE NAME option is required"))?,
294 availability_zones: self
295 .availability_zones
296 .ok_or_else(|| sql_err!("AVAILABILITY ZONES option is required"))?,
297 };
298 if let Some(supported_azs) = scx.catalog.aws_privatelink_availability_zones() {
299 let mut unique_azs: BTreeSet<String> = BTreeSet::new();
300 let mut duplicate_azs: BTreeSet<String> = BTreeSet::new();
301 for connection_az in &connection.availability_zones {
303 if unique_azs.contains(connection_az) {
304 duplicate_azs.insert(connection_az.to_string());
305 } else {
306 unique_azs.insert(connection_az.to_string());
307 }
308 if !supported_azs.contains(connection_az) {
309 return Err(PlanError::InvalidPrivatelinkAvailabilityZone {
310 name: connection_az.to_string(),
311 supported_azs,
312 });
313 }
314 }
315 if duplicate_azs.len() > 0 {
316 return Err(PlanError::DuplicatePrivatelinkAvailabilityZone {
317 duplicate_azs,
318 });
319 }
320 }
321 ConnectionDetails::AwsPrivatelink(connection)
322 }
323 CreateConnectionType::Gcp => {
324 let credentials_json = self
325 .service_account_key
326 .ok_or_else(|| sql_err!("SERVICE ACCOUNT KEY option is required"))?
327 .into();
328 ConnectionDetails::Gcp(GcpConnection { credentials_json })
329 }
330 CreateConnectionType::Kafka => {
331 let (tls, sasl) = plan_kafka_security(scx, &self)?;
332 let (static_brokers, matching_rules) = self.get_brokers_and_rules(scx)?;
333
334 if !matching_rules.is_empty() {
335 scx.require_feature_flag(&vars::ENABLE_KAFKA_BROKER_MATCHING_RULES)?;
336 }
337
338 ConnectionDetails::Kafka(KafkaConnection {
339 brokers: static_brokers,
340 default_tunnel: build_tunnel_definition(
341 scx,
342 self.ssh_tunnel,
343 self.aws_privatelink,
344 if matching_rules.is_empty() { None } else { Some(matching_rules) },
345 )?,
346 progress_topic: self.progress_topic,
347 progress_topic_options: KafkaTopicOptions {
348 partition_count: Some(NonNeg::try_from(1).expect("1 is positive")),
352 replication_factor: self.progress_topic_replication_factor.map(|val| {
353 if val <= 0 {
354 Err(sql_err!("invalid CONNECTION: PROGRESS TOPIC REPLICATION FACTOR must be greater than 0"))?
355 }
356 NonNeg::try_from(val).map_err(|e| sql_err!("{e}"))
357 }).transpose()?,
358 topic_config: btreemap! {
359 "cleanup.policy".to_string() => "compact".to_string(),
360 "segment.bytes".to_string() => "134217728".to_string(), },
362 },
363 options: BTreeMap::new(),
364 tls,
365 sasl,
366 })
367 }
368 CreateConnectionType::Csr => {
369 let url: reqwest::Url = match self.url {
370 Some(url) => url
371 .parse()
372 .map_err(|e| sql_err!("parsing schema registry url: {e}"))?,
373 None => sql_bail!("invalid CONNECTION: must specify URL"),
374 };
375 let _ = url
376 .host_str()
377 .ok_or_else(|| sql_err!("invalid CONNECTION: URL must specify domain name"))?;
378 if url.path() != "/" {
379 sql_bail!("invalid CONNECTION: URL must have an empty path");
380 }
381 let cert = self.ssl_certificate;
382 let key = self.ssl_key.map(|secret| secret.into());
383 let tls_identity = match (cert, key) {
384 (None, None) => None,
385 (Some(cert), Some(key)) => Some(TlsIdentity { cert, key }),
386 _ => sql_bail!(
387 "invalid CONNECTION: reading from SSL-auth Confluent Schema Registry requires both SSL KEY and SSL CERTIFICATE"
388 ),
389 };
390 let http_auth = self.user.map(|username| CsrConnectionHttpAuth {
391 username,
392 password: self.password.map(|secret| secret.into()),
393 });
394
395 if let Some(privatelink) = self.aws_privatelink.as_ref() {
397 if privatelink.port.is_some() {
398 sql_bail!(
399 "invalid CONNECTION: CONFLUENT SCHEMA REGISTRY does not support PORT for AWS PRIVATELINK"
400 )
401 }
402 }
403 let tunnel = build_tunnel_definition(
404 scx,
405 self.ssh_tunnel,
406 self.aws_privatelink,
407 None, )?;
409
410 ConnectionDetails::Csr(CsrConnection {
411 url,
412 tls_root_cert: self.ssl_certificate_authority,
413 tls_identity,
414 http_auth,
415 tunnel,
416 })
417 }
418 CreateConnectionType::GlueSchemaRegistry => {
419 let aws_connection = get_aws_connection_reference(scx, &self)?
420 .ok_or_else(|| sql_err!("AWS CONNECTION option is required"))?;
421 let registry_name = self
422 .registry
423 .ok_or_else(|| sql_err!("REGISTRY option is required"))?;
424 if registry_name.is_empty() {
425 sql_bail!("invalid CONNECTION: REGISTRY must not be empty");
426 }
427
428 ConnectionDetails::GlueSchemaRegistry(GlueSchemaRegistryConnection {
429 aws_connection,
430 registry_name,
431 })
432 }
433 CreateConnectionType::Postgres => {
434 let cert = self.ssl_certificate;
435 let key = self.ssl_key.map(|secret| secret.into());
436 let tls_identity = match (cert, key) {
437 (None, None) => None,
438 (Some(cert), Some(key)) => Some(TlsIdentity { cert, key }),
439 _ => sql_bail!(
440 "invalid CONNECTION: both SSL KEY and SSL CERTIFICATE are required"
441 ),
442 };
443 let tls_mode = match self.ssl_mode.as_ref().map(|m| m.as_str()) {
444 None | Some("disable") => tokio_postgres::config::SslMode::Disable,
445 Some("require") | Some("required") => tokio_postgres::config::SslMode::Require,
448 Some("verify_ca") | Some("verify-ca") => {
449 tokio_postgres::config::SslMode::VerifyCa
450 }
451 Some("verify_full") | Some("verify-full") => {
452 tokio_postgres::config::SslMode::VerifyFull
453 }
454 Some(m) => sql_bail!("invalid CONNECTION: unknown SSL MODE {}", m.quoted()),
455 };
456
457 if let Some(privatelink) = self.aws_privatelink.as_ref() {
459 if privatelink.port.is_some() {
460 sql_bail!(
461 "invalid CONNECTION: POSTGRES does not support PORT for AWS PRIVATELINK"
462 )
463 }
464 }
465 let tunnel = build_tunnel_definition(
466 scx,
467 self.ssh_tunnel,
468 self.aws_privatelink,
469 None, )?;
471
472 ConnectionDetails::Postgres(PostgresConnection {
473 database: self
474 .database
475 .ok_or_else(|| sql_err!("DATABASE option is required"))?,
476 password: self.password.map(|password| password.into()),
477 host: self
478 .host
479 .ok_or_else(|| sql_err!("HOST option is required"))?,
480 port: self.port.unwrap_or(5432_u16),
481 tunnel,
482 tls_mode,
483 tls_root_cert: self.ssl_certificate_authority,
484 tls_identity,
485 user: self
486 .user
487 .ok_or_else(|| sql_err!("USER option is required"))?,
488 })
489 }
490 CreateConnectionType::Ssh => {
491 let ensure_key = |public_key| match public_key {
492 Some(public_key) => Ok::<_, anyhow::Error>(SshKey::PublicOnly(public_key)),
493 None => {
494 let key = SshKeyPair::new().context("creating SSH key")?;
495 Ok(SshKey::Both(key))
496 }
497 };
498 ConnectionDetails::Ssh {
499 connection: SshConnection {
500 host: self
501 .host
502 .ok_or_else(|| sql_err!("HOST option is required"))?,
503 port: self.port.unwrap_or(22_u16),
504 user: match self
505 .user
506 .ok_or_else(|| sql_err!("USER option is required"))?
507 {
508 StringOrSecret::String(user) => user,
509 StringOrSecret::Secret(_) => {
510 sql_bail!(
511 "SSH connections do not support supplying USER value as SECRET"
512 )
513 }
514 },
515 },
516 key_1: ensure_key(self.public_key1)?,
517 key_2: ensure_key(self.public_key2)?,
518 }
519 }
520 CreateConnectionType::MySql => {
521 let aws_connection = get_aws_connection_reference(scx, &self)?;
522 if aws_connection.is_some() && self.password.is_some() {
523 sql_bail!(
524 "invalid CONNECTION: AWS IAM authentication is not supported with password"
525 );
526 }
527
528 let cert = self.ssl_certificate;
529 let key = self.ssl_key.map(|secret| secret.into());
530 let tls_identity = match (cert, key) {
531 (None, None) => None,
532 (Some(cert), Some(key)) => Some(TlsIdentity { cert, key }),
533 _ => sql_bail!(
534 "invalid CONNECTION: both SSL KEY and SSL CERTIFICATE are required"
535 ),
536 };
537 let tls_mode = match self
540 .ssl_mode
541 .map(|f| f.to_uppercase())
542 .as_ref()
543 .map(|m| m.as_str())
544 {
545 None | Some("DISABLED") => {
546 if aws_connection.is_some() {
547 sql_bail!(
548 "invalid CONNECTION: AWS IAM authentication requires SSL to be enabled"
549 )
550 }
551 MySqlSslMode::Disabled
552 }
553 Some("REQUIRED") | Some("REQUIRE") => MySqlSslMode::Required,
556 Some("VERIFY_CA") | Some("VERIFY-CA") => MySqlSslMode::VerifyCa,
557 Some("VERIFY_IDENTITY") | Some("VERIFY-IDENTITY") => {
558 MySqlSslMode::VerifyIdentity
559 }
560 Some(m) => sql_bail!("invalid CONNECTION: unknown SSL MODE {}", m.quoted()),
561 };
562
563 if let Some(privatelink) = self.aws_privatelink.as_ref() {
565 if privatelink.port.is_some() {
566 sql_bail!(
567 "invalid CONNECTION: MYSQL does not support PORT for AWS PRIVATELINK"
568 )
569 }
570 }
571 let tunnel = build_tunnel_definition(
572 scx,
573 self.ssh_tunnel,
574 self.aws_privatelink,
575 None, )?;
577
578 ConnectionDetails::MySql(MySqlConnection {
579 password: self.password.map(|password| password.into()),
580 host: self
581 .host
582 .ok_or_else(|| sql_err!("HOST option is required"))?,
583 port: self.port.unwrap_or(3306_u16),
584 tunnel,
585 tls_mode,
586 tls_root_cert: self.ssl_certificate_authority,
587 tls_identity,
588 user: self
589 .user
590 .ok_or_else(|| sql_err!("USER option is required"))?,
591 aws_connection,
592 })
593 }
594 CreateConnectionType::SqlServer => {
595 let aws_connection = get_aws_connection_reference(scx, &self)?;
596 if aws_connection.is_some() && self.password.is_some() {
597 sql_bail!(
598 "invalid CONNECTION: AWS IAM authentication is not supported with password"
599 );
600 }
601
602 let (encryption, certificate_validation_policy) = match self
603 .ssl_mode
604 .map(|mode| mode.to_uppercase())
605 .as_ref()
606 .map(|mode| mode.as_str())
607 {
608 None | Some("DISABLED") => (
609 mz_sql_server_util::config::EncryptionLevel::None,
610 mz_sql_server_util::config::CertificateValidationPolicy::TrustAll,
611 ),
612 Some("REQUIRED") => (
613 mz_sql_server_util::config::EncryptionLevel::Required,
614 mz_sql_server_util::config::CertificateValidationPolicy::TrustAll,
615 ),
616 Some("VERIFY") => (
617 mz_sql_server_util::config::EncryptionLevel::Required,
618 mz_sql_server_util::config::CertificateValidationPolicy::VerifySystem,
619 ),
620 Some("VERIFY_CA") => {
621 if self.ssl_certificate_authority.is_none() {
622 sql_bail!(
623 "invalid CONNECTION: SSL MODE 'verify_ca' requires SSL CERTIFICATE AUTHORITY"
624 );
625 }
626 (
627 mz_sql_server_util::config::EncryptionLevel::Required,
628 mz_sql_server_util::config::CertificateValidationPolicy::VerifyCA,
629 )
630 }
631 Some(mode) => {
632 sql_bail!("invalid CONNECTION: unknown SSL MODE {}", mode.quoted())
633 }
634 };
635
636 if let Some(privatelink) = self.aws_privatelink.as_ref() {
637 if privatelink.port.is_some() {
638 sql_bail!(
639 "invalid CONNECTION: SQL SERVER does not support PORT for AWS PRIVATELINK"
640 )
641 }
642 }
643
644 let port = self.port.unwrap_or(1433_u16);
648 let tunnel = build_tunnel_definition(
649 scx,
650 self.ssh_tunnel,
651 self.aws_privatelink,
652 None, )?;
654
655 ConnectionDetails::SqlServer(SqlServerConnectionDetails {
656 host: self
657 .host
658 .ok_or_else(|| sql_err!("HOST option is required"))?,
659 port,
660 database: self
661 .database
662 .ok_or_else(|| sql_err!("DATABASE option is required"))?,
663 user: self
664 .user
665 .ok_or_else(|| sql_err!("USER option is required"))?,
666 password: self
667 .password
668 .ok_or_else(|| sql_err!("PASSWORD option is required"))
669 .map(|pass| pass.into())?,
670 tunnel,
671 encryption,
672 certificate_validation_policy,
673 tls_root_cert: self.ssl_certificate_authority,
674 })
675 }
676 CreateConnectionType::IcebergCatalog => {
677 let catalog_type = self.catalog_type.clone().ok_or_else(|| {
678 sql_err!("invalid CONNECTION: ICEBERG connections must specify CATALOG TYPE")
679 })?;
680
681 let uri: reqwest::Url = match &self.url {
682 Some(url) => url
683 .parse()
684 .map_err(|e| sql_err!("parsing Iceberg catalog url: {e}"))?,
685 None => sql_bail!("invalid CONNECTION: must specify URL"),
686 };
687
688 let warehouse = self.warehouse.clone();
689 let credential = self.credential.clone();
690 let aws_connection = get_aws_connection_reference(scx, &self)?;
691 let gcp_connection = get_gcp_connection_reference(scx, &self)?;
692
693 let catalog = match catalog_type {
694 IcebergCatalogType::S3TablesRest => {
695 if gcp_connection.is_some() {
696 sql_bail!(
697 "invalid CONNECTION: ICEBERG s3tablesrest connections do not support GCP CONNECTION"
698 );
699 }
700 let Some(warehouse) = warehouse else {
701 sql_bail!(
702 "invalid CONNECTION: ICEBERG s3tablesrest connections must specify WAREHOUSE"
703 );
704 };
705 let Some(aws_connection) = aws_connection else {
706 sql_bail!(
707 "invalid CONNECTION: ICEBERG s3tablesrest connections require an AWS connection"
708 );
709 };
710
711 IcebergCatalogImpl::S3TablesRest(S3TablesRestIcebergCatalog {
712 aws_connection,
713 warehouse,
714 })
715 }
716 IcebergCatalogType::Rest => {
717 if aws_connection.is_some() {
718 sql_bail!(
719 "invalid CONNECTION: ICEBERG rest connections do not support AWS CONNECTION.\n\nTry s3tablesrest instead."
720 );
721 }
722 let auth = match (credential, gcp_connection) {
723 (Some(_), Some(_)) => sql_bail!(
724 "invalid CONNECTION: ICEBERG rest connections may set CREDENTIAL or GCP CONNECTION, not both"
725 ),
726 (Some(credential), None) => IcebergCatalogAuth::OAuth {
727 credential,
728 scope: self.scope.clone(),
729 },
730 (None, Some(gcp_connection)) => {
731 const BIGLAKE_CATALOG_URI: &str =
733 "https://biglake.googleapis.com/iceberg/v1/restcatalog";
734 if uri.to_string() != BIGLAKE_CATALOG_URI {
735 sql_bail!(
736 "GCP connection can only be used with '{}'",
737 BIGLAKE_CATALOG_URI
738 );
739 }
740 IcebergCatalogAuth::Gcp(gcp_connection)
741 }
742 (None, None) => sql_bail!(
743 "invalid CONNECTION: ICEBERG rest connections require a CREDENTIAL or GCP CONNECTION"
744 ),
745 };
746
747 IcebergCatalogImpl::Rest(RestIcebergCatalog { auth, warehouse })
748 }
749 };
750
751 ConnectionDetails::IcebergCatalog(IcebergCatalogConnection { catalog, uri })
752 }
753 };
754
755 Ok(connection)
756 }
757
758 pub fn get_brokers_and_rules(
759 &self,
760 scx: &StatementContext,
761 ) -> Result<
762 (
763 Vec<mz_storage_types::connections::KafkaBroker<ReferencedConnection>>,
764 Vec<KafkaMatchingBrokerRule<Aug>>,
765 ),
766 PlanError,
767 > {
768 let mut all_brokers: Vec<KafkaBroker<Aug>> = vec![];
770 let mut matching_rules: Vec<KafkaMatchingBrokerRule<Aug>> = vec![];
771
772 match (&self.broker, &self.brokers, &self.aws_privatelink) {
774 (Some(broker), None, None) => all_brokers.extend(broker.iter().cloned()),
776 (None, Some(broker_list), None) => {
778 all_brokers.extend(broker_list.static_entries.iter().cloned());
779 matching_rules.extend(broker_list.matching_rules.iter().cloned());
780 }
781 (None, None, Some(_privatelink)) => {
783 }
785 (None, None, None) => {
787 sql_bail!("invalid CONNECTION: must set one of BROKER, BROKERS, or AWS PRIVATELINK")
788 }
789 _ => sql_bail!(
791 "invalid CONNECTION: can only set one of BROKER, BROKERS, or AWS PRIVATELINK"
792 ),
793 };
794
795 if !matching_rules.is_empty() && all_brokers.is_empty() {
797 sql_bail!(
798 "invalid CONNECTION: BROKERS must contain at least one static broker address"
799 );
800 }
801
802 let mut out = vec![];
805 for broker in &all_brokers {
806 if broker.address.contains(',') {
807 sql_bail!(
808 "invalid CONNECTION: cannot specify multiple Kafka broker addresses in one string.\n\nInstead, specify BROKERS using multiple strings, e.g. BROKERS ('kafka:9092', 'kafka:9093')"
809 );
810 }
811
812 let tunnel = match &broker.tunnel {
813 KafkaBrokerTunnel::Direct => Tunnel::Direct,
814 KafkaBrokerTunnel::AwsPrivatelink(aws_privatelink) => {
815 Tunnel::AwsPrivatelink(plan_privatelink(scx, aws_privatelink)?)
816 }
817 KafkaBrokerTunnel::SshTunnel(ssh) => {
818 let id = match &ssh {
819 ResolvedItemName::Item { id, .. } => id,
820 _ => sql_bail!(
821 "internal error: Kafka SSH tunnel connection was not resolved"
822 ),
823 };
824 let ssh_tunnel = scx.catalog.get_item(id);
825 match ssh_tunnel.connection()? {
826 Connection::Ssh(_connection) => Tunnel::Ssh(SshTunnel {
827 connection_id: *id,
828 connection: *id,
829 }),
830 _ => {
831 sql_bail!("{} is not an SSH connection", ssh_tunnel.name().item)
832 }
833 }
834 }
835 };
836
837 out.push(mz_storage_types::connections::KafkaBroker {
838 address: broker.address.clone(),
839 tunnel,
840 });
841 }
842
843 Ok((out, matching_rules))
844 }
845}
846
847fn get_aws_connection_reference(
848 scx: &StatementContext,
849 conn_options: &ConnectionOptionExtracted,
850) -> Result<Option<AwsConnectionReference<ReferencedConnection>>, PlanError> {
851 let Some(aws_connection_id) = conn_options.aws_connection else {
852 return Ok(None);
853 };
854
855 let id = CatalogItemId::from(aws_connection_id);
856 let item = scx.catalog.get_item(&id);
857 Ok(match item.connection()? {
858 Connection::Aws(_) => Some(AwsConnectionReference {
859 connection_id: id,
860 connection: id,
861 }),
862 _ => sql_bail!("{} is not an AWS connection", item.name().item),
863 })
864}
865fn get_gcp_connection_reference(
866 scx: &StatementContext,
867 conn_options: &ConnectionOptionExtracted,
868) -> Result<Option<GcpConnectionReference<ReferencedConnection>>, PlanError> {
869 let Some(gcp_connection_id) = conn_options.gcp_connection else {
870 return Ok(None);
871 };
872
873 let id = CatalogItemId::from(gcp_connection_id);
874 let item = scx.catalog.get_item(&id);
875 Ok(match item.connection()? {
876 Connection::Gcp(_) => Some(GcpConnectionReference {
877 connection_id: id,
878 connection: id,
879 }),
880 _ => sql_bail!("{} is not a GCP connection", item.name().item),
881 })
882}
883
884fn plan_kafka_security(
885 scx: &StatementContext,
886 v: &ConnectionOptionExtracted,
887) -> Result<
888 (
889 Option<KafkaTlsConfig>,
890 Option<KafkaSaslConfig<ReferencedConnection>>,
891 ),
892 PlanError,
893> {
894 const SASL_CONFIGS: [ConnectionOptionName; 4] = [
895 ConnectionOptionName::AwsConnection,
896 ConnectionOptionName::SaslMechanisms,
897 ConnectionOptionName::SaslUsername,
898 ConnectionOptionName::SaslPassword,
899 ];
900
901 const ALL_CONFIGS: [ConnectionOptionName; 7] = concat_arrays!(
902 [
903 ConnectionOptionName::SslKey,
904 ConnectionOptionName::SslCertificate,
905 ConnectionOptionName::SslCertificateAuthority,
906 ],
907 SASL_CONFIGS
908 );
909
910 enum SecurityProtocol {
911 Plaintext,
912 Ssl,
913 SaslPlaintext,
914 SaslSsl,
915 }
916
917 let security_protocol = v.security_protocol.as_ref().map(|v| v.to_uppercase());
918 let security_protocol = match security_protocol.as_deref() {
919 Some("PLAINTEXT") => SecurityProtocol::Plaintext,
920 Some("SSL") => SecurityProtocol::Ssl,
921 Some("SASL_PLAINTEXT") => SecurityProtocol::SaslPlaintext,
922 Some("SASL_SSL") => SecurityProtocol::SaslSsl,
923 Some(p) => sql_bail!("unknown security protocol: {}", p),
924 None if SASL_CONFIGS.iter().any(|c| v.seen.contains(c)) => SecurityProtocol::SaslSsl,
929 None => SecurityProtocol::Ssl,
930 };
931
932 let mut outstanding = ALL_CONFIGS
933 .into_iter()
934 .filter(|c| v.seen.contains(c))
935 .collect::<BTreeSet<ConnectionOptionName>>();
936
937 let tls = match security_protocol {
938 SecurityProtocol::Ssl | SecurityProtocol::SaslSsl => {
939 outstanding.remove(&ConnectionOptionName::SslCertificate);
940 let identity = match &v.ssl_certificate {
941 None => None,
942 Some(cert) => {
943 outstanding.remove(&ConnectionOptionName::SslKey);
944 let Some(key) = &v.ssl_key else {
945 sql_bail!("SSL KEY must be specified with SSL CERTIFICATE");
946 };
947 Some(TlsIdentity {
948 cert: cert.clone(),
949 key: (*key).into(),
950 })
951 }
952 };
953 outstanding.remove(&ConnectionOptionName::SslCertificateAuthority);
954 Some(KafkaTlsConfig {
955 identity,
956 root_cert: v.ssl_certificate_authority.clone(),
957 })
958 }
959 _ => None,
960 };
961
962 let sasl = match security_protocol {
963 SecurityProtocol::SaslPlaintext | SecurityProtocol::SaslSsl => {
964 outstanding.remove(&ConnectionOptionName::AwsConnection);
965 match get_aws_connection_reference(scx, v)? {
966 Some(aws) => Some(KafkaSaslConfig {
967 mechanism: "OAUTHBEARER".into(),
968 username: "".into(),
969 password: None,
970 aws: Some(aws),
971 }),
972 None => {
973 outstanding.remove(&ConnectionOptionName::SaslMechanisms);
974 outstanding.remove(&ConnectionOptionName::SaslUsername);
975 outstanding.remove(&ConnectionOptionName::SaslPassword);
976 let Some(mechanism) = &v.sasl_mechanisms else {
979 sql_bail!("SASL MECHANISMS must be specified");
980 };
981 let Some(username) = &v.sasl_username else {
982 sql_bail!("SASL USERNAME must be specified");
983 };
984 let Some(password) = &v.sasl_password else {
985 sql_bail!("SASL PASSWORD must be specified");
986 };
987 Some(KafkaSaslConfig {
988 mechanism: mechanism.to_uppercase(),
999 username: username.clone(),
1000 password: Some((*password).into()),
1001 aws: None,
1002 })
1003 }
1004 }
1005 }
1006 _ => None,
1007 };
1008
1009 if let Some(outstanding) = outstanding.first() {
1010 sql_bail!("option {outstanding} not supported with this configuration");
1011 }
1012
1013 Ok((tls, sasl))
1014}
1015pub fn plan_default_privatelink(
1016 scx: &StatementContext,
1017 pl: &mz_sql_parser::ast::ConnectionDefaultAwsPrivatelink<Aug>,
1018) -> Result<AwsPrivatelink, PlanError> {
1019 let id = pl.connection.item_id().clone();
1020 let entry = scx.catalog.get_item(&id);
1021 match entry.connection()? {
1022 Connection::AwsPrivatelink(_) => Ok(AwsPrivatelink {
1023 connection_id: id,
1024 availability_zone: None,
1026 port: pl.port,
1028 }),
1029 _ => sql_bail!("{} is not an AWS PRIVATELINK connection", entry.name().item),
1030 }
1031}
1032
1033pub fn plan_privatelink(
1034 scx: &StatementContext,
1035 pl: &mz_sql_parser::ast::KafkaBrokerAwsPrivatelink<Aug>,
1036) -> Result<AwsPrivatelink, PlanError> {
1037 let KafkaBrokerAwsPrivatelinkOptionExtracted {
1038 availability_zone,
1039 port,
1040 seen: _,
1041 } = KafkaBrokerAwsPrivatelinkOptionExtracted::try_from(pl.options.clone())?;
1042
1043 let id = match &pl.connection {
1044 ResolvedItemName::Item { id, .. } => id,
1045 _ => sql_bail!("internal error: Kafka PrivateLink connection was not resolved"),
1046 };
1047 let entry = scx.catalog.get_item(id);
1048 match entry.connection()? {
1049 Connection::AwsPrivatelink(connection) => {
1050 if let Some(az) = &availability_zone {
1051 if !connection.availability_zones.contains(az) {
1052 sql_bail!(
1053 "AWS PrivateLink availability zone {} does not match any of the \
1054 availability zones on the AWS PrivateLink connection {}",
1055 az.quoted(),
1056 scx.catalog
1057 .resolve_full_name(entry.name())
1058 .to_string()
1059 .quoted()
1060 )
1061 }
1062 }
1063 Ok(AwsPrivatelink {
1064 connection_id: *id,
1065 availability_zone,
1066 port,
1067 })
1068 }
1069 _ => {
1070 sql_bail!("{} is not an AWS PRIVATELINK connection", entry.name().item)
1071 }
1072 }
1073}
1074
1075pub(crate) fn build_tunnel_definition(
1076 scx: &StatementContext,
1077 ssh_tunnel: Option<with_options::Object>,
1078 aws_privatelink: Option<ConnectionDefaultAwsPrivatelink<Aug>>,
1079 matching_rules: Option<Vec<KafkaMatchingBrokerRule<Aug>>>,
1080) -> Result<Tunnel<ReferencedConnection>, PlanError> {
1081 Ok(match (ssh_tunnel, aws_privatelink, matching_rules) {
1082 (None, None, None) => Tunnel::Direct,
1083 (Some(ssh_tunnel), None, None) => {
1084 let id = CatalogItemId::from(ssh_tunnel);
1085 let ssh_tunnel = scx.catalog.get_item(&id);
1086 match ssh_tunnel.connection()? {
1087 Connection::Ssh(_connection) => Tunnel::Ssh(SshTunnel {
1088 connection_id: id,
1089 connection: id,
1090 }),
1091 _ => sql_bail!("{} is not an SSH connection", ssh_tunnel.name().item),
1092 }
1093 }
1094 (None, Some(aws_privatelink), None) => {
1095 Tunnel::AwsPrivatelink(plan_default_privatelink(scx, &aws_privatelink)?)
1096 }
1097 (None, None, Some(rules)) => {
1098 if rules.is_empty() {
1099 sql_bail!("BROKERS MATCHING rules list cannot be empty");
1100 }
1101
1102 let rules = rules
1103 .iter()
1104 .map(|rule| {
1105 Ok(AwsPrivatelinkRule {
1106 pattern: rule.pattern.clone(),
1107 to: plan_privatelink(scx, &rule.tunnel)?,
1108 })
1109 })
1110 .collect::<Result<Vec<_>, PlanError>>()?;
1111 Tunnel::AwsPrivatelinks(mz_storage_types::connections::AwsPrivatelinks { rules })
1112 }
1113 _ => {
1114 sql_bail!("cannot specify both SSH TUNNEL and AWS PRIVATELINK");
1115 }
1116 })
1117}