diff --git a/api/utils/aws/identifiers.go b/api/utils/aws/identifiers.go index 330fb8319478f..efc093e4a702b 100644 --- a/api/utils/aws/identifiers.go +++ b/api/utils/aws/identifiers.go @@ -18,8 +18,6 @@ package aws import ( "regexp" - "strings" - "unicode" "github.com/gravitational/trace" ) @@ -40,9 +38,6 @@ func IsValidAccountID(accountID string) error { return nil } -// matchRoleName is a regex that matches against AWS IAM Role Names. -var matchRoleName = regexp.MustCompile(`^[\w+=,.@-]+$`).MatchString - // IsValidIAMRoleName checks whether the role name is a valid AWS IAM Role identifier. // // > Length Constraints: Minimum length of 1. Maximum length of 64. @@ -60,17 +55,26 @@ func IsValidIAMRoleName(roleName string) error { // It does not do a full validation, because AWS doesn't provide documentation for that. // However, they usually only have the following chars: [a-z0-9\-] func IsValidRegion(region string) error { - indexNotFound := -1 - - if len(region) == 0 { - return trace.BadParameter("region is invalid") - } - - if strings.IndexFunc(region, func(r rune) bool { - return !(unicode.IsDigit(r) || unicode.IsLetter(r) || r == '-') - }) == indexNotFound { + if matchRegion.MatchString(region) { return nil } - - return trace.BadParameter("region is invalid") + return trace.BadParameter("region %q is invalid", region) } + +var ( + // matchRoleName is a regex that matches against AWS IAM Role Names. + matchRoleName = regexp.MustCompile(`^[\w+=,.@-]+$`).MatchString + + // matchRegion is a regex that defines the format of AWS regions. + // + // The regex matches the following from left to right: + // - starts with 2 lower case letters that represents a geo region like a + // country code + // - optional -gov, -iso, -isob for corresponding partitions + // - a word that should be a direction like "east", "west", etc. + // - a number counter + // + // Reference: + // https://github.com/aws/aws-sdk-go-v2/blob/main/codegen/smithy-aws-go-codegen/src/main/resources/software/amazon/smithy/aws/go/codegen/endpoints.json + matchRegion = regexp.MustCompile(`^[a-z]{2}(-gov|-iso|-isob)?-\w+-\d+$`) +) diff --git a/api/utils/aws/identifiers_test.go b/api/utils/aws/identifiers_test.go index f5718beef88bd..452e36f54939c 100644 --- a/api/utils/aws/identifiers_test.go +++ b/api/utils/aws/identifiers_test.go @@ -153,6 +153,11 @@ func TestIsValidRegion(t *testing.T) { region: "us-gov-east-1", errCheck: require.NoError, }, + { + name: "valid format", + region: "xx-iso-somewhere-100", + errCheck: require.NoError, + }, { name: "empty", region: "", @@ -163,6 +168,11 @@ func TestIsValidRegion(t *testing.T) { region: "us@east-1", errCheck: isBadParamErrFn, }, + { + name: "invalid country code", + region: "xxx-east-1", + errCheck: isBadParamErrFn, + }, } { t.Run(tt.name, func(t *testing.T) { tt.errCheck(t, IsValidRegion(tt.region)) diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 275681758a5d0..42c02ab030f1e 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -61,6 +61,7 @@ import ( "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" + awsutils "github.com/gravitational/teleport/lib/utils/aws" ) // CommandLineFlags stores command line flag values, it's a much simplified subset @@ -1332,6 +1333,17 @@ func applyDiscoveryConfig(fc *FileConfig, cfg *servicecfg.Config) error { return trace.Wrap(err) } + for _, region := range matcher.Regions { + if !awsutils.IsKnownRegion(region) { + log.Warnf("AWS matcher uses unknown region %q. "+ + "There could be a typo in %q. "+ + "Ignore this message if this is a new AWS region that is unknown to the AWS SDK used to compile this binary. "+ + "Known regions are: %v.", + region, region, awsutils.GetKnownRegions(), + ) + } + } + cfg.Discovery.AWSMatchers = append(cfg.Discovery.AWSMatchers, types.AWSMatcher{ Types: matcher.Types, diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index a362fef25d6c2..7a0eb21c9d7eb 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -30,13 +30,11 @@ import ( "strings" "time" - "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/coreos/go-oidc/oauth2" "github.com/gravitational/trace" log "github.com/sirupsen/logrus" "golang.org/x/crypto/acme" "golang.org/x/crypto/ssh" - "golang.org/x/exp/maps" "golang.org/x/exp/slices" "gopkg.in/yaml.v2" @@ -45,6 +43,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/installers" apiutils "github.com/gravitational/teleport/api/utils" + awsapiutils "github.com/gravitational/teleport/api/utils/aws" "github.com/gravitational/teleport/api/utils/tlsutils" "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/client" @@ -494,21 +493,9 @@ kubernetes matchers are present`) return nil } -// awsRegions returns the list of all regions available on every aws partition. -// It is used to validate the AWSMatcher.Regions values. -func awsRegions() []string { - var regions []string - partitions := endpoints.DefaultPartitions() - for _, partition := range partitions { - regions = append(regions, maps.Keys(partition.Regions())...) - } - return regions -} - // checkAndSetDefaultsForAWSMatchers sets the default values for discovery AWS matchers // and validates the provided types. func checkAndSetDefaultsForAWSMatchers(matcherInput []AWSMatcher) error { - regions := awsRegions() for i := range matcherInput { matcher := &matcherInput[i] for _, matcherType := range matcher.Types { @@ -520,13 +507,13 @@ func checkAndSetDefaultsForAWSMatchers(matcherInput []AWSMatcher) error { if len(matcher.Regions) == 0 { return trace.BadParameter("discovery service requires at least one region; supported regions are: %v", - regions) + awsutils.GetKnownRegions()) } for _, region := range matcher.Regions { - if !slices.Contains(regions, region) { + if err := awsapiutils.IsValidRegion(region); err != nil { return trace.BadParameter("discovery service does not support region %q; supported regions are: %v", - region, regions) + region, awsutils.GetKnownRegions()) } } diff --git a/lib/utils/aws/region.go b/lib/utils/aws/region.go new file mode 100644 index 0000000000000..01f7615e5a120 --- /dev/null +++ b/lib/utils/aws/region.go @@ -0,0 +1,50 @@ +/* +Copyright 2023 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 ( + "sync" + + "github.com/aws/aws-sdk-go/aws/endpoints" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// IsKnownRegion returns true if provided region is one of the "well-known" +// AWS regions. +func IsKnownRegion(region string) bool { + return slices.Contains(GetKnownRegions(), region) +} + +// GetKnownRegions returns a list of "well-known" AWS regions generated from +// AWS SDK. +func GetKnownRegions() []string { + knownRegionsOnce.Do(func() { + var regions []string + partitions := endpoints.DefaultPartitions() + for _, partition := range partitions { + regions = append(regions, maps.Keys(partition.Regions())...) + } + knownRegions = regions + }) + return knownRegions +} + +var ( + knownRegions []string + knownRegionsOnce sync.Once +) diff --git a/lib/utils/aws/region_test.go b/lib/utils/aws/region_test.go new file mode 100644 index 0000000000000..e7dac2c51fc2e --- /dev/null +++ b/lib/utils/aws/region_test.go @@ -0,0 +1,62 @@ +/* +Copyright 2023 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 ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/utils/aws" +) + +func TestGetKnownRegions(t *testing.T) { + // Picked a few regions just to make sure GetKnownRegions is returning + // something that includes these. + t.Run("hand picked", func(t *testing.T) { + for _, region := range []string{ + "us-east-1", + "il-central-1", + "cn-north-1", + "us-gov-west-1", + "us-isob-east-1", + } { + require.Contains(t, GetKnownRegions(), region) + } + }) + + // Ideally this should be tested in api/utils/aws but api has no access + // to AWS SDK. If this fails aws.IsValidRegion should be updated. + t.Run("IsValidRegion", func(t *testing.T) { + for _, region := range GetKnownRegions() { + require.NoError(t, aws.IsValidRegion(region)) + } + }) + +} +func TestIsKnownRegion(t *testing.T) { + for _, region := range GetKnownRegions() { + require.True(t, IsKnownRegion(region)) + } + + for _, region := range []string{ + "us-east-100", + "cn-north", + } { + require.False(t, IsKnownRegion(region)) + } +}