diff --git a/lib/cloud/awsconfig/awsconfig.go b/lib/cloud/awsconfig/awsconfig.go index 92f7e8aa96e86..5c7aa33192069 100644 --- a/lib/cloud/awsconfig/awsconfig.go +++ b/lib/cloud/awsconfig/awsconfig.go @@ -19,6 +19,7 @@ package awsconfig import ( "context" "log/slog" + "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" @@ -47,16 +48,21 @@ const ( // This is used to generate aws configs for clients that must use an integration instead of ambient credentials. type IntegrationCredentialProviderFunc func(ctx context.Context, region, integration string) (aws.CredentialsProvider, error) +// AssumeRoleAPIClientFunc provides an AWS STS assume role API client. +type AssumeRoleAPIClientFunc func(aws.Config) stscreds.AssumeRoleAPIClient + +// assumeRole is an AWS role ARN to assume, optionally with an external ID. +type assumeRole struct { + roleARN string + externalID string +} + // options is a struct of additional options for assuming an AWS role // when construction an underlying AWS config. type options struct { - // baseConfigis a config to use instead of the default config for an - // AWS region, which is used to enable role chaining. - baseConfig *aws.Config - // assumeRoleARN is the AWS IAM Role ARN to assume. - assumeRoleARN string - // assumeRoleExternalID is used to assume an external AWS IAM Role. - assumeRoleExternalID string + // assumeRoles are AWS IAM roles that should be assumed one by one in order, + // as a chain of assumed roles. + assumeRoles []assumeRole // credentialsSource describes which source to use to fetch credentials. credentialsSource credentialsSource // integration is the name of the integration to be used to fetch the credentials. @@ -67,22 +73,39 @@ type options struct { customRetryer func() aws.Retryer // maxRetries is the maximum number of retries to use for the config. maxRetries *int + // newAssumeRoleAPIClientFn sets the STS assume role client provider func. + newAssumeRoleAPIClientFn AssumeRoleAPIClientFunc +} + +func buildOptions(optFns ...OptionsFn) (*options, error) { + var opts options + for _, optFn := range optFns { + optFn(&opts) + } + if err := opts.checkAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + return &opts, nil } -func (a *options) checkAndSetDefaults() error { - switch a.credentialsSource { +func (o *options) checkAndSetDefaults() error { + switch o.credentialsSource { case credentialsSourceAmbient: - if a.integration != "" { + if o.integration != "" { return trace.BadParameter("integration and ambient credentials cannot be used at the same time") } case credentialsSourceIntegration: - if a.integration == "" { + if o.integration == "" { return trace.BadParameter("missing integration name") } default: return trace.BadParameter("missing credentials source (ambient or integration)") } + if o.newAssumeRoleAPIClientFn == nil { + o.newAssumeRoleAPIClientFn = newAssumeRoleAPIClient + } + return nil } @@ -93,8 +116,14 @@ type OptionsFn func(*options) // WithAssumeRole configures options needed for assuming an AWS role. func WithAssumeRole(roleARN, externalID string) OptionsFn { return func(options *options) { - options.assumeRoleARN = roleARN - options.assumeRoleExternalID = externalID + if roleARN == "" { + // ignore empty role ARN for caller convenience. + return + } + options.assumeRoles = append(options.assumeRoles, assumeRole{ + roleARN: roleARN, + externalID: externalID, + }) } } @@ -146,96 +175,108 @@ func WithIntegrationCredentialProvider(cred IntegrationCredentialProviderFunc) O } } +// WithAssumeRoleAPIClientFunc sets the STS API client factory func used to +// assume roles. +func WithAssumeRoleAPIClientFunc(fn AssumeRoleAPIClientFunc) OptionsFn { + return func(options *options) { + options.newAssumeRoleAPIClientFn = fn + } +} + // GetConfig returns an AWS config for the specified region, optionally // assuming AWS IAM Roles. -func GetConfig(ctx context.Context, region string, opts ...OptionsFn) (aws.Config, error) { - var options options - for _, opt := range opts { - opt(&options) - } - if options.baseConfig == nil { - cfg, err := getConfigForRegion(ctx, region, options) - if err != nil { - return aws.Config{}, trace.Wrap(err) - } - options.baseConfig = &cfg +func GetConfig(ctx context.Context, region string, optFns ...OptionsFn) (aws.Config, error) { + opts, err := buildOptions(optFns...) + if err != nil { + return aws.Config{}, trace.Wrap(err) } - if options.assumeRoleARN == "" { - return *options.baseConfig, nil + + cfg, err := getBaseConfig(ctx, region, opts) + if err != nil { + return aws.Config{}, trace.Wrap(err) } - return getConfigForRole(ctx, region, options) + return getConfigForRoleChain(ctx, cfg, opts.assumeRoles, opts.newAssumeRoleAPIClientFn) } -// ambientConfigProvider loads a new config using the environment variables. -func ambientConfigProvider(region string, cred aws.CredentialsProvider, options options) (aws.Config, error) { - opts := buildConfigOptions(region, cred, options) - cfg, err := config.LoadDefaultConfig(context.Background(), opts...) +// loadDefaultConfig loads a new config. +func loadDefaultConfig(ctx context.Context, region string, cred aws.CredentialsProvider, opts *options) (aws.Config, error) { + configOpts := buildConfigOptions(region, cred, opts) + cfg, err := config.LoadDefaultConfig(ctx, configOpts...) return cfg, trace.Wrap(err) } -func buildConfigOptions(region string, cred aws.CredentialsProvider, options options) []func(*config.LoadOptions) error { - opts := []func(*config.LoadOptions) error{ +func buildConfigOptions(region string, cred aws.CredentialsProvider, opts *options) []func(*config.LoadOptions) error { + configOpts := []func(*config.LoadOptions) error{ config.WithDefaultRegion(defaultRegion), config.WithRegion(region), config.WithCredentialsProvider(cred), } if modules.GetModules().IsBoringBinary() { - opts = append(opts, config.WithUseFIPSEndpoint(aws.FIPSEndpointStateEnabled)) + configOpts = append(configOpts, config.WithUseFIPSEndpoint(aws.FIPSEndpointStateEnabled)) } - if options.customRetryer != nil { - opts = append(opts, config.WithRetryer(options.customRetryer)) + if opts.customRetryer != nil { + configOpts = append(configOpts, config.WithRetryer(opts.customRetryer)) } - if options.maxRetries != nil { - opts = append(opts, config.WithRetryMaxAttempts(*options.maxRetries)) + if opts.maxRetries != nil { + configOpts = append(configOpts, config.WithRetryMaxAttempts(*opts.maxRetries)) } - return opts + return configOpts } -// getConfigForRegion returns AWS config for the specified region. -func getConfigForRegion(ctx context.Context, region string, options options) (aws.Config, error) { - if err := options.checkAndSetDefaults(); err != nil { - return aws.Config{}, trace.Wrap(err) - } - +// getBaseConfig returns an AWS config without assuming any roles. +func getBaseConfig(ctx context.Context, region string, opts *options) (aws.Config, error) { var cred aws.CredentialsProvider - if options.credentialsSource == credentialsSourceIntegration { - if options.integrationCredentialsProvider == nil { + if opts.credentialsSource == credentialsSourceIntegration { + if opts.integrationCredentialsProvider == nil { return aws.Config{}, trace.BadParameter("missing aws integration credential provider") } - slog.DebugContext(ctx, "Initializing AWS config with integration", "region", region, "integration", options.integration) + slog.DebugContext(ctx, "Initializing AWS config with integration", "region", region, "integration", opts.integration) var err error - cred, err = options.integrationCredentialsProvider(ctx, region, options.integration) + cred, err = opts.integrationCredentialsProvider(ctx, region, opts.integration) if err != nil { return aws.Config{}, trace.Wrap(err) } } else { - slog.DebugContext(ctx, "Initializing AWS config from environment", "region", region) + slog.DebugContext(ctx, "Initializing AWS config from default credential chain", "region", region) } - cfg, err := ambientConfigProvider(region, cred, options) + cfg, err := loadDefaultConfig(ctx, region, cred, opts) return cfg, trace.Wrap(err) } -// getConfigForRole returns an AWS config for the specified region and role. -func getConfigForRole(ctx context.Context, region string, options options) (aws.Config, error) { - if err := options.checkAndSetDefaults(); err != nil { - return aws.Config{}, trace.Wrap(err) +func getConfigForRoleChain(ctx context.Context, cfg aws.Config, roles []assumeRole, newSTSCltFn AssumeRoleAPIClientFunc) (aws.Config, error) { + for _, r := range roles { + cfg.Credentials = getAssumeRoleProvider(newSTSCltFn(cfg), r) + } + if len(roles) > 0 { + // no point caching every assumed role in the chain, we can just cache + // the last one. + cfg.Credentials = aws.NewCredentialsCache(cfg.Credentials, func(cacheOpts *aws.CredentialsCacheOptions) { + // expire early to avoid expiration race. + cacheOpts.ExpiryWindow = 5 * time.Minute + }) + if _, err := cfg.Credentials.Retrieve(ctx); err != nil { + return aws.Config{}, trace.Wrap(err) + } } + return cfg, nil +} + +func newAssumeRoleAPIClient(cfg aws.Config) stscreds.AssumeRoleAPIClient { + return newSTSClient(cfg) +} - stsClient := sts.NewFromConfig(*options.baseConfig, func(o *sts.Options) { +func newSTSClient(cfg aws.Config) *sts.Client { + return sts.NewFromConfig(cfg, func(o *sts.Options) { o.TracerProvider = smithyoteltracing.Adapt(otel.GetTracerProvider()) }) - cred := stscreds.NewAssumeRoleProvider(stsClient, options.assumeRoleARN, func(aro *stscreds.AssumeRoleOptions) { - if options.assumeRoleExternalID != "" { - aro.ExternalID = aws.String(options.assumeRoleExternalID) +} + +func getAssumeRoleProvider(clt stscreds.AssumeRoleAPIClient, role assumeRole) aws.CredentialsProvider { + return stscreds.NewAssumeRoleProvider(clt, role.roleARN, func(aro *stscreds.AssumeRoleOptions) { + if role.externalID != "" { + aro.ExternalID = aws.String(role.externalID) } }) - if _, err := cred.Retrieve(ctx); err != nil { - return aws.Config{}, trace.Wrap(err) - } - - opts := buildConfigOptions(region, cred, options) - cfg, err := config.LoadDefaultConfig(ctx, opts...) - return cfg, trace.Wrap(err) } diff --git a/lib/cloud/awsconfig/awsconfig_test.go b/lib/cloud/awsconfig/awsconfig_test.go index 5c0ab10ed6abb..73603487d6fa5 100644 --- a/lib/cloud/awsconfig/awsconfig_test.go +++ b/lib/cloud/awsconfig/awsconfig_test.go @@ -18,9 +18,14 @@ package awsconfig import ( "context" + "fmt" "testing" + "time" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/sts" + ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" "github.com/gravitational/trace" "github.com/stretchr/testify/require" ) @@ -29,18 +34,60 @@ type mockCredentialProvider struct { cred aws.Credentials } -func (m *mockCredentialProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { +func (m *mockCredentialProvider) Retrieve(_ context.Context) (aws.Credentials, error) { return m.cred, nil } +type mockAssumeRoleAPIClient struct{} + +func (m *mockAssumeRoleAPIClient) AssumeRole(_ context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) { + fakeKeyID := fmt.Sprintf("role: %s, externalID: %s", aws.ToString(params.RoleArn), aws.ToString(params.ExternalId)) + return &sts.AssumeRoleOutput{ + AssumedRoleUser: &ststypes.AssumedRoleUser{ + Arn: params.RoleArn, + AssumedRoleId: aws.String("role-id"), + }, + Credentials: &ststypes.Credentials{ + AccessKeyId: aws.String(fakeKeyID), + Expiration: aws.Time(time.Time{}), + SecretAccessKey: aws.String("fake-secret-access-key"), + SessionToken: aws.String("fake-session-token"), + }, + }, nil +} + func TestGetConfigIntegration(t *testing.T) { t.Parallel() + + cache, err := NewCache() + require.NoError(t, err) + tests := []struct { + desc string + Provider + }{ + { + desc: "uncached", + Provider: ProviderFunc(GetConfig), + }, + { + desc: "cached", + Provider: cache, + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + testGetConfigIntegration(t, test.Provider) + }) + } +} + +func testGetConfigIntegration(t *testing.T, provider Provider) { dummyIntegration := "integration-test" dummyRegion := "test-region-123" t.Run("without an integration credential provider, must return missing credential provider error", func(t *testing.T) { ctx := context.Background() - _, err := GetConfig(ctx, dummyRegion, WithCredentialsMaybeIntegration(dummyIntegration)) + _, err := provider.GetConfig(ctx, dummyRegion, WithCredentialsMaybeIntegration(dummyIntegration)) require.True(t, trace.IsBadParameter(err), "unexpected error: %v", err) require.ErrorContains(t, err, "missing aws integration credential provider") }) @@ -48,7 +95,7 @@ func TestGetConfigIntegration(t *testing.T) { t.Run("with an integration credential provider, must return the credentials", func(t *testing.T) { ctx := context.Background() - cfg, err := GetConfig(ctx, dummyRegion, + cfg, err := provider.GetConfig(ctx, dummyRegion, WithCredentialsMaybeIntegration(dummyIntegration), WithIntegrationCredentialProvider(func(ctx context.Context, region, integration string) (aws.CredentialsProvider, error) { if region == dummyRegion && integration == dummyIntegration { @@ -66,10 +113,40 @@ func TestGetConfigIntegration(t *testing.T) { require.Equal(t, "foo-bar", creds.SessionToken) }) + t.Run("with an integration credential provider assuming a role, must return assumed role credentials", func(t *testing.T) { + ctx := context.Background() + + cfg, err := provider.GetConfig(ctx, dummyRegion, + WithCredentialsMaybeIntegration(dummyIntegration), + WithIntegrationCredentialProvider(func(ctx context.Context, region, integration string) (aws.CredentialsProvider, error) { + if region == dummyRegion && integration == dummyIntegration { + return &mockCredentialProvider{ + cred: aws.Credentials{ + SessionToken: "foo-bar", + }, + }, nil + } + return nil, trace.NotFound("no creds in region %q with integration %q", region, integration) + }), + WithAssumeRole("roleA", "abc123"), + WithAssumeRoleAPIClientFunc(func(cfg aws.Config) stscreds.AssumeRoleAPIClient { + creds, err := cfg.Credentials.Retrieve(context.Background()) + require.NoError(t, err) + require.Equal(t, "foo-bar", creds.SessionToken) + return &mockAssumeRoleAPIClient{} + }), + ) + require.NoError(t, err) + creds, err := cfg.Credentials.Retrieve(ctx) + require.NoError(t, err) + require.Equal(t, "role: roleA, externalID: abc123", creds.AccessKeyID) + require.Equal(t, "fake-session-token", creds.SessionToken) + }) + t.Run("with an integration credential provider, but using an empty integration falls back to ambient credentials", func(t *testing.T) { ctx := context.Background() - _, err := GetConfig(ctx, dummyRegion, + _, err := provider.GetConfig(ctx, dummyRegion, WithCredentialsMaybeIntegration(""), WithIntegrationCredentialProvider(func(ctx context.Context, region, integration string) (aws.CredentialsProvider, error) { require.Fail(t, "this function should not be called") @@ -81,7 +158,7 @@ func TestGetConfigIntegration(t *testing.T) { t.Run("with an integration credential provider, but using ambient credentials", func(t *testing.T) { ctx := context.Background() - _, err := GetConfig(ctx, dummyRegion, + _, err := provider.GetConfig(ctx, dummyRegion, WithAmbientCredentials(), WithIntegrationCredentialProvider(func(ctx context.Context, region, integration string) (aws.CredentialsProvider, error) { require.Fail(t, "this function should not be called") @@ -93,7 +170,7 @@ func TestGetConfigIntegration(t *testing.T) { t.Run("with an integration credential provider, but no credential source", func(t *testing.T) { ctx := context.Background() - _, err := GetConfig(ctx, dummyRegion, + _, err := provider.GetConfig(ctx, dummyRegion, WithIntegrationCredentialProvider(func(ctx context.Context, region, integration string) (aws.CredentialsProvider, error) { require.Fail(t, "this function should not be called") return nil, nil @@ -102,3 +179,50 @@ func TestGetConfigIntegration(t *testing.T) { require.ErrorContains(t, err, "missing credentials source") }) } + +func TestGetCacheKeyForRoles(t *testing.T) { + tests := []struct { + desc string + roles []assumeRole + want string + wantErr string + }{ + { + desc: "valid without external ID", + roles: []assumeRole{ + {roleARN: "roleA"}, + {roleARN: "roleB"}, + }, + want: "roleA||roleB||", + }, + { + desc: "valid with external ID", + roles: []assumeRole{ + {roleARN: "roleA", externalID: "extA"}, + {roleARN: "roleB", externalID: "extB"}, + }, + want: "roleA|extA|roleB|extB|", + }, + { + desc: "invalid role ARN", + roles: []assumeRole{{roleARN: "roleA|extA|roleB"}}, + wantErr: "invalid role ARN", + }, + { + desc: "invalid external ID", + roles: []assumeRole{{roleARN: "roleA", externalID: "extA|roleB|extB"}}, + wantErr: "invalid external ID", + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + got, err := getCacheKeyForRoles(test.roles) + if test.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, test.wantErr) + return + } + require.Equal(t, test.want, got) + }) + } +} diff --git a/lib/cloud/awsconfig/cache.go b/lib/cloud/awsconfig/cache.go new file mode 100644 index 0000000000000..98103391ac7ff --- /dev/null +++ b/lib/cloud/awsconfig/cache.go @@ -0,0 +1,170 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package awsconfig + +import ( + "context" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/utils" +) + +// Cache is an AWS config [Provider] that caches credentials by integration and +// role. +type Cache struct { + awsConfigCache *utils.FnCache +} + +var _ Provider = (*Cache)(nil) + +// NewCache returns a new [Cache]. +func NewCache() (*Cache, error) { + c, err := utils.NewFnCache(utils.FnCacheConfig{ + TTL: 15 * time.Minute, + ReloadOnErr: true, + }) + if err != nil { + return nil, trace.Wrap(err) + } + return &Cache{ + awsConfigCache: c, + }, nil +} + +// GetConfig returns an [aws.Config] for the given region and options. +func (c *Cache) GetConfig(ctx context.Context, region string, optFns ...OptionsFn) (aws.Config, error) { + opts, err := buildOptions(optFns...) + if err != nil { + return aws.Config{}, trace.Wrap(err) + } + + cfg, err := c.getBaseConfig(ctx, region, opts) + if err != nil { + return aws.Config{}, trace.Wrap(err) + } + cfg, err = c.getConfigForRoleChain(ctx, cfg, opts) + if err != nil { + return aws.Config{}, trace.Wrap(err) + } + return cfg, nil +} + +func (c *Cache) getBaseConfig(ctx context.Context, region string, opts *options) (aws.Config, error) { + // The AWS SDK combines config loading with default credential chain + // loading. + // We cache the entire config by integration name, which is empty for + // non-integration config, but only use credentials from it on cache hit. + cacheKey := configCacheKey{ + integration: opts.integration, + } + var reloaded bool + cfg, err := utils.FnCacheGet(ctx, c.awsConfigCache, cacheKey, + func(ctx context.Context) (aws.Config, error) { + reloaded = true + cfg, err := getBaseConfig(ctx, region, opts) + return cfg, trace.Wrap(err) + }) + if err != nil { + return aws.Config{}, trace.Wrap(err) + } + + if reloaded { + // If the cache reload func was called, then the config we got back has + // already applied our options so we can return the config itself. + return cfg, nil + } + + // On cache hit we just take the credentials from the cached config. + // Then, we apply those credentials while loading config with current + // options. + cfg, err = loadDefaultConfig(ctx, region, cfg.Credentials, opts) + return cfg, trace.Wrap(err) +} + +func (c *Cache) getConfigForRoleChain(ctx context.Context, cfg aws.Config, opts *options) (aws.Config, error) { + for i, r := range opts.assumeRoles { + // cache credentials by integration and assumed-role chain. + roleChain, err := getCacheKeyForRoles(opts.assumeRoles[:i+1]) + if err != nil { + return aws.Config{}, trace.Wrap(err) + } + cacheKey := configCacheKey{ + integration: opts.integration, + roleChain: roleChain, + } + credProvider, err := utils.FnCacheGet(ctx, c.awsConfigCache, cacheKey, + func(ctx context.Context) (aws.CredentialsProvider, error) { + clt := opts.newAssumeRoleAPIClientFn(cfg) + credProvider := getAssumeRoleProvider(clt, r) + return aws.NewCredentialsCache(credProvider, + func(cacheOpts *aws.CredentialsCacheOptions) { + // expire early to avoid expiration race. + cacheOpts.ExpiryWindow = 5 * time.Minute + }, + ), nil + }) + if err != nil { + return aws.Config{}, trace.Wrap(err) + } + cfg.Credentials = credProvider + } + if len(opts.assumeRoles) > 0 { + if _, err := cfg.Credentials.Retrieve(ctx); err != nil { + return aws.Config{}, trace.Wrap(err) + } + } + return cfg, nil +} + +// configCacheKey defines the cache key used for AWS config. +// Config is cached by integration and AWS IAM role chain. +type configCacheKey struct { + // integration is the name of an AWS integration. + integration string + // roleChain is the AWS IAM role chain as a string of roles. + roleChain string +} + +// getCacheKeyForRoles makes a cache key for roles. +// cache key format: role1|ext1|role2|ext2|... +func getCacheKeyForRoles(roles []assumeRole) (string, error) { + // The cache key can be used to get role credentials without calling AWS + // STS. + // Therefore, we should be paranoid and do some validation here to be sure + // that the cache cannot be exploited. + // Neither role ARN nor external ID can contain this delimiter: + // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html + const delimiter = "|" + var sb strings.Builder + for _, r := range roles { + if strings.Contains(r.roleARN, delimiter) { + return "", trace.BadParameter("invalid role ARN %s", r.roleARN) + } + if strings.Contains(r.externalID, delimiter) { + return "", trace.BadParameter("invalid external ID %s", r.externalID) + } + sb.WriteString(r.roleARN) + sb.WriteString(delimiter) + sb.WriteString(r.externalID) + sb.WriteString(delimiter) + } + return sb.String(), nil +} diff --git a/lib/cloud/awsconfig/provider.go b/lib/cloud/awsconfig/provider.go new file mode 100644 index 0000000000000..cff06964ce785 --- /dev/null +++ b/lib/cloud/awsconfig/provider.go @@ -0,0 +1,37 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package awsconfig + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +// Provider provides an [aws.Config]. +type Provider interface { + // GetConfig returns an [aws.Config] for the given region and options. + GetConfig(ctx context.Context, region string, optFns ...OptionsFn) (aws.Config, error) +} + +// ProviderFunc is a [Provider] adapter for functions. +type ProviderFunc func(ctx context.Context, region string, optFns ...OptionsFn) (aws.Config, error) + +// GetConfig returns an [aws.Config] for the given region and options. +func (fn ProviderFunc) GetConfig(ctx context.Context, region string, optFns ...OptionsFn) (aws.Config, error) { + return fn(ctx, region, optFns...) +}