github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/utils/aws/endpoint.go (about) 1 /* 2 Copyright 2022 Gravitational, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package aws 18 19 import ( 20 "fmt" 21 "net" 22 "net/url" 23 "strconv" 24 "strings" 25 26 "github.com/gravitational/trace" 27 ) 28 29 // IsAWSEndpoint returns true if the input URI is an AWS endpoint. 30 func IsAWSEndpoint(uri string) bool { 31 // Note that AWSCNEndpointSuffix contains AWSEndpointSuffix so there is no 32 // need to search for AWSCNEndpointSuffix explicitly. 33 return strings.Contains(uri, AWSEndpointSuffix) 34 } 35 36 // IsRDSEndpoint returns true if the input URI is an RDS endpoint. 37 // 38 // https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.Endpoints.html 39 func IsRDSEndpoint(uri string) bool { 40 return isAWSServiceEndpoint(uri, RDSServiceName) 41 } 42 43 // IsRedshiftEndpoint returns true if the input URI is an Redshift endpoint. 44 // 45 // https://docs.aws.amazon.com/redshift/latest/mgmt/connecting-from-psql.html 46 func IsRedshiftEndpoint(uri string) bool { 47 return isAWSServiceEndpoint(uri, RedshiftServiceName) 48 } 49 50 // IsRedshiftServerlessEndpoint returns true if the input URI is an Redshift 51 // Serverless endpoint. 52 // 53 // https://docs.aws.amazon.com/redshift/latest/mgmt/serverless-connecting.html 54 func IsRedshiftServerlessEndpoint(uri string) bool { 55 return isAWSServiceEndpoint(uri, RedshiftServerlessServiceName) 56 } 57 58 // IsElastiCacheEndpoint returns true if the input URI is an ElastiCache 59 // endpoint. 60 func IsElastiCacheEndpoint(uri string) bool { 61 return isAWSServiceEndpoint(uri, ElastiCacheServiceName) 62 } 63 64 // IsMemoryDBEndpoint returns true if the input URI is an MemoryDB 65 // endpoint. 66 func IsMemoryDBEndpoint(uri string) bool { 67 return isAWSServiceEndpoint(uri, MemoryDBSServiceName) 68 } 69 70 // IsKeyspacesEndpoint returns true if input URI is an AWS Keyspaces endpoint. 71 // https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html 72 func IsKeyspacesEndpoint(uri string) bool { 73 hasCassandraPrefix := strings.HasPrefix(uri, "cassandra.") || strings.HasPrefix(uri, "cassandra-fips.") 74 return hasCassandraPrefix && IsAWSEndpoint(uri) 75 } 76 77 // IsOpenSearchEndpoint returns true if input URI is an OpenSearch endpoint. 78 func IsOpenSearchEndpoint(uri string) bool { 79 return isAWSServiceEndpoint(uri, OpenSearchServiceName) 80 } 81 82 // RDSEndpointDetails contains information about an RDS endpoint. 83 type RDSEndpointDetails struct { 84 // InstanceID is the identifier of an RDS instance. 85 InstanceID string 86 // ClusterID is the identifier of an RDS Aurora cluster. 87 ClusterID string 88 // ClusterCustomEndpointName is the identifier of an Aurora cluster custom endpoint. 89 ClusterCustomEndpointName string 90 // ProxyName is the identifier of an RDS proxy. 91 ProxyName string 92 // ProxyCustomEndpointName is the identifier of an RDS proxy custom endpoint. 93 ProxyCustomEndpointName string 94 // Region is the AWS region the database resides in. 95 Region string 96 // EndpointType specifies the type of the endpoint, if available. 97 // 98 // Note that the endpoint type of RDS Proxies are determined by their 99 // targets, so the endpoint type will be empty for RDS Proxies here as it 100 // cannot be decided by the endpoint URL itself. 101 EndpointType string 102 } 103 104 // IsProxy returns true if the RDS endpoint is an RDS Proxy. 105 func (d RDSEndpointDetails) IsProxy() bool { 106 return d.ProxyName != "" || d.ProxyCustomEndpointName != "" 107 } 108 109 // ParseRDSEndpoint extracts the identifier and region from the provided RDS 110 // endpoint. 111 func ParseRDSEndpoint(endpoint string) (d *RDSEndpointDetails, err error) { 112 if strings.ContainsRune(endpoint, ':') { 113 endpoint, _, err = net.SplitHostPort(endpoint) 114 if err != nil { 115 return nil, trace.Wrap(err) 116 } 117 } 118 119 if strings.HasSuffix(endpoint, AWSCNEndpointSuffix) { 120 return parseRDSCNEndpoint(endpoint) 121 } 122 return parseRDSEndpoint(endpoint) 123 } 124 125 // parseRDSEndpoint extracts the identifier and region from the provided RDS 126 // endpoint for standard regions. 127 // 128 // RDS/Aurora endpoints look like this: 129 // aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com 130 func parseRDSEndpoint(endpoint string) (*RDSEndpointDetails, error) { 131 parts := strings.Split(endpoint, ".") 132 hasCorrectLen := len(parts) == 6 || len(parts) == 7 133 serviceNameIndex := len(parts) - 3 134 regionIndex := len(parts) - 4 135 suffixStart := regionIndex 136 137 if !strings.HasSuffix(endpoint, AWSEndpointSuffix) || !hasCorrectLen || parts[serviceNameIndex] != RDSServiceName { 138 return nil, trace.BadParameter("failed to parse %v as RDS endpoint", endpoint) 139 } 140 141 details, err := parseRDSWithoutSuffixes(endpoint, parts[:suffixStart], parts[regionIndex]) 142 return details, trace.Wrap(err) 143 } 144 145 // parseRDSEndpoint extracts the identifier and region from the provided RDS 146 // endpoint for AWS China regions. 147 // 148 // RDS/Aurora endpoints look like this for AWS China regions: 149 // aurora-instance-2.abcdefghijklmnop.rds.cn-north-1.amazonaws.com.cn 150 func parseRDSCNEndpoint(endpoint string) (*RDSEndpointDetails, error) { 151 parts := strings.Split(endpoint, ".") 152 hasCorrectLen := len(parts) == 7 || len(parts) == 8 153 regionIndex := len(parts) - 4 154 serviceNameIndex := len(parts) - 5 155 suffixStart := serviceNameIndex 156 157 if !strings.HasSuffix(endpoint, AWSCNEndpointSuffix) || !hasCorrectLen || parts[serviceNameIndex] != RDSServiceName { 158 return nil, trace.BadParameter("failed to parse %v as RDS CN endpoint", endpoint) 159 } 160 161 details, err := parseRDSWithoutSuffixes(endpoint, parts[:suffixStart], parts[regionIndex]) 162 return details, trace.Wrap(err) 163 } 164 165 // parseRDSWithoutSuffixes extracts identifiers from provided parts and returns 166 // RDSEndpointDetails. It is expected that the provided parts has either: 167 // - two parts (e.g. aurora-instance-1.abcdefghijklmnop) 168 // - or three parts (e.g. my-proxy-custom.endpoint.proxy-abcdefghijklmnop) 169 // as region/service/partition suffixes are removed by the caller. 170 func parseRDSWithoutSuffixes(endpoint string, parts []string, region string) (*RDSEndpointDetails, error) { 171 // RDS/Aurora instance endpoints look like this: 172 // aurora-instance-1.abcdefghijklmnop.<suffixes> 173 // 174 // Aurora cluster endpoints look like this: 175 // my-cluster.cluster-abcdefghijklmnop.<suffixes> 176 // my-cluster.cluster-ro-abcdefghijklmnop.<suffixes> 177 // my-custom.cluster-custom-abcdefghijklmnop.<suffixes> 178 // 179 // RDS Proxy "default" endpoints look like this: 180 // my-proxy.proxy-abcdefghijklmnop.<suffixes> 181 // 182 // RDS Proxy custom endpoints look like this: 183 // my-proxy-custom.endpoint.proxy-abcdefghijklmnop.<suffixes> 184 // 185 // https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.Endpoints.html 186 // https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-setup.html#rds-proxy-connecting 187 // https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-endpoints.html 188 switch len(parts) { 189 case 2: 190 switch { 191 case strings.HasPrefix(parts[1], "cluster-custom-"): 192 // Note that we are not able to get the cluster ID from the cluster 193 // custom endpoints. The cluster ID must be provided separately in 194 // addition to the endpoints. 195 return &RDSEndpointDetails{ 196 ClusterCustomEndpointName: parts[0], 197 Region: region, 198 EndpointType: RDSEndpointTypeCustom, 199 }, nil 200 201 case strings.HasPrefix(parts[1], "cluster-ro-"): 202 return &RDSEndpointDetails{ 203 ClusterID: parts[0], 204 Region: region, 205 EndpointType: RDSEndpointTypeReader, 206 }, nil 207 208 case strings.HasPrefix(parts[1], "cluster-"): 209 return &RDSEndpointDetails{ 210 ClusterID: parts[0], 211 Region: region, 212 EndpointType: RDSEndpointTypePrimary, 213 }, nil 214 215 case strings.HasPrefix(parts[1], "proxy-"): 216 return &RDSEndpointDetails{ 217 ProxyName: parts[0], 218 Region: region, 219 }, nil 220 221 default: 222 return &RDSEndpointDetails{ 223 InstanceID: parts[0], 224 Region: region, 225 EndpointType: RDSEndpointTypeInstance, 226 }, nil 227 } 228 229 case 3: 230 if strings.HasPrefix(parts[2], "proxy-") && parts[1] == "endpoint" { 231 return &RDSEndpointDetails{ 232 ProxyCustomEndpointName: parts[0], 233 Region: region, 234 }, nil 235 } 236 return nil, trace.BadParameter("failed to parse %v as RDS Proxy custom endpoint", endpoint) 237 238 default: 239 return nil, trace.BadParameter("failed to parse %v as RDS endpoint", endpoint) 240 } 241 } 242 243 // ParseRedshiftEndpoint extracts cluster ID and region from the provided 244 // Redshift endpoint. 245 func ParseRedshiftEndpoint(endpoint string) (clusterID, region string, err error) { 246 if strings.ContainsRune(endpoint, ':') { 247 endpoint, _, err = net.SplitHostPort(endpoint) 248 if err != nil { 249 return "", "", trace.Wrap(err) 250 } 251 } 252 253 if strings.HasSuffix(endpoint, AWSCNEndpointSuffix) { 254 return parseRedshiftCNEndpoint(endpoint) 255 } 256 return parseRedshiftEndpoint(endpoint) 257 } 258 259 // parseRedshiftEndpoint extracts cluster ID and region from the provided 260 // Redshift endpoint for standard regions. 261 // 262 // Redshift endpoints look like this: 263 // redshift-cluster-1.abcdefghijklmnop.us-east-1.redshift.amazonaws.com 264 func parseRedshiftEndpoint(endpoint string) (clusterID, region string, err error) { 265 parts := strings.Split(endpoint, ".") 266 if !strings.HasSuffix(endpoint, AWSEndpointSuffix) || len(parts) != 6 || parts[3] != RedshiftServiceName { 267 return "", "", trace.BadParameter("failed to parse %v as Redshift endpoint", endpoint) 268 } 269 return parts[0], parts[2], nil 270 } 271 272 // parseRedshiftCNEndpoint extracts cluster ID and region from the provided 273 // Redshift endpoint for AWS China regions. 274 // 275 // Redshift endpoints look like this for AWS China regions: 276 // redshift-cluster-2.abcdefghijklmnop.redshift.cn-north-1.amazonaws.com.cn 277 func parseRedshiftCNEndpoint(endpoint string) (clusterID, region string, err error) { 278 parts := strings.Split(endpoint, ".") 279 if !strings.HasSuffix(endpoint, AWSCNEndpointSuffix) || len(parts) != 7 || parts[2] != RedshiftServiceName { 280 return "", "", trace.BadParameter("failed to parse %v as Redshift CN endpoint", endpoint) 281 } 282 return parts[0], parts[3], nil 283 } 284 285 // RedshiftServerlessEndpointDetails contains information about an Redshift 286 // Serverless endpoint. 287 type RedshiftServerlessEndpointDetails struct { 288 // WorkgroupName is the name of the workgroup. 289 WorkgroupName string 290 // EndpointName is the name of the VPC endpoint. 291 EndpointName string 292 // AccountID is the AWS Account ID. 293 AccountID string 294 // Region is the AWS region the database resides in. 295 Region string 296 } 297 298 // ParseRedshiftServerlessEndpoint extracts name, AWS Account ID, and region 299 // from the provided Redshift Serverless endpoint. 300 func ParseRedshiftServerlessEndpoint(endpoint string) (details *RedshiftServerlessEndpointDetails, err error) { 301 if strings.ContainsRune(endpoint, ':') { 302 endpoint, _, err = net.SplitHostPort(endpoint) 303 if err != nil { 304 return nil, trace.Wrap(err) 305 } 306 } 307 308 if strings.HasSuffix(endpoint, AWSCNEndpointSuffix) { 309 // TODO(greedy52) add AWS China support when Redshift Serverless come to those regions. 310 return nil, trace.NotImplemented("failed to parse %v as Redshift Serverless endpoint: AWS China regions are not supported yet", endpoint) 311 } 312 return parseRedshiftServerlessEndpoint(endpoint) 313 } 314 315 // parseRedshiftServerlessEndpoint extracts name, AWS account ID, and region 316 // from the provided Redshift Serverless endpoint for standard regions. 317 // 318 // Workgroup endpoint looks like this: 319 // <workgroup-name>.<account-id>.<region>.redshift-serverless.amazonaws.com 320 // 321 // VPC endpoint looks like this: 322 // <vpc-endpoint-name>-endpoint-<some-hash>.<account-id>.<region>.redshift-serverless.amazonaws.com 323 func parseRedshiftServerlessEndpoint(endpoint string) (*RedshiftServerlessEndpointDetails, error) { 324 parts := strings.Split(endpoint, ".") 325 if !strings.HasSuffix(endpoint, AWSEndpointSuffix) || len(parts) != 6 || parts[3] != RedshiftServerlessServiceName { 326 return nil, trace.BadParameter("failed to parse %v as Redshift Serverless endpoint", endpoint) 327 } 328 if endpointName, _, found := strings.Cut(parts[0], "-endpoint-"); found { 329 return &RedshiftServerlessEndpointDetails{ 330 EndpointName: endpointName, 331 AccountID: parts[1], 332 Region: parts[2], 333 }, nil 334 } 335 336 return &RedshiftServerlessEndpointDetails{ 337 WorkgroupName: parts[0], 338 AccountID: parts[1], 339 Region: parts[2], 340 }, nil 341 } 342 343 // RedisEndpointInfo describes details extracted from a ElastiCache or MemoryDB 344 // Redis endpoint. 345 type RedisEndpointInfo struct { 346 // ID is the identifier of the endpoint. 347 ID string 348 // Region is the AWS region for the endpoint. 349 Region string 350 // TransitEncryptionEnabled specifies if in-transit encryption (TLS) is 351 // enabled. 352 TransitEncryptionEnabled bool 353 // EndpointType specifies the type of the endpoint. 354 EndpointType string 355 } 356 357 const ( 358 // ElastiCacheConfigurationEndpoint is the configuration endpoint that used 359 // for cluster mode connection. 360 ElastiCacheConfigurationEndpoint = "configuration" 361 // ElastiCachePrimaryEndpoint is the endpoint of the primary node in the 362 // node group. 363 ElastiCachePrimaryEndpoint = "primary" 364 // ElastiCacheReaderEndpoint is the endpoint of the replica nodes in the 365 // node group. 366 ElastiCacheReaderEndpoint = "reader" 367 // ElastiCacheNodeEndpoint is the endpoint that used to connect to an 368 // individual node. 369 ElastiCacheNodeEndpoint = "node" 370 371 // MemoryDBClusterEndpoint is the cluster configuration endpoint for a 372 // MemoryDB cluster. 373 MemoryDBClusterEndpoint = "cluster" 374 // MemoryDBNodeEndpoint is the endpoint of an individual MemoryDB node. 375 MemoryDBNodeEndpoint = "node" 376 377 // OpenSearchDefaultEndpoint is the default endpoint for domain. 378 OpenSearchDefaultEndpoint = "default" 379 // OpenSearchCustomEndpoint is the custom endpoint configured for domain. 380 OpenSearchCustomEndpoint = "custom" 381 // OpenSearchVPCEndpoint is the VPC endpoint for domain. 382 OpenSearchVPCEndpoint = "vpc" 383 384 // RDSEndpointTypePrimary is the endpoint that specifies the connection for 385 // the primary instance of the RDS cluster. 386 RDSEndpointTypePrimary = "primary" 387 // RDSEndpointTypeReader is the endpoint that load-balances connections 388 // across the Aurora Replicas that are available in an RDS cluster. 389 RDSEndpointTypeReader = "reader" 390 // RDSEndpointTypeCustom is the endpoint that specifies one of the custom 391 // endpoints associated with the RDS cluster. 392 RDSEndpointTypeCustom = "custom" 393 // RDSEndpointTypeInstance is the endpoint of an RDS DB instance. 394 RDSEndpointTypeInstance = "instance" 395 ) 396 397 // ParseElastiCacheEndpoint extracts the details from the provided 398 // ElastiCache Redis endpoint. 399 // 400 // https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/GettingStarted.ConnectToCacheNode.html 401 func ParseElastiCacheEndpoint(endpoint string) (*RedisEndpointInfo, error) { 402 endpoint, err := removeSchemaAndPort(endpoint) 403 if err != nil { 404 return nil, trace.Wrap(err) 405 } 406 407 // Remove partition suffix. Note that endpoints for CN regions use the same 408 // format except they end with AWSCNEndpointSuffix. 409 endpointWithoutSuffix, _, err := removePartitionSuffix(endpoint) 410 if err != nil { 411 return nil, trace.Wrap(err) 412 } 413 414 // Split into parts to extract details. They look like this in general: 415 // <part>.<part>.<part>.<short-region>.cache 416 // 417 // Note that ElastiCache uses short region codes like "use1". 418 // 419 // For Redis with cluster mode enabled, users can connect through either 420 // "configuration" endpoint or individual "node" endpoints. 421 // For Redis with cluster mode disabled, users can connect through either 422 // "primary", "reader", or individual "node" endpoints. 423 parts := strings.Split(endpointWithoutSuffix, ".") 424 if len(parts) == 5 && parts[4] == ElastiCacheServiceName { 425 region, ok := ShortRegionToRegion(parts[3]) 426 if !ok { 427 return nil, trace.BadParameter("%v is not a valid region", parts[3]) 428 } 429 430 // Configuration endpoint for Redis with TLS enabled looks like: 431 // clustercfg.my-redis-shards.xxxxxx.use1.cache.<suffix>:6379 432 if parts[0] == "clustercfg" { 433 return &RedisEndpointInfo{ 434 ID: parts[1], 435 Region: region, 436 TransitEncryptionEnabled: true, 437 EndpointType: ElastiCacheConfigurationEndpoint, 438 }, nil 439 } 440 441 // Configuration endpoint for Redis with TLS disabled looks like: 442 // my-redis-shards.xxxxxx.clustercfg.use1.cache.<suffix>:6379 443 if parts[2] == "clustercfg" { 444 return &RedisEndpointInfo{ 445 ID: parts[0], 446 Region: region, 447 TransitEncryptionEnabled: false, 448 EndpointType: ElastiCacheConfigurationEndpoint, 449 }, nil 450 } 451 452 // Node endpoint for Redis with TLS disabled looks like: 453 // my-redis-cluster-001.xxxxxx.0001.use0.cache.<suffix>:6379 454 // my-redis-shards-0001-001.xxxxxx.0001.use0.cache.<suffix>:6379 455 if isElasticCacheShardID(parts[2]) { 456 return &RedisEndpointInfo{ 457 ID: trimElastiCacheShardAndNodeID(parts[0]), 458 Region: region, 459 TransitEncryptionEnabled: false, 460 EndpointType: ElastiCacheNodeEndpoint, 461 }, nil 462 } 463 464 // Node, primary, reader endpoints for Redis with TLS enabled look like: 465 // my-redis-cluster-001.my-redis-cluster.xxxxxx.use1.cache.<suffix>:6379 466 // my-redis-shards-0001-001.my-redis-shards.xxxxxx.use1.cache.<suffix>:6379 467 // master.my-redis-cluster.xxxxxx.use1.cache.<suffix>:6379 468 // replica.my-redis-cluster.xxxxxx.use1.cache.<suffix>:6379 469 var endpointType string 470 switch strings.ToLower(parts[0]) { 471 case "master": 472 endpointType = ElastiCachePrimaryEndpoint 473 case "replica": 474 endpointType = ElastiCacheReaderEndpoint 475 default: 476 endpointType = ElastiCacheNodeEndpoint 477 } 478 return &RedisEndpointInfo{ 479 ID: parts[1], 480 Region: region, 481 TransitEncryptionEnabled: true, 482 EndpointType: endpointType, 483 }, nil 484 } 485 486 // Primary and reader endpoints for Redis with TLS disabled have an extra 487 // shard ID in the endpoints, and they look like: 488 // my-redis-cluster.xxxxxx.ng.0001.use1.cache.<suffix>:6379 489 // my-redis-cluster-ro.xxxxxx.ng.0001.use1.cache.<suffix>:6379 490 if len(parts) == 6 && parts[5] == ElastiCacheServiceName && isElasticCacheShardID(parts[3]) { 491 region, ok := ShortRegionToRegion(parts[4]) 492 if !ok { 493 return nil, trace.BadParameter("%v is not a valid region", parts[4]) 494 } 495 496 // Remove "-ro" from reader endpoint. 497 if strings.HasSuffix(parts[0], "-ro") { 498 return &RedisEndpointInfo{ 499 ID: strings.TrimSuffix(parts[0], "-ro"), 500 Region: region, 501 TransitEncryptionEnabled: false, 502 EndpointType: ElastiCacheReaderEndpoint, 503 }, nil 504 } 505 506 return &RedisEndpointInfo{ 507 ID: parts[0], 508 Region: region, 509 TransitEncryptionEnabled: false, 510 EndpointType: ElastiCachePrimaryEndpoint, 511 }, nil 512 } 513 514 return nil, trace.BadParameter("unknown ElastiCache Redis endpoint format %q", endpoint) 515 } 516 517 // isElasticCacheShardID returns true if the input part is in shard ID format. 518 // The shard ID is a 4 character string of an integer left padded with zeros 519 // (e.g. 0001). 520 func isElasticCacheShardID(part string) bool { 521 if len(part) != 4 { 522 return false 523 } 524 _, err := strconv.Atoi(part) 525 return err == nil 526 } 527 528 // isElasticCacheNodeID returns true if the input part is in node ID format. 529 // The node ID is a 3 character string of an integer left padded with zeros 530 // (e.g. 001). 531 func isElasticCacheNodeID(part string) bool { 532 if len(part) != 3 { 533 return false 534 } 535 _, err := strconv.Atoi(part) 536 return err == nil 537 } 538 539 // trimElastiCacheShardAndNodeID trims shard and node ID suffix from input. 540 func trimElastiCacheShardAndNodeID(input string) string { 541 // input can be one of: 542 // <replication-group-id> 543 // <replication-group-id>-<node-id> 544 // <replication-group-id>-<shard-id>-<node-id> 545 parts := strings.Split(input, "-") 546 if len(parts) > 0 { 547 if isElasticCacheNodeID(parts[len(parts)-1]) { 548 parts = parts[:len(parts)-1] 549 } 550 } 551 if len(parts) > 0 { 552 if isElasticCacheShardID(parts[len(parts)-1]) { 553 parts = parts[:len(parts)-1] 554 } 555 } 556 return strings.Join(parts, "-") 557 } 558 559 // ParseMemoryDBEndpoint extracts the details from the provided 560 // MemoryDB endpoint. 561 // 562 // https://docs.aws.amazon.com/memorydb/latest/devguide/endpoints.html 563 func ParseMemoryDBEndpoint(endpoint string) (*RedisEndpointInfo, error) { 564 endpoint, err := removeSchemaAndPort(endpoint) 565 if err != nil { 566 return nil, trace.Wrap(err) 567 } 568 569 // Here is a sample endpoint for MemoryDB: 570 // clustercfg.my-memorydb.scwzlu.memorydb.ca-central-1.amazonaws.com 571 // 572 // Unlike RDS/Redshift endpoints, the service subdomain is before region. 573 // Unlike ElastiCache endpoints, MemoryDB uses full region name. 574 endpointWithoutSuffix, _, err := removePartitionSuffix(endpoint) 575 if err != nil { 576 return nil, trace.Wrap(err) 577 } 578 579 parts := strings.Split(endpointWithoutSuffix, ".") 580 if len(parts) != 5 || parts[3] != MemoryDBSServiceName { 581 return nil, trace.BadParameter("unknown MemoryDB endpoint format") 582 } 583 584 switch { 585 // TLS disabled cluster endpoints look like this: 586 // <cluster-name>.<xxxx>.clustercfg.memorydb.<region>.<suffix> 587 case parts[2] == "clustercfg": 588 return &RedisEndpointInfo{ 589 ID: parts[0], 590 Region: parts[4], 591 TransitEncryptionEnabled: false, 592 EndpointType: MemoryDBClusterEndpoint, 593 }, nil 594 595 // TLS enabled cluster endpoints look like this: 596 // clustercfg.<cluster-name>.<xxxx>.memorydb.<region>.<suffix> 597 case parts[0] == "clustercfg": 598 return &RedisEndpointInfo{ 599 ID: parts[1], 600 Region: parts[4], 601 TransitEncryptionEnabled: true, 602 EndpointType: MemoryDBClusterEndpoint, 603 }, nil 604 605 // TLS disabled node endpoints look like this: 606 // <cluster-name>-<shard-id>-<node-id>.<xxxx>.<shard-id>.memorydb.<region>.<suffix> 607 // 608 // MemoryDB and ElastiCache share same shard/node ID format. 609 case isElasticCacheShardID(parts[2]): 610 return &RedisEndpointInfo{ 611 ID: trimElastiCacheShardAndNodeID(parts[0]), 612 Region: parts[4], 613 TransitEncryptionEnabled: false, 614 EndpointType: MemoryDBNodeEndpoint, 615 }, nil 616 617 // TLS enabled node endpoints look like this: 618 // <cluster-name>-<shard-id>-<node-id>.<cluster-name>.<xxxx>.memorydb.<region>.<suffix> 619 default: 620 return &RedisEndpointInfo{ 621 ID: trimElastiCacheShardAndNodeID(parts[0]), 622 Region: parts[4], 623 TransitEncryptionEnabled: true, 624 EndpointType: MemoryDBNodeEndpoint, 625 }, nil 626 } 627 } 628 629 // isAWSServiceEndpoint returns true if uri is a valid AWS endpoint and uri 630 // contains the provided service name as a subdomain. 631 func isAWSServiceEndpoint(uri, serviceName string) bool { 632 return strings.Contains(uri, fmt.Sprintf(".%s.", serviceName)) && 633 IsAWSEndpoint(uri) 634 } 635 636 func removeSchemaAndPort(endpoint string) (string, error) { 637 // Add a temporary schema to make a valid URL for url.Parse. 638 if !strings.Contains(endpoint, "://") { 639 endpoint = "schema://" + endpoint 640 } 641 642 parsedURL, err := url.Parse(endpoint) 643 if err != nil { 644 return "", trace.Wrap(err) 645 } 646 647 return parsedURL.Hostname(), nil 648 } 649 650 func removePartitionSuffix(endpoint string) (string, string, error) { 651 switch { 652 case strings.HasSuffix(endpoint, AWSEndpointSuffix): 653 return strings.TrimSuffix(endpoint, AWSEndpointSuffix), AWSEndpointSuffix, nil 654 655 case strings.HasSuffix(endpoint, AWSCNEndpointSuffix): 656 return strings.TrimSuffix(endpoint, AWSCNEndpointSuffix), AWSCNEndpointSuffix, nil 657 658 default: 659 return "", "", trace.BadParameter("%v is not a valid AWS endpoint", endpoint) 660 } 661 } 662 663 const ( 664 // AWSEndpointSuffix is the endpoint suffix for AWS Standard and AWS US 665 // GovCloud regions. 666 // 667 // https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints 668 // https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/using-govcloud-endpoints.html 669 AWSEndpointSuffix = ".amazonaws.com" 670 671 // AWSCNEndpointSuffix is the endpoint suffix for AWS China regions. 672 // 673 // https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-arns.html 674 AWSCNEndpointSuffix = ".amazonaws.com.cn" 675 676 // RDSServiceName is the service name for AWS RDS. 677 RDSServiceName = "rds" 678 679 // RedshiftServiceName is the service name for AWS Redshift. 680 RedshiftServiceName = "redshift" 681 682 // RedshiftServerlessServiceName is the service name for AWS Redshift Serverless. 683 RedshiftServerlessServiceName = "redshift-serverless" 684 685 // ElastiCacheServiceName is the service name for AWS ElastiCache. 686 ElastiCacheServiceName = "cache" 687 688 // MemoryDBSServiceName is the service name for AWS MemoryDB. 689 MemoryDBSServiceName = "memorydb" 690 691 // DynamoDBServiceName is the service name for AWS DynamoDB. 692 DynamoDBServiceName = "dynamodb" 693 // DynamoDBFipsServiceName is the fips variant service name for AWS DynamoDB. 694 DynamoDBFipsServiceName = "dynamodb-fips" 695 // DynamoDBStreamsServiceName is the AWS DynamoDB Streams service name. 696 DynamoDBStreamsServiceName = "streams.dynamodb" 697 // DAXServiceName is the AWS DynamoDB Accelerator service name. 698 DAXServiceName = "dax" 699 700 // OpenSearchServiceName is the AWS OpenSearch service name. 701 OpenSearchServiceName = "es" 702 ) 703 704 // CassandraEndpointURLForRegion returns a Cassandra endpoint based on the provided region. 705 // https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html 706 func CassandraEndpointURLForRegion(region string) string { 707 if IsCNRegion(region) { 708 return fmt.Sprintf("cassandra.%s%s:9142", region, AWSCNEndpointSuffix) 709 } 710 return fmt.Sprintf("cassandra.%s%s:9142", region, AWSEndpointSuffix) 711 } 712 713 // CassandraEndpointRegion returns an AWS region from cassandra endpoint: 714 // where endpoint looks like cassandra.us-east-2.amazonaws.com 715 // https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html 716 func CassandraEndpointRegion(endpoint string) (string, error) { 717 parts, _, err := extractAWSEndpointParts(endpoint) 718 if err != nil { 719 return "", trace.Wrap(err) 720 } 721 if len(parts) != 2 { 722 return "", trace.BadParameter("invalid Cassandra endpoint") 723 } 724 return parts[1], nil 725 } 726 727 // DynamoDBEndpointInfo describes info extracted from a DynamoDB endpoint. 728 type DynamoDBEndpointInfo struct { 729 // Service is the service subdomain of the endpoint, for example "dynamodb" or "dax". 730 Service string 731 // Region is the AWS region for the endpoint, for example "us-west-1". 732 Region string 733 // Partition is the AWS partition for the endpoint, for example ".amazonaws.com" 734 Partition string 735 } 736 737 // ParseDynamoDBEndpoint parses and extract info from the provided DynamoDB endpoint. 738 func ParseDynamoDBEndpoint(endpoint string) (*DynamoDBEndpointInfo, error) { 739 endpoint = strings.ToLower(endpoint) 740 parts, partition, err := extractAWSEndpointParts(endpoint) 741 if err != nil { 742 return nil, trace.Wrap(err) 743 } 744 switch len(parts) { 745 case 2, 3: 746 default: 747 return nil, trace.BadParameter("invalid DynamoDB endpoint %q", endpoint) 748 } 749 info := &DynamoDBEndpointInfo{ 750 Service: strings.Join(parts[:len(parts)-1], "."), 751 Region: parts[len(parts)-1], 752 Partition: partition, 753 } 754 755 // check for recognized service name. 756 switch info.Service { 757 case DynamoDBServiceName, DynamoDBFipsServiceName, 758 DynamoDBStreamsServiceName, DAXServiceName: 759 default: 760 return nil, trace.BadParameter("invalid DynamoDB endpoint %q", endpoint) 761 } 762 763 // check that the partition is valid for the region. 764 if info.Region == "" || info.Partition == "" { 765 return nil, trace.BadParameter("invalid DynamoDB endpoint %q", endpoint) 766 } 767 switch { 768 case info.Partition == AWSCNEndpointSuffix && IsCNRegion(info.Region): 769 case info.Partition == AWSEndpointSuffix && !IsCNRegion(info.Region): 770 default: 771 return nil, trace.BadParameter("invalid AWS region %q for AWS partition %q", 772 info.Region, info.Partition) 773 } 774 return info, nil 775 } 776 777 // OpenSearchEndpointInfo describes info extracted from an AWS endpoint. 778 type OpenSearchEndpointInfo struct { 779 // Service is the service subdomain of the endpoint. Only "es" allowed for now. 780 Service string 781 // Region is the AWS region for the endpoint, for example "us-west-1". 782 Region string 783 // Partition is the AWS partition for the endpoint, for example ".amazonaws.com" 784 Partition string 785 } 786 787 // ParseOpensearchEndpoint parses and extract info from the provided OpenSearch endpoint. 788 func ParseOpensearchEndpoint(endpoint string) (*OpenSearchEndpointInfo, error) { 789 endpoint = strings.ToLower(endpoint) 790 parts, partition, err := extractAWSEndpointParts(endpoint) 791 if err != nil { 792 return nil, trace.Wrap(err) 793 } 794 795 if len(parts) != 3 { 796 return nil, trace.BadParameter("invalid OpenSearch endpoint %q, wrong number of parts %v", endpoint, len(parts)) 797 } 798 799 info := &OpenSearchEndpointInfo{ 800 Region: parts[len(parts)-2], 801 Service: parts[len(parts)-1], 802 Partition: partition, 803 } 804 805 // check for recognized service name. 806 if info.Service != OpenSearchServiceName { 807 return nil, trace.BadParameter("invalid OpenSearch endpoint %q, invalid service %q", endpoint, info.Service) 808 } 809 810 // check that the partition is valid for the region. 811 switch { 812 case info.Region == "" || info.Partition == "": 813 return nil, trace.BadParameter("invalid OpenSearch endpoint %q, empty partition and region", endpoint) 814 case info.Region == "": 815 return nil, trace.BadParameter("invalid OpenSearch endpoint %q, empty region", endpoint) 816 case info.Partition == "": 817 return nil, trace.BadParameter("invalid OpenSearch endpoint %q, empty partition", endpoint) 818 } 819 820 switch { 821 case info.Partition == AWSCNEndpointSuffix && IsCNRegion(info.Region): 822 case info.Partition == AWSEndpointSuffix && !IsCNRegion(info.Region): 823 default: 824 return nil, trace.BadParameter("invalid AWS region %q for AWS partition %q", 825 info.Region, info.Partition) 826 } 827 return info, nil 828 } 829 830 // DynamoDBURIForRegion constructs a DynamoDB URI based on the AWS region. 831 // The URI uses a custom schema aws:// to differentiate an auto-generated URI from 832 // a user-configured URI in the engine. 833 // When the Teleport DynamoDB engine sees this custom URI schema, it will resolve 834 // the real endpoint using the request API target. 835 // https://docs.aws.amazon.com/general/latest/gr/ddb.html 836 func DynamoDBURIForRegion(region string) string { 837 var suffix string 838 if IsCNRegion(region) { 839 suffix = AWSCNEndpointSuffix 840 } else { 841 suffix = AWSEndpointSuffix 842 } 843 return fmt.Sprintf("aws://dynamodb.%s%s", region, suffix) 844 } 845 846 // extractAWSEndpointParts strips the schema, port, and AWS suffix, 847 // then splits the prefix by subdomain separator (".") and returns the parts and suffix. 848 func extractAWSEndpointParts(endpoint string) ([]string, string, error) { 849 uri, err := removeSchemaAndPort(endpoint) 850 if err != nil { 851 return nil, "", trace.Wrap(err) 852 } 853 prefix, suffix, err := removePartitionSuffix(uri) 854 if err != nil { 855 return nil, "", trace.Wrap(err) 856 } 857 return strings.Split(prefix, "."), suffix, nil 858 }