Files
teleport/api/utils/aws/endpoint_test.go
T
Gavin Frazar 5bb0ef15db add dynamodb database access (#18843)
* Add a new db engine
* Add tests for new engine
* Update tsh db subcommands
* Refactor error message and suggestions for unsupported tsh commands
* Add dynamodb to test plan
* Add AWS external ID to db config and update protos
2022-12-30 18:22:30 +00:00

641 lines
18 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"
"testing"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
)
func TestParseRDSEndpoint(t *testing.T) {
tests := []struct {
name string
endpoint string
expectIsRDSEndpoint bool
expectDetails *RDSEndpointDetails
expectParseErrorIs func(error) bool
}{
{
name: "RDS instance",
endpoint: "aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com:5432",
expectIsRDSEndpoint: true,
expectDetails: &RDSEndpointDetails{
InstanceID: "aurora-instance-1",
Region: "us-west-1",
},
},
{
name: "RDS instance in cn-north-1",
endpoint: "aurora-instance-2.abcdefghijklmnop.rds.cn-north-1.amazonaws.com.cn",
expectIsRDSEndpoint: true,
expectDetails: &RDSEndpointDetails{
InstanceID: "aurora-instance-2",
Region: "cn-north-1",
},
},
{
name: "RDS cluster",
endpoint: "my-cluster.cluster-abcdefghijklmnop.us-west-1.rds.amazonaws.com:5432",
expectIsRDSEndpoint: true,
expectDetails: &RDSEndpointDetails{
ClusterID: "my-cluster",
Region: "us-west-1",
},
},
{
name: "RDS cluster custom endpoint",
endpoint: "my-custom.cluster-custom-abcdefghijklmnop.us-west-1.rds.amazonaws.com:5432",
expectIsRDSEndpoint: true,
expectDetails: &RDSEndpointDetails{
ClusterCustomEndpointName: "my-custom",
Region: "us-west-1",
},
},
{
name: "RDS proxy",
endpoint: "my-proxy.proxy-abcdefghijklmnop.us-west-1.rds.amazonaws.com:5432",
expectIsRDSEndpoint: true,
expectDetails: &RDSEndpointDetails{
ProxyName: "my-proxy",
Region: "us-west-1",
},
},
{
name: "RDS proxy custom endpoint",
endpoint: "my-custom.endpoint.proxy-abcdefghijklmnop.us-west-1.rds.amazonaws.com:5432",
expectIsRDSEndpoint: true,
expectDetails: &RDSEndpointDetails{
ProxyCustomEndpointName: "my-custom",
Region: "us-west-1",
},
},
{
name: "localhost:5432",
endpoint: "localhost",
expectIsRDSEndpoint: false,
expectParseErrorIs: trace.IsBadParameter,
},
{
name: "Redshift endpoint fails",
endpoint: "redshift-cluster-1.abcdefghijklmnop.us-east-1.redshift.amazonaws.com",
expectIsRDSEndpoint: false,
expectParseErrorIs: trace.IsBadParameter,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, test.expectIsRDSEndpoint, IsRDSEndpoint(test.endpoint))
actualDetails, err := ParseRDSEndpoint(test.endpoint)
if test.expectParseErrorIs != nil {
require.Error(t, err)
require.True(t, test.expectParseErrorIs(err))
} else {
require.NoError(t, err)
require.Equal(t, test.expectDetails, actualDetails)
}
})
}
}
func TestParseRedshiftEndpoint(t *testing.T) {
tests := []struct {
name string
endpoint string
expectIsRedshiftEndpoint bool
expectClusterID string
expectRegion string
expectParseErrorIs func(error) bool
}{
{
name: "standard",
endpoint: "redshift-cluster-1.abcdefghijklmnop.us-east-1.redshift.amazonaws.com:5432",
expectClusterID: "redshift-cluster-1",
expectRegion: "us-east-1",
expectIsRedshiftEndpoint: true,
},
{
name: "cn-north-1",
endpoint: "redshift-cluster-2.abcdefghijklmnop.redshift.cn-north-1.amazonaws.com.cn",
expectClusterID: "redshift-cluster-2",
expectRegion: "cn-north-1",
expectIsRedshiftEndpoint: true,
},
{
name: "localhost:5432",
endpoint: "localhost",
expectIsRedshiftEndpoint: false,
expectParseErrorIs: trace.IsBadParameter,
},
{
name: "RDS endpoint fails",
endpoint: "aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com",
expectIsRedshiftEndpoint: false,
expectParseErrorIs: trace.IsBadParameter,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, test.expectIsRedshiftEndpoint, IsRedshiftEndpoint(test.endpoint))
clusterID, region, err := ParseRedshiftEndpoint(test.endpoint)
if test.expectParseErrorIs != nil {
require.Error(t, err)
require.True(t, test.expectParseErrorIs(err))
} else {
require.NoError(t, err)
require.Equal(t, test.expectClusterID, clusterID)
require.Equal(t, test.expectRegion, region)
}
})
}
}
func TestParseElastiCacheEndpoint(t *testing.T) {
tests := []struct {
name string
inputURI string
expectInfo *RedisEndpointInfo
expectError bool
}{
{
name: "configuration endpoint, TLS enabled",
inputURI: "clustercfg.my-redis-shards.xxxxxx.use1.cache.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-redis-shards",
Region: "us-east-1",
TransitEncryptionEnabled: true,
EndpointType: ElastiCacheConfigurationEndpoint,
},
},
{
name: "primary endpoint, TLS enabled",
inputURI: "master.my-redis-cluster.xxxxxx.cac1.cache.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-redis-cluster",
Region: "ca-central-1",
TransitEncryptionEnabled: true,
EndpointType: ElastiCachePrimaryEndpoint,
},
},
{
name: "reader endpoint, TLS enabled",
inputURI: "replica.my-redis-cluster.xxxxxx.cac1.cache.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-redis-cluster",
Region: "ca-central-1",
TransitEncryptionEnabled: true,
EndpointType: ElastiCacheReaderEndpoint,
},
},
{
name: "node endpoint, TLS enabled",
inputURI: "my-redis-shards-0002-001.my-redis-shards.xxxxxx.cac1.cache.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-redis-shards",
Region: "ca-central-1",
TransitEncryptionEnabled: true,
EndpointType: ElastiCacheNodeEndpoint,
},
},
{
name: "configuration endpoint, TLS disabled",
inputURI: "my-redis-shards.xxxxxx.clustercfg.use1.cache.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-redis-shards",
Region: "us-east-1",
EndpointType: ElastiCacheConfigurationEndpoint,
},
},
{
name: "primary endpoint, TLS disabled",
inputURI: "my-redis-cluster.xxxxxx.ng.0001.cac1.cache.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-redis-cluster",
Region: "ca-central-1",
EndpointType: ElastiCachePrimaryEndpoint,
},
},
{
name: "reader endpoint, TLS disabled",
inputURI: "my-redis-cluster-ro.xxxxxx.ng.0001.cac1.cache.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-redis-cluster",
Region: "ca-central-1",
EndpointType: ElastiCacheReaderEndpoint,
},
},
{
name: "node endpoint, TLS disabled",
inputURI: "my-redis-shards-0001-001.xxxxxx.0001.cac1.cache.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-redis-shards",
Region: "ca-central-1",
EndpointType: ElastiCacheNodeEndpoint,
},
},
{
name: "CN endpoint",
inputURI: "replica.my-redis-cluster.xxxxxx.cnn1.cache.amazonaws.com.cn:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-redis-cluster",
Region: "cn-north-1",
TransitEncryptionEnabled: true,
EndpointType: ElastiCacheReaderEndpoint,
},
},
{
name: "endpoint with schema and parameters",
inputURI: "redis://my-redis-cluster.xxxxxx.ng.0001.cac1.cache.amazonaws.com:6379?a=b&c=d",
expectInfo: &RedisEndpointInfo{
ID: "my-redis-cluster",
Region: "ca-central-1",
EndpointType: ElastiCachePrimaryEndpoint,
},
},
{
name: "invalid suffix",
inputURI: "replica.my-redis-cluster.xxxxxx.cac1.cache.amazonaws.ca:6379",
expectError: true,
},
{
name: "invalid url",
inputURI: "://replica.my-redis-cluster.xxxxxx.cac1.cache.amazonaws.com:6379",
expectError: true,
},
{
name: "invalid format",
inputURI: "my-redis-cluster.cac1.cache.amazonaws.com:6379",
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualInfo, err := ParseElastiCacheEndpoint(test.inputURI)
if test.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, test.expectInfo, actualInfo)
}
})
}
}
func TestParseMemoryDBEndpoint(t *testing.T) {
t.Parallel()
tests := []struct {
name string
inputURI string
expectInfo *RedisEndpointInfo
expectError bool
}{
{
name: "TLS enabled cluster endpoint",
inputURI: "clustercfg.my-memorydb.xxxxxx.memorydb.us-east-1.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-memorydb",
Region: "us-east-1",
TransitEncryptionEnabled: true,
EndpointType: "cluster",
},
},
{
name: "TLS disabled cluster endpoint",
inputURI: "my-memorydb.xxxxxx.clustercfg.memorydb.us-east-1.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-memorydb",
Region: "us-east-1",
TransitEncryptionEnabled: false,
EndpointType: "cluster",
},
},
{
name: "TLS enabled node endpoint",
inputURI: "my-memorydb-0002-001.my-memorydb.xxxxxx.memorydb.us-east-1.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-memorydb",
Region: "us-east-1",
TransitEncryptionEnabled: true,
EndpointType: "node",
},
},
{
name: "TLS disabled node endpoint",
inputURI: "my-memorydb-0002-001.xxxxx.0002.memorydb.us-east-1.amazonaws.com:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-memorydb",
Region: "us-east-1",
TransitEncryptionEnabled: false,
EndpointType: "node",
},
},
{
name: "CN endpoint",
inputURI: "clustercfg.my-memorydb.xxxxxx.memorydb.cn-north-1.amazonaws.com.cn:6379",
expectInfo: &RedisEndpointInfo{
ID: "my-memorydb",
Region: "cn-north-1",
TransitEncryptionEnabled: true,
EndpointType: "cluster",
},
},
{
name: "endpoint with schema and parameters",
inputURI: "redis://clustercfg.my-memorydb.xxxxxx.memorydb.us-east-1.amazonaws.com:6379?a=b&c=d",
expectInfo: &RedisEndpointInfo{
ID: "my-memorydb",
Region: "us-east-1",
TransitEncryptionEnabled: true,
EndpointType: "cluster",
},
},
{
name: "invalid suffix",
inputURI: "clustercfg.my-memorydb.xxxxxx.memorydb.ca-central-1.amazonaws.ca:6379",
expectError: true,
},
{
name: "invalid url",
inputURI: "://clustercfg.my-memorydb.xxxxxx.memorydb.ca-central-1.amazonaws.com:6379",
expectError: true,
},
{
name: "invalid format",
inputURI: "unknown.format.memorydb.ca-central-1.amazonaws.com:6379",
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualInfo, err := ParseMemoryDBEndpoint(test.inputURI)
if test.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, test.expectInfo, actualInfo)
}
})
}
}
func TestCassandraEndpointRegion(t *testing.T) {
t.Parallel()
tests := []struct {
name string
inputURI string
wantRegion string
expectError bool
}{
{
name: "us-east-1",
inputURI: "cassandra.us-east-1.amazonaws.com",
wantRegion: "us-east-1",
expectError: false,
},
{
name: "cn-north-1.",
inputURI: "cassandra.cn-north-1.amazonaws.com.cn",
wantRegion: "cn-north-1",
expectError: false,
},
{
name: "us-gov-east-1",
inputURI: "cassandra.us-gov-east-1.amazonaws.com",
wantRegion: "us-gov-east-1",
expectError: false,
},
{
name: "invalid uri",
inputURI: "foo.cassandra.us-east-1.amazonaws.com",
wantRegion: "us-east-1",
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := CassandraEndpointRegion(test.inputURI)
if test.expectError {
require.Error(t, err)
require.False(t, IsKeyspacesEndpoint(test.inputURI))
} else {
require.NoError(t, err)
require.Equal(t, got, test.wantRegion)
require.True(t, IsKeyspacesEndpoint(test.inputURI))
}
})
}
}
func TestRedshiftServerlessEndpoint(t *testing.T) {
tests := []struct {
name string
endpoint string
expectIsRedshiftServerlessEndpoint bool
expectDetails *RedshiftServerlessEndpointDetails
}{
{
name: "workgroup endpoint",
endpoint: "my-workgroup.123456789012.us-east-1.redshift-serverless.amazonaws.com:5439",
expectIsRedshiftServerlessEndpoint: true,
expectDetails: &RedshiftServerlessEndpointDetails{
WorkgroupName: "my-workgroup",
AccountID: "123456789012",
Region: "us-east-1",
},
},
{
name: "vpc endpoint",
endpoint: "my-vpc-endpoint-xxxyyyzzz.123456789012.us-east-1.redshift-serverless.amazonaws.com",
expectIsRedshiftServerlessEndpoint: true,
expectDetails: &RedshiftServerlessEndpointDetails{
EndpointName: "my-vpc",
AccountID: "123456789012",
Region: "us-east-1",
},
},
{
name: "localhost:5432",
endpoint: "localhost",
expectIsRedshiftServerlessEndpoint: false,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, test.expectIsRedshiftServerlessEndpoint, IsRedshiftServerlessEndpoint(test.endpoint))
actualDetails, err := ParseRedshiftServerlessEndpoint(test.endpoint)
if !test.expectIsRedshiftServerlessEndpoint {
require.Error(t, err)
require.True(t, trace.IsBadParameter(err))
} else {
require.NoError(t, err)
require.Equal(t, test.expectDetails, actualDetails)
}
})
}
}
func TestDynamoDBURIForRegion(t *testing.T) {
t.Parallel()
tests := []struct {
desc string
region string
wantURI string
}{
{
desc: "region is in correct AWS partition",
region: "us-east-1",
wantURI: "aws://dynamodb.us-east-1.amazonaws.com",
},
{
desc: "china north region is in correct AWS partition",
region: "cn-north-1",
wantURI: "aws://dynamodb.cn-north-1.amazonaws.com.cn",
},
{
desc: "china northwest region is in correct AWS partition",
region: "cn-northwest-1",
wantURI: "aws://dynamodb.cn-northwest-1.amazonaws.com.cn",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
require.Equal(t, tt.wantURI, DynamoDBURIForRegion(tt.region))
})
}
}
func TestParseDynamoDBEndpoint(t *testing.T) {
t.Parallel()
t.Run("parses valid endpoint", func(t *testing.T) {
t.Parallel()
for _, parts := range []struct {
services []string
regions []string
partition string
}{
{
services: []string{DynamoDBServiceName, DynamoDBFipsServiceName, DynamoDBStreamsServiceName, DAXServiceName},
regions: []string{"us-east-1", "us-gov-east-1"},
partition: AWSEndpointSuffix,
},
{
services: []string{DynamoDBServiceName, DynamoDBStreamsServiceName, DAXServiceName},
regions: []string{"cn-north-1", "cn-northwest-1"},
partition: AWSCNEndpointSuffix,
},
} {
parts := parts
for _, svc := range parts.services {
svc := svc
for _, region := range parts.regions {
region := region
endpoint := fmt.Sprintf("%s.%s%s", svc, region, parts.partition)
t.Run(endpoint, func(t *testing.T) {
t.Parallel()
info, err := ParseDynamoDBEndpoint(endpoint)
require.NoError(t, err)
wantInfo := DynamoDBEndpointInfo{
Service: svc,
Region: region,
Partition: parts.partition,
}
require.NotNil(t, info)
require.Equal(t, wantInfo, *info)
})
}
}
}
})
tests := []struct {
desc string
services []string
regions []string
endpoint string
wantInfo *DynamoDBEndpointInfo
}{
{
desc: "empty uri",
endpoint: "",
},
{
desc: "not AWS uri",
endpoint: "localhost",
},
{
desc: "missing region",
endpoint: "amazonaws.com",
},
{
desc: "missing china region",
endpoint: "amazonaws.com.cn",
},
{
desc: "unrecognized service subdomain",
endpoint: "foo.us-east-1.amazonaws.com",
},
{
desc: "unrecognized dynamodb service subdomain",
endpoint: "foo.dynamodb.us-east-1.amazonaws.com",
},
{
desc: "unrecognized streams service subdomain",
endpoint: "streams.foo.us-east-1.amazonaws.com",
},
{
desc: "mismatched us region and china partition",
endpoint: "streams.dynamodb.us-east-1.amazonaws.com.cn",
},
{
desc: "mismatched china region and non-china partition",
endpoint: "streams.dynamodb.cn-north-1.amazonaws.com",
},
}
for _, tt := range tests {
tt := tt
t.Run("detects invalid endpoint with"+tt.desc, func(t *testing.T) {
t.Parallel()
info, err := ParseDynamoDBEndpoint(tt.endpoint)
require.Error(t, err, "endpoint %s should be invalid", tt.endpoint)
require.Nil(t, info)
})
}
}