mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 06:08:23 +08:00
* Improve error message when auto-user provisioning fails on reader endpoints * fix lint
859 lines
30 KiB
Go
859 lines
30 KiB
Go
/*
|
|
Copyright 2022 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package aws
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gravitational/trace"
|
|
)
|
|
|
|
// IsAWSEndpoint returns true if the input URI is an AWS endpoint.
|
|
func IsAWSEndpoint(uri string) bool {
|
|
// Note that AWSCNEndpointSuffix contains AWSEndpointSuffix so there is no
|
|
// need to search for AWSCNEndpointSuffix explicitly.
|
|
return strings.Contains(uri, AWSEndpointSuffix)
|
|
}
|
|
|
|
// IsRDSEndpoint returns true if the input URI is an RDS endpoint.
|
|
//
|
|
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.Endpoints.html
|
|
func IsRDSEndpoint(uri string) bool {
|
|
return isAWSServiceEndpoint(uri, RDSServiceName)
|
|
}
|
|
|
|
// IsRedshiftEndpoint returns true if the input URI is an Redshift endpoint.
|
|
//
|
|
// https://docs.aws.amazon.com/redshift/latest/mgmt/connecting-from-psql.html
|
|
func IsRedshiftEndpoint(uri string) bool {
|
|
return isAWSServiceEndpoint(uri, RedshiftServiceName)
|
|
}
|
|
|
|
// IsRedshiftServerlessEndpoint returns true if the input URI is an Redshift
|
|
// Serverless endpoint.
|
|
//
|
|
// https://docs.aws.amazon.com/redshift/latest/mgmt/serverless-connecting.html
|
|
func IsRedshiftServerlessEndpoint(uri string) bool {
|
|
return isAWSServiceEndpoint(uri, RedshiftServerlessServiceName)
|
|
}
|
|
|
|
// IsElastiCacheEndpoint returns true if the input URI is an ElastiCache
|
|
// endpoint.
|
|
func IsElastiCacheEndpoint(uri string) bool {
|
|
return isAWSServiceEndpoint(uri, ElastiCacheServiceName)
|
|
}
|
|
|
|
// IsMemoryDBEndpoint returns true if the input URI is an MemoryDB
|
|
// endpoint.
|
|
func IsMemoryDBEndpoint(uri string) bool {
|
|
return isAWSServiceEndpoint(uri, MemoryDBSServiceName)
|
|
}
|
|
|
|
// IsKeyspacesEndpoint returns true if input URI is an AWS Keyspaces endpoint.
|
|
// https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html
|
|
func IsKeyspacesEndpoint(uri string) bool {
|
|
hasCassandraPrefix := strings.HasPrefix(uri, "cassandra.") || strings.HasPrefix(uri, "cassandra-fips.")
|
|
return hasCassandraPrefix && IsAWSEndpoint(uri)
|
|
}
|
|
|
|
// IsOpenSearchEndpoint returns true if input URI is an OpenSearch endpoint.
|
|
func IsOpenSearchEndpoint(uri string) bool {
|
|
return isAWSServiceEndpoint(uri, OpenSearchServiceName)
|
|
}
|
|
|
|
// RDSEndpointDetails contains information about an RDS endpoint.
|
|
type RDSEndpointDetails struct {
|
|
// InstanceID is the identifier of an RDS instance.
|
|
InstanceID string
|
|
// ClusterID is the identifier of an RDS Aurora cluster.
|
|
ClusterID string
|
|
// ClusterCustomEndpointName is the identifier of an Aurora cluster custom endpoint.
|
|
ClusterCustomEndpointName string
|
|
// ProxyName is the identifier of an RDS proxy.
|
|
ProxyName string
|
|
// ProxyCustomEndpointName is the identifier of an RDS proxy custom endpoint.
|
|
ProxyCustomEndpointName string
|
|
// Region is the AWS region the database resides in.
|
|
Region string
|
|
// EndpointType specifies the type of the endpoint, if available.
|
|
//
|
|
// Note that the endpoint type of RDS Proxies are determined by their
|
|
// targets, so the endpoint type will be empty for RDS Proxies here as it
|
|
// cannot be decided by the endpoint URL itself.
|
|
EndpointType string
|
|
}
|
|
|
|
// IsProxy returns true if the RDS endpoint is an RDS Proxy.
|
|
func (d RDSEndpointDetails) IsProxy() bool {
|
|
return d.ProxyName != "" || d.ProxyCustomEndpointName != ""
|
|
}
|
|
|
|
// ParseRDSEndpoint extracts the identifier and region from the provided RDS
|
|
// endpoint.
|
|
func ParseRDSEndpoint(endpoint string) (d *RDSEndpointDetails, err error) {
|
|
if strings.ContainsRune(endpoint, ':') {
|
|
endpoint, _, err = net.SplitHostPort(endpoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if strings.HasSuffix(endpoint, AWSCNEndpointSuffix) {
|
|
return parseRDSCNEndpoint(endpoint)
|
|
}
|
|
return parseRDSEndpoint(endpoint)
|
|
}
|
|
|
|
// parseRDSEndpoint extracts the identifier and region from the provided RDS
|
|
// endpoint for standard regions.
|
|
//
|
|
// RDS/Aurora endpoints look like this:
|
|
// aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com
|
|
func parseRDSEndpoint(endpoint string) (*RDSEndpointDetails, error) {
|
|
parts := strings.Split(endpoint, ".")
|
|
hasCorrectLen := len(parts) == 6 || len(parts) == 7
|
|
serviceNameIndex := len(parts) - 3
|
|
regionIndex := len(parts) - 4
|
|
suffixStart := regionIndex
|
|
|
|
if !strings.HasSuffix(endpoint, AWSEndpointSuffix) || !hasCorrectLen || parts[serviceNameIndex] != RDSServiceName {
|
|
return nil, trace.BadParameter("failed to parse %v as RDS endpoint", endpoint)
|
|
}
|
|
|
|
details, err := parseRDSWithoutSuffixes(endpoint, parts[:suffixStart], parts[regionIndex])
|
|
return details, trace.Wrap(err)
|
|
}
|
|
|
|
// parseRDSEndpoint extracts the identifier and region from the provided RDS
|
|
// endpoint for AWS China regions.
|
|
//
|
|
// RDS/Aurora endpoints look like this for AWS China regions:
|
|
// aurora-instance-2.abcdefghijklmnop.rds.cn-north-1.amazonaws.com.cn
|
|
func parseRDSCNEndpoint(endpoint string) (*RDSEndpointDetails, error) {
|
|
parts := strings.Split(endpoint, ".")
|
|
hasCorrectLen := len(parts) == 7 || len(parts) == 8
|
|
regionIndex := len(parts) - 4
|
|
serviceNameIndex := len(parts) - 5
|
|
suffixStart := serviceNameIndex
|
|
|
|
if !strings.HasSuffix(endpoint, AWSCNEndpointSuffix) || !hasCorrectLen || parts[serviceNameIndex] != RDSServiceName {
|
|
return nil, trace.BadParameter("failed to parse %v as RDS CN endpoint", endpoint)
|
|
}
|
|
|
|
details, err := parseRDSWithoutSuffixes(endpoint, parts[:suffixStart], parts[regionIndex])
|
|
return details, trace.Wrap(err)
|
|
}
|
|
|
|
// parseRDSWithoutSuffixes extracts identifiers from provided parts and returns
|
|
// RDSEndpointDetails. It is expected that the provided parts has either:
|
|
// - two parts (e.g. aurora-instance-1.abcdefghijklmnop)
|
|
// - or three parts (e.g. my-proxy-custom.endpoint.proxy-abcdefghijklmnop)
|
|
// as region/service/partition suffixes are removed by the caller.
|
|
func parseRDSWithoutSuffixes(endpoint string, parts []string, region string) (*RDSEndpointDetails, error) {
|
|
// RDS/Aurora instance endpoints look like this:
|
|
// aurora-instance-1.abcdefghijklmnop.<suffixes>
|
|
//
|
|
// Aurora cluster endpoints look like this:
|
|
// my-cluster.cluster-abcdefghijklmnop.<suffixes>
|
|
// my-cluster.cluster-ro-abcdefghijklmnop.<suffixes>
|
|
// my-custom.cluster-custom-abcdefghijklmnop.<suffixes>
|
|
//
|
|
// RDS Proxy "default" endpoints look like this:
|
|
// my-proxy.proxy-abcdefghijklmnop.<suffixes>
|
|
//
|
|
// RDS Proxy custom endpoints look like this:
|
|
// my-proxy-custom.endpoint.proxy-abcdefghijklmnop.<suffixes>
|
|
//
|
|
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.Endpoints.html
|
|
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-setup.html#rds-proxy-connecting
|
|
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-endpoints.html
|
|
switch len(parts) {
|
|
case 2:
|
|
switch {
|
|
case strings.HasPrefix(parts[1], "cluster-custom-"):
|
|
// Note that we are not able to get the cluster ID from the cluster
|
|
// custom endpoints. The cluster ID must be provided separately in
|
|
// addition to the endpoints.
|
|
return &RDSEndpointDetails{
|
|
ClusterCustomEndpointName: parts[0],
|
|
Region: region,
|
|
EndpointType: RDSEndpointTypeCustom,
|
|
}, nil
|
|
|
|
case strings.HasPrefix(parts[1], "cluster-ro-"):
|
|
return &RDSEndpointDetails{
|
|
ClusterID: parts[0],
|
|
Region: region,
|
|
EndpointType: RDSEndpointTypeReader,
|
|
}, nil
|
|
|
|
case strings.HasPrefix(parts[1], "cluster-"):
|
|
return &RDSEndpointDetails{
|
|
ClusterID: parts[0],
|
|
Region: region,
|
|
EndpointType: RDSEndpointTypePrimary,
|
|
}, nil
|
|
|
|
case strings.HasPrefix(parts[1], "proxy-"):
|
|
return &RDSEndpointDetails{
|
|
ProxyName: parts[0],
|
|
Region: region,
|
|
}, nil
|
|
|
|
default:
|
|
return &RDSEndpointDetails{
|
|
InstanceID: parts[0],
|
|
Region: region,
|
|
EndpointType: RDSEndpointTypeInstance,
|
|
}, nil
|
|
}
|
|
|
|
case 3:
|
|
if strings.HasPrefix(parts[2], "proxy-") && parts[1] == "endpoint" {
|
|
return &RDSEndpointDetails{
|
|
ProxyCustomEndpointName: parts[0],
|
|
Region: region,
|
|
}, nil
|
|
}
|
|
return nil, trace.BadParameter("failed to parse %v as RDS Proxy custom endpoint", endpoint)
|
|
|
|
default:
|
|
return nil, trace.BadParameter("failed to parse %v as RDS endpoint", endpoint)
|
|
}
|
|
}
|
|
|
|
// ParseRedshiftEndpoint extracts cluster ID and region from the provided
|
|
// Redshift endpoint.
|
|
func ParseRedshiftEndpoint(endpoint string) (clusterID, region string, err error) {
|
|
if strings.ContainsRune(endpoint, ':') {
|
|
endpoint, _, err = net.SplitHostPort(endpoint)
|
|
if err != nil {
|
|
return "", "", trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if strings.HasSuffix(endpoint, AWSCNEndpointSuffix) {
|
|
return parseRedshiftCNEndpoint(endpoint)
|
|
}
|
|
return parseRedshiftEndpoint(endpoint)
|
|
}
|
|
|
|
// parseRedshiftEndpoint extracts cluster ID and region from the provided
|
|
// Redshift endpoint for standard regions.
|
|
//
|
|
// Redshift endpoints look like this:
|
|
// redshift-cluster-1.abcdefghijklmnop.us-east-1.redshift.amazonaws.com
|
|
func parseRedshiftEndpoint(endpoint string) (clusterID, region string, err error) {
|
|
parts := strings.Split(endpoint, ".")
|
|
if !strings.HasSuffix(endpoint, AWSEndpointSuffix) || len(parts) != 6 || parts[3] != RedshiftServiceName {
|
|
return "", "", trace.BadParameter("failed to parse %v as Redshift endpoint", endpoint)
|
|
}
|
|
return parts[0], parts[2], nil
|
|
}
|
|
|
|
// parseRedshiftCNEndpoint extracts cluster ID and region from the provided
|
|
// Redshift endpoint for AWS China regions.
|
|
//
|
|
// Redshift endpoints look like this for AWS China regions:
|
|
// redshift-cluster-2.abcdefghijklmnop.redshift.cn-north-1.amazonaws.com.cn
|
|
func parseRedshiftCNEndpoint(endpoint string) (clusterID, region string, err error) {
|
|
parts := strings.Split(endpoint, ".")
|
|
if !strings.HasSuffix(endpoint, AWSCNEndpointSuffix) || len(parts) != 7 || parts[2] != RedshiftServiceName {
|
|
return "", "", trace.BadParameter("failed to parse %v as Redshift CN endpoint", endpoint)
|
|
}
|
|
return parts[0], parts[3], nil
|
|
}
|
|
|
|
// RedshiftServerlessEndpointDetails contains information about an Redshift
|
|
// Serverless endpoint.
|
|
type RedshiftServerlessEndpointDetails struct {
|
|
// WorkgroupName is the name of the workgroup.
|
|
WorkgroupName string
|
|
// EndpointName is the name of the VPC endpoint.
|
|
EndpointName string
|
|
// AccountID is the AWS Account ID.
|
|
AccountID string
|
|
// Region is the AWS region the database resides in.
|
|
Region string
|
|
}
|
|
|
|
// ParseRedshiftServerlessEndpoint extracts name, AWS Account ID, and region
|
|
// from the provided Redshift Serverless endpoint.
|
|
func ParseRedshiftServerlessEndpoint(endpoint string) (details *RedshiftServerlessEndpointDetails, err error) {
|
|
if strings.ContainsRune(endpoint, ':') {
|
|
endpoint, _, err = net.SplitHostPort(endpoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if strings.HasSuffix(endpoint, AWSCNEndpointSuffix) {
|
|
// TODO(greedy52) add AWS China support when Redshift Serverless come to those regions.
|
|
return nil, trace.NotImplemented("failed to parse %v as Redshift Serverless endpoint: AWS China regions are not supported yet", endpoint)
|
|
}
|
|
return parseRedshiftServerlessEndpoint(endpoint)
|
|
}
|
|
|
|
// parseRedshiftServerlessEndpoint extracts name, AWS account ID, and region
|
|
// from the provided Redshift Serverless endpoint for standard regions.
|
|
//
|
|
// Workgroup endpoint looks like this:
|
|
// <workgroup-name>.<account-id>.<region>.redshift-serverless.amazonaws.com
|
|
//
|
|
// VPC endpoint looks like this:
|
|
// <vpc-endpoint-name>-endpoint-<some-hash>.<account-id>.<region>.redshift-serverless.amazonaws.com
|
|
func parseRedshiftServerlessEndpoint(endpoint string) (*RedshiftServerlessEndpointDetails, error) {
|
|
parts := strings.Split(endpoint, ".")
|
|
if !strings.HasSuffix(endpoint, AWSEndpointSuffix) || len(parts) != 6 || parts[3] != RedshiftServerlessServiceName {
|
|
return nil, trace.BadParameter("failed to parse %v as Redshift Serverless endpoint", endpoint)
|
|
}
|
|
if endpointName, _, found := strings.Cut(parts[0], "-endpoint-"); found {
|
|
return &RedshiftServerlessEndpointDetails{
|
|
EndpointName: endpointName,
|
|
AccountID: parts[1],
|
|
Region: parts[2],
|
|
}, nil
|
|
}
|
|
|
|
return &RedshiftServerlessEndpointDetails{
|
|
WorkgroupName: parts[0],
|
|
AccountID: parts[1],
|
|
Region: parts[2],
|
|
}, nil
|
|
}
|
|
|
|
// RedisEndpointInfo describes details extracted from a ElastiCache or MemoryDB
|
|
// Redis endpoint.
|
|
type RedisEndpointInfo struct {
|
|
// ID is the identifier of the endpoint.
|
|
ID string
|
|
// Region is the AWS region for the endpoint.
|
|
Region string
|
|
// TransitEncryptionEnabled specifies if in-transit encryption (TLS) is
|
|
// enabled.
|
|
TransitEncryptionEnabled bool
|
|
// EndpointType specifies the type of the endpoint.
|
|
EndpointType string
|
|
}
|
|
|
|
const (
|
|
// ElastiCacheConfigurationEndpoint is the configuration endpoint that used
|
|
// for cluster mode connection.
|
|
ElastiCacheConfigurationEndpoint = "configuration"
|
|
// ElastiCachePrimaryEndpoint is the endpoint of the primary node in the
|
|
// node group.
|
|
ElastiCachePrimaryEndpoint = "primary"
|
|
// ElastiCacheReaderEndpoint is the endpoint of the replica nodes in the
|
|
// node group.
|
|
ElastiCacheReaderEndpoint = "reader"
|
|
// ElastiCacheNodeEndpoint is the endpoint that used to connect to an
|
|
// individual node.
|
|
ElastiCacheNodeEndpoint = "node"
|
|
|
|
// MemoryDBClusterEndpoint is the cluster configuration endpoint for a
|
|
// MemoryDB cluster.
|
|
MemoryDBClusterEndpoint = "cluster"
|
|
// MemoryDBNodeEndpoint is the endpoint of an individual MemoryDB node.
|
|
MemoryDBNodeEndpoint = "node"
|
|
|
|
// OpenSearchDefaultEndpoint is the default endpoint for domain.
|
|
OpenSearchDefaultEndpoint = "default"
|
|
// OpenSearchCustomEndpoint is the custom endpoint configured for domain.
|
|
OpenSearchCustomEndpoint = "custom"
|
|
// OpenSearchVPCEndpoint is the VPC endpoint for domain.
|
|
OpenSearchVPCEndpoint = "vpc"
|
|
|
|
// RDSEndpointTypePrimary is the endpoint that specifies the connection for
|
|
// the primary instance of the RDS cluster.
|
|
RDSEndpointTypePrimary = "primary"
|
|
// RDSEndpointTypeReader is the endpoint that load-balances connections
|
|
// across the Aurora Replicas that are available in an RDS cluster.
|
|
RDSEndpointTypeReader = "reader"
|
|
// RDSEndpointTypeCustom is the endpoint that specifies one of the custom
|
|
// endpoints associated with the RDS cluster.
|
|
RDSEndpointTypeCustom = "custom"
|
|
// RDSEndpointTypeInstance is the endpoint of an RDS DB instance.
|
|
RDSEndpointTypeInstance = "instance"
|
|
)
|
|
|
|
// ParseElastiCacheEndpoint extracts the details from the provided
|
|
// ElastiCache Redis endpoint.
|
|
//
|
|
// https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/GettingStarted.ConnectToCacheNode.html
|
|
func ParseElastiCacheEndpoint(endpoint string) (*RedisEndpointInfo, error) {
|
|
endpoint, err := removeSchemaAndPort(endpoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Remove partition suffix. Note that endpoints for CN regions use the same
|
|
// format except they end with AWSCNEndpointSuffix.
|
|
endpointWithoutSuffix, _, err := removePartitionSuffix(endpoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Split into parts to extract details. They look like this in general:
|
|
// <part>.<part>.<part>.<short-region>.cache
|
|
//
|
|
// Note that ElastiCache uses short region codes like "use1".
|
|
//
|
|
// For Redis with cluster mode enabled, users can connect through either
|
|
// "configuration" endpoint or individual "node" endpoints.
|
|
// For Redis with cluster mode disabled, users can connect through either
|
|
// "primary", "reader", or individual "node" endpoints.
|
|
parts := strings.Split(endpointWithoutSuffix, ".")
|
|
if len(parts) == 5 && parts[4] == ElastiCacheServiceName {
|
|
region, ok := ShortRegionToRegion(parts[3])
|
|
if !ok {
|
|
return nil, trace.BadParameter("%v is not a valid region", parts[3])
|
|
}
|
|
|
|
// Configuration endpoint for Redis with TLS enabled looks like:
|
|
// clustercfg.my-redis-shards.xxxxxx.use1.cache.<suffix>:6379
|
|
if parts[0] == "clustercfg" {
|
|
return &RedisEndpointInfo{
|
|
ID: parts[1],
|
|
Region: region,
|
|
TransitEncryptionEnabled: true,
|
|
EndpointType: ElastiCacheConfigurationEndpoint,
|
|
}, nil
|
|
}
|
|
|
|
// Configuration endpoint for Redis with TLS disabled looks like:
|
|
// my-redis-shards.xxxxxx.clustercfg.use1.cache.<suffix>:6379
|
|
if parts[2] == "clustercfg" {
|
|
return &RedisEndpointInfo{
|
|
ID: parts[0],
|
|
Region: region,
|
|
TransitEncryptionEnabled: false,
|
|
EndpointType: ElastiCacheConfigurationEndpoint,
|
|
}, nil
|
|
}
|
|
|
|
// Node endpoint for Redis with TLS disabled looks like:
|
|
// my-redis-cluster-001.xxxxxx.0001.use0.cache.<suffix>:6379
|
|
// my-redis-shards-0001-001.xxxxxx.0001.use0.cache.<suffix>:6379
|
|
if isElasticCacheShardID(parts[2]) {
|
|
return &RedisEndpointInfo{
|
|
ID: trimElastiCacheShardAndNodeID(parts[0]),
|
|
Region: region,
|
|
TransitEncryptionEnabled: false,
|
|
EndpointType: ElastiCacheNodeEndpoint,
|
|
}, nil
|
|
}
|
|
|
|
// Node, primary, reader endpoints for Redis with TLS enabled look like:
|
|
// my-redis-cluster-001.my-redis-cluster.xxxxxx.use1.cache.<suffix>:6379
|
|
// my-redis-shards-0001-001.my-redis-shards.xxxxxx.use1.cache.<suffix>:6379
|
|
// master.my-redis-cluster.xxxxxx.use1.cache.<suffix>:6379
|
|
// replica.my-redis-cluster.xxxxxx.use1.cache.<suffix>:6379
|
|
var endpointType string
|
|
switch strings.ToLower(parts[0]) {
|
|
case "master":
|
|
endpointType = ElastiCachePrimaryEndpoint
|
|
case "replica":
|
|
endpointType = ElastiCacheReaderEndpoint
|
|
default:
|
|
endpointType = ElastiCacheNodeEndpoint
|
|
}
|
|
return &RedisEndpointInfo{
|
|
ID: parts[1],
|
|
Region: region,
|
|
TransitEncryptionEnabled: true,
|
|
EndpointType: endpointType,
|
|
}, nil
|
|
}
|
|
|
|
// Primary and reader endpoints for Redis with TLS disabled have an extra
|
|
// shard ID in the endpoints, and they look like:
|
|
// my-redis-cluster.xxxxxx.ng.0001.use1.cache.<suffix>:6379
|
|
// my-redis-cluster-ro.xxxxxx.ng.0001.use1.cache.<suffix>:6379
|
|
if len(parts) == 6 && parts[5] == ElastiCacheServiceName && isElasticCacheShardID(parts[3]) {
|
|
region, ok := ShortRegionToRegion(parts[4])
|
|
if !ok {
|
|
return nil, trace.BadParameter("%v is not a valid region", parts[4])
|
|
}
|
|
|
|
// Remove "-ro" from reader endpoint.
|
|
if strings.HasSuffix(parts[0], "-ro") {
|
|
return &RedisEndpointInfo{
|
|
ID: strings.TrimSuffix(parts[0], "-ro"),
|
|
Region: region,
|
|
TransitEncryptionEnabled: false,
|
|
EndpointType: ElastiCacheReaderEndpoint,
|
|
}, nil
|
|
}
|
|
|
|
return &RedisEndpointInfo{
|
|
ID: parts[0],
|
|
Region: region,
|
|
TransitEncryptionEnabled: false,
|
|
EndpointType: ElastiCachePrimaryEndpoint,
|
|
}, nil
|
|
}
|
|
|
|
return nil, trace.BadParameter("unknown ElastiCache Redis endpoint format %q", endpoint)
|
|
}
|
|
|
|
// isElasticCacheShardID returns true if the input part is in shard ID format.
|
|
// The shard ID is a 4 character string of an integer left padded with zeros
|
|
// (e.g. 0001).
|
|
func isElasticCacheShardID(part string) bool {
|
|
if len(part) != 4 {
|
|
return false
|
|
}
|
|
_, err := strconv.Atoi(part)
|
|
return err == nil
|
|
}
|
|
|
|
// isElasticCacheNodeID returns true if the input part is in node ID format.
|
|
// The node ID is a 3 character string of an integer left padded with zeros
|
|
// (e.g. 001).
|
|
func isElasticCacheNodeID(part string) bool {
|
|
if len(part) != 3 {
|
|
return false
|
|
}
|
|
_, err := strconv.Atoi(part)
|
|
return err == nil
|
|
}
|
|
|
|
// trimElastiCacheShardAndNodeID trims shard and node ID suffix from input.
|
|
func trimElastiCacheShardAndNodeID(input string) string {
|
|
// input can be one of:
|
|
// <replication-group-id>
|
|
// <replication-group-id>-<node-id>
|
|
// <replication-group-id>-<shard-id>-<node-id>
|
|
parts := strings.Split(input, "-")
|
|
if len(parts) > 0 {
|
|
if isElasticCacheNodeID(parts[len(parts)-1]) {
|
|
parts = parts[:len(parts)-1]
|
|
}
|
|
}
|
|
if len(parts) > 0 {
|
|
if isElasticCacheShardID(parts[len(parts)-1]) {
|
|
parts = parts[:len(parts)-1]
|
|
}
|
|
}
|
|
return strings.Join(parts, "-")
|
|
}
|
|
|
|
// ParseMemoryDBEndpoint extracts the details from the provided
|
|
// MemoryDB endpoint.
|
|
//
|
|
// https://docs.aws.amazon.com/memorydb/latest/devguide/endpoints.html
|
|
func ParseMemoryDBEndpoint(endpoint string) (*RedisEndpointInfo, error) {
|
|
endpoint, err := removeSchemaAndPort(endpoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Here is a sample endpoint for MemoryDB:
|
|
// clustercfg.my-memorydb.scwzlu.memorydb.ca-central-1.amazonaws.com
|
|
//
|
|
// Unlike RDS/Redshift endpoints, the service subdomain is before region.
|
|
// Unlike ElastiCache endpoints, MemoryDB uses full region name.
|
|
endpointWithoutSuffix, _, err := removePartitionSuffix(endpoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
parts := strings.Split(endpointWithoutSuffix, ".")
|
|
if len(parts) != 5 || parts[3] != MemoryDBSServiceName {
|
|
return nil, trace.BadParameter("unknown MemoryDB endpoint format")
|
|
}
|
|
|
|
switch {
|
|
// TLS disabled cluster endpoints look like this:
|
|
// <cluster-name>.<xxxx>.clustercfg.memorydb.<region>.<suffix>
|
|
case parts[2] == "clustercfg":
|
|
return &RedisEndpointInfo{
|
|
ID: parts[0],
|
|
Region: parts[4],
|
|
TransitEncryptionEnabled: false,
|
|
EndpointType: MemoryDBClusterEndpoint,
|
|
}, nil
|
|
|
|
// TLS enabled cluster endpoints look like this:
|
|
// clustercfg.<cluster-name>.<xxxx>.memorydb.<region>.<suffix>
|
|
case parts[0] == "clustercfg":
|
|
return &RedisEndpointInfo{
|
|
ID: parts[1],
|
|
Region: parts[4],
|
|
TransitEncryptionEnabled: true,
|
|
EndpointType: MemoryDBClusterEndpoint,
|
|
}, nil
|
|
|
|
// TLS disabled node endpoints look like this:
|
|
// <cluster-name>-<shard-id>-<node-id>.<xxxx>.<shard-id>.memorydb.<region>.<suffix>
|
|
//
|
|
// MemoryDB and ElastiCache share same shard/node ID format.
|
|
case isElasticCacheShardID(parts[2]):
|
|
return &RedisEndpointInfo{
|
|
ID: trimElastiCacheShardAndNodeID(parts[0]),
|
|
Region: parts[4],
|
|
TransitEncryptionEnabled: false,
|
|
EndpointType: MemoryDBNodeEndpoint,
|
|
}, nil
|
|
|
|
// TLS enabled node endpoints look like this:
|
|
// <cluster-name>-<shard-id>-<node-id>.<cluster-name>.<xxxx>.memorydb.<region>.<suffix>
|
|
default:
|
|
return &RedisEndpointInfo{
|
|
ID: trimElastiCacheShardAndNodeID(parts[0]),
|
|
Region: parts[4],
|
|
TransitEncryptionEnabled: true,
|
|
EndpointType: MemoryDBNodeEndpoint,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// isAWSServiceEndpoint returns true if uri is a valid AWS endpoint and uri
|
|
// contains the provided service name as a subdomain.
|
|
func isAWSServiceEndpoint(uri, serviceName string) bool {
|
|
return strings.Contains(uri, fmt.Sprintf(".%s.", serviceName)) &&
|
|
IsAWSEndpoint(uri)
|
|
}
|
|
|
|
func removeSchemaAndPort(endpoint string) (string, error) {
|
|
// Add a temporary schema to make a valid URL for url.Parse.
|
|
if !strings.Contains(endpoint, "://") {
|
|
endpoint = "schema://" + endpoint
|
|
}
|
|
|
|
parsedURL, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
|
|
return parsedURL.Hostname(), nil
|
|
}
|
|
|
|
func removePartitionSuffix(endpoint string) (string, string, error) {
|
|
switch {
|
|
case strings.HasSuffix(endpoint, AWSEndpointSuffix):
|
|
return strings.TrimSuffix(endpoint, AWSEndpointSuffix), AWSEndpointSuffix, nil
|
|
|
|
case strings.HasSuffix(endpoint, AWSCNEndpointSuffix):
|
|
return strings.TrimSuffix(endpoint, AWSCNEndpointSuffix), AWSCNEndpointSuffix, nil
|
|
|
|
default:
|
|
return "", "", trace.BadParameter("%v is not a valid AWS endpoint", endpoint)
|
|
}
|
|
}
|
|
|
|
const (
|
|
// AWSEndpointSuffix is the endpoint suffix for AWS Standard and AWS US
|
|
// GovCloud regions.
|
|
//
|
|
// https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints
|
|
// https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/using-govcloud-endpoints.html
|
|
AWSEndpointSuffix = ".amazonaws.com"
|
|
|
|
// AWSCNEndpointSuffix is the endpoint suffix for AWS China regions.
|
|
//
|
|
// https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-arns.html
|
|
AWSCNEndpointSuffix = ".amazonaws.com.cn"
|
|
|
|
// RDSServiceName is the service name for AWS RDS.
|
|
RDSServiceName = "rds"
|
|
|
|
// RedshiftServiceName is the service name for AWS Redshift.
|
|
RedshiftServiceName = "redshift"
|
|
|
|
// RedshiftServerlessServiceName is the service name for AWS Redshift Serverless.
|
|
RedshiftServerlessServiceName = "redshift-serverless"
|
|
|
|
// ElastiCacheServiceName is the service name for AWS ElastiCache.
|
|
ElastiCacheServiceName = "cache"
|
|
|
|
// MemoryDBSServiceName is the service name for AWS MemoryDB.
|
|
MemoryDBSServiceName = "memorydb"
|
|
|
|
// DynamoDBServiceName is the service name for AWS DynamoDB.
|
|
DynamoDBServiceName = "dynamodb"
|
|
// DynamoDBFipsServiceName is the fips variant service name for AWS DynamoDB.
|
|
DynamoDBFipsServiceName = "dynamodb-fips"
|
|
// DynamoDBStreamsServiceName is the AWS DynamoDB Streams service name.
|
|
DynamoDBStreamsServiceName = "streams.dynamodb"
|
|
// DAXServiceName is the AWS DynamoDB Accelerator service name.
|
|
DAXServiceName = "dax"
|
|
|
|
// OpenSearchServiceName is the AWS OpenSearch service name.
|
|
OpenSearchServiceName = "es"
|
|
)
|
|
|
|
// CassandraEndpointURLForRegion returns a Cassandra endpoint based on the provided region.
|
|
// https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html
|
|
func CassandraEndpointURLForRegion(region string) string {
|
|
if IsCNRegion(region) {
|
|
return fmt.Sprintf("cassandra.%s%s:9142", region, AWSCNEndpointSuffix)
|
|
}
|
|
return fmt.Sprintf("cassandra.%s%s:9142", region, AWSEndpointSuffix)
|
|
}
|
|
|
|
// CassandraEndpointRegion returns an AWS region from cassandra endpoint:
|
|
// where endpoint looks like cassandra.us-east-2.amazonaws.com
|
|
// https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.endpoints.html
|
|
func CassandraEndpointRegion(endpoint string) (string, error) {
|
|
parts, _, err := extractAWSEndpointParts(endpoint)
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
if len(parts) != 2 {
|
|
return "", trace.BadParameter("invalid Cassandra endpoint")
|
|
}
|
|
return parts[1], nil
|
|
}
|
|
|
|
// DynamoDBEndpointInfo describes info extracted from a DynamoDB endpoint.
|
|
type DynamoDBEndpointInfo struct {
|
|
// Service is the service subdomain of the endpoint, for example "dynamodb" or "dax".
|
|
Service string
|
|
// Region is the AWS region for the endpoint, for example "us-west-1".
|
|
Region string
|
|
// Partition is the AWS partition for the endpoint, for example ".amazonaws.com"
|
|
Partition string
|
|
}
|
|
|
|
// ParseDynamoDBEndpoint parses and extract info from the provided DynamoDB endpoint.
|
|
func ParseDynamoDBEndpoint(endpoint string) (*DynamoDBEndpointInfo, error) {
|
|
endpoint = strings.ToLower(endpoint)
|
|
parts, partition, err := extractAWSEndpointParts(endpoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
switch len(parts) {
|
|
case 2, 3:
|
|
default:
|
|
return nil, trace.BadParameter("invalid DynamoDB endpoint %q", endpoint)
|
|
}
|
|
info := &DynamoDBEndpointInfo{
|
|
Service: strings.Join(parts[:len(parts)-1], "."),
|
|
Region: parts[len(parts)-1],
|
|
Partition: partition,
|
|
}
|
|
|
|
// check for recognized service name.
|
|
switch info.Service {
|
|
case DynamoDBServiceName, DynamoDBFipsServiceName,
|
|
DynamoDBStreamsServiceName, DAXServiceName:
|
|
default:
|
|
return nil, trace.BadParameter("invalid DynamoDB endpoint %q", endpoint)
|
|
}
|
|
|
|
// check that the partition is valid for the region.
|
|
if info.Region == "" || info.Partition == "" {
|
|
return nil, trace.BadParameter("invalid DynamoDB endpoint %q", endpoint)
|
|
}
|
|
switch {
|
|
case info.Partition == AWSCNEndpointSuffix && IsCNRegion(info.Region):
|
|
case info.Partition == AWSEndpointSuffix && !IsCNRegion(info.Region):
|
|
default:
|
|
return nil, trace.BadParameter("invalid AWS region %q for AWS partition %q",
|
|
info.Region, info.Partition)
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
// OpenSearchEndpointInfo describes info extracted from an AWS endpoint.
|
|
type OpenSearchEndpointInfo struct {
|
|
// Service is the service subdomain of the endpoint. Only "es" allowed for now.
|
|
Service string
|
|
// Region is the AWS region for the endpoint, for example "us-west-1".
|
|
Region string
|
|
// Partition is the AWS partition for the endpoint, for example ".amazonaws.com"
|
|
Partition string
|
|
}
|
|
|
|
// ParseOpensearchEndpoint parses and extract info from the provided OpenSearch endpoint.
|
|
func ParseOpensearchEndpoint(endpoint string) (*OpenSearchEndpointInfo, error) {
|
|
endpoint = strings.ToLower(endpoint)
|
|
parts, partition, err := extractAWSEndpointParts(endpoint)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
if len(parts) != 3 {
|
|
return nil, trace.BadParameter("invalid OpenSearch endpoint %q, wrong number of parts %v", endpoint, len(parts))
|
|
}
|
|
|
|
info := &OpenSearchEndpointInfo{
|
|
Region: parts[len(parts)-2],
|
|
Service: parts[len(parts)-1],
|
|
Partition: partition,
|
|
}
|
|
|
|
// check for recognized service name.
|
|
if info.Service != OpenSearchServiceName {
|
|
return nil, trace.BadParameter("invalid OpenSearch endpoint %q, invalid service %q", endpoint, info.Service)
|
|
}
|
|
|
|
// check that the partition is valid for the region.
|
|
switch {
|
|
case info.Region == "" || info.Partition == "":
|
|
return nil, trace.BadParameter("invalid OpenSearch endpoint %q, empty partition and region", endpoint)
|
|
case info.Region == "":
|
|
return nil, trace.BadParameter("invalid OpenSearch endpoint %q, empty region", endpoint)
|
|
case info.Partition == "":
|
|
return nil, trace.BadParameter("invalid OpenSearch endpoint %q, empty partition", endpoint)
|
|
}
|
|
|
|
switch {
|
|
case info.Partition == AWSCNEndpointSuffix && IsCNRegion(info.Region):
|
|
case info.Partition == AWSEndpointSuffix && !IsCNRegion(info.Region):
|
|
default:
|
|
return nil, trace.BadParameter("invalid AWS region %q for AWS partition %q",
|
|
info.Region, info.Partition)
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
// DynamoDBURIForRegion constructs a DynamoDB URI based on the AWS region.
|
|
// The URI uses a custom schema aws:// to differentiate an auto-generated URI from
|
|
// a user-configured URI in the engine.
|
|
// When the Teleport DynamoDB engine sees this custom URI schema, it will resolve
|
|
// the real endpoint using the request API target.
|
|
// https://docs.aws.amazon.com/general/latest/gr/ddb.html
|
|
func DynamoDBURIForRegion(region string) string {
|
|
var suffix string
|
|
if IsCNRegion(region) {
|
|
suffix = AWSCNEndpointSuffix
|
|
} else {
|
|
suffix = AWSEndpointSuffix
|
|
}
|
|
return fmt.Sprintf("aws://dynamodb.%s%s", region, suffix)
|
|
}
|
|
|
|
// extractAWSEndpointParts strips the schema, port, and AWS suffix,
|
|
// then splits the prefix by subdomain separator (".") and returns the parts and suffix.
|
|
func extractAWSEndpointParts(endpoint string) ([]string, string, error) {
|
|
uri, err := removeSchemaAndPort(endpoint)
|
|
if err != nil {
|
|
return nil, "", trace.Wrap(err)
|
|
}
|
|
prefix, suffix, err := removePartitionSuffix(uri)
|
|
if err != nil {
|
|
return nil, "", trace.Wrap(err)
|
|
}
|
|
return strings.Split(prefix, "."), suffix, nil
|
|
}
|