Migrate AWS STS client to AWS SDK v2 (#51573)

This commit is contained in:
Gavin Frazar
2025-01-29 21:16:56 +00:00
committed by GitHub
parent 533202f18c
commit a9be7dbfbb
23 changed files with 185 additions and 157 deletions
-20
View File
@@ -91,8 +91,6 @@ type GCPClients interface {
type AWSClients interface {
// GetAWSSession returns AWS session for the specified region and any role(s).
GetAWSSession(ctx context.Context, region string, opts ...AWSOptionsFn) (*awssession.Session, error)
// GetAWSSTSClient returns AWS STS client for the specified region.
GetAWSSTSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (stsiface.STSAPI, error)
}
// AzureClients is an interface for Azure-specific API clients
@@ -468,15 +466,6 @@ func (c *cloudClients) GetAWSSession(ctx context.Context, region string, opts ..
return c.getAWSSessionForRole(ctx, region, options)
}
// GetAWSSTSClient returns AWS STS client for the specified region.
func (c *cloudClients) GetAWSSTSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (stsiface.STSAPI, error) {
session, err := c.GetAWSSession(ctx, region, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return sts.New(session), nil
}
// GetGCPIAMClient returns GCP IAM client.
func (c *cloudClients) GetGCPIAMClient(ctx context.Context) (*gcpcredentials.IamCredentialsClient, error) {
c.mtx.RLock()
@@ -964,15 +953,6 @@ func (c *TestCloudClients) getAWSSessionForRegion(region string) (*awssession.Se
})
}
// GetAWSSTSClient returns AWS STS client for the specified region.
func (c *TestCloudClients) GetAWSSTSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (stsiface.STSAPI, error) {
_, err := c.GetAWSSession(ctx, region, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return c.STS, nil
}
// GetGCPIAMClient returns GCP IAM client.
func (c *TestCloudClients) GetGCPIAMClient(ctx context.Context) (*gcpcredentials.IamCredentialsClient, error) {
return gcpcredentials.NewIamCredentialsClient(ctx,
+1 -1
View File
@@ -81,7 +81,7 @@ func (m *STSClientV1) AssumeRoleWithContext(ctx aws.Context, in *sts.AssumeRoleI
expiry := time.Now().Add(60 * time.Minute)
return &sts.AssumeRoleOutput{
Credentials: &sts.Credentials{
AccessKeyId: in.RoleArn,
AccessKeyId: aws.String("FAKEACCESSKEYID"),
SecretAccessKey: aws.String("secret"),
SessionToken: aws.String("token"),
Expiration: &expiry,
+15 -2
View File
@@ -46,6 +46,7 @@ import (
type STSClient struct {
STSClientV1
Unauth bool
// credentialProvider is only set when a chain of assumed roles is used.
credentialProvider aws.CredentialsProvider
// recordFn records the role and external ID when a role is assumed.
@@ -55,17 +56,25 @@ type STSClient struct {
}
func (m *STSClient) GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) {
if m.Unauth {
return nil, trace.AccessDenied("unauthorized")
}
return &sts.GetCallerIdentityOutput{
Arn: aws.String(m.ARN),
}, nil
}
func (m *STSClient) AssumeRoleWithWebIdentity(ctx context.Context, in *sts.AssumeRoleWithWebIdentityInput, _ ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) {
if m.Unauth {
return nil, trace.AccessDenied("unauthorized")
}
m.record(aws.ToString(in.RoleArn), "")
expiry := time.Now().Add(60 * time.Minute)
return &sts.AssumeRoleWithWebIdentityOutput{
Credentials: &ststypes.Credentials{
AccessKeyId: in.RoleArn,
AccessKeyId: aws.String("WEBIDENTITYFAKEACCESSKEYID"),
SecretAccessKey: aws.String("secret"),
SessionToken: aws.String("token"),
Expiration: &expiry,
@@ -74,6 +83,10 @@ func (m *STSClient) AssumeRoleWithWebIdentity(ctx context.Context, in *sts.Assum
}
func (m *STSClient) AssumeRole(ctx context.Context, in *sts.AssumeRoleInput, _ ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) {
if m.Unauth {
return nil, trace.AccessDenied("unauthorized")
}
// Retrieve credentials if we have a credential provider, so that all
// assume-role providers in a role chain are triggered to call AssumeRole.
if m.credentialProvider != nil {
@@ -87,7 +100,7 @@ func (m *STSClient) AssumeRole(ctx context.Context, in *sts.AssumeRoleInput, _ .
expiry := time.Now().Add(60 * time.Minute)
return &sts.AssumeRoleOutput{
Credentials: &ststypes.Credentials{
AccessKeyId: in.RoleArn,
AccessKeyId: aws.String("FAKEACCESSKEYID"),
SecretAccessKey: aws.String("secret"),
SessionToken: aws.String("token"),
Expiration: &expiry,
+6
View File
@@ -26,6 +26,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
@@ -57,6 +58,11 @@ import (
awsutils "github.com/gravitational/teleport/lib/utils/aws"
)
func TestMain(m *testing.M) {
utils.InitLoggerForTests()
os.Exit(m.Run())
}
type makeRequest func(url string, provider client.ConfigProvider, awsHost string) error
func s3Request(url string, provider client.ConfigProvider, awsHost string) error {
+4 -2
View File
@@ -269,11 +269,13 @@ func (c *IAM) getAWSIdentity(ctx context.Context, database types.Database) (awsl
return c.agentIdentity, nil
}
c.mu.RUnlock()
sts, err := c.cfg.Clients.GetAWSSTSClient(ctx, meta.Region, cloud.WithAmbientCredentials())
awsCfg, err := c.cfg.AWSConfigProvider.GetConfig(ctx, meta.Region, awsconfig.WithAmbientCredentials())
if err != nil {
return nil, trace.Wrap(err)
}
awsIdentity, err := awslib.GetIdentityWithClient(ctx, sts)
clt := c.cfg.awsClients.getSTSClient(awsCfg)
awsIdentity, err := awslib.GetIdentityWithClientV2(ctx, clt)
if err != nil {
return nil, trace.Wrap(err)
}
+32 -41
View File
@@ -156,16 +156,15 @@ func TestAWSIAM(t *testing.T) {
AWSConfigProvider: &mocks.AWSConfigProvider{
STSClient: stsClient,
},
Clients: &clients.TestCloudClients{
STS: &stsClient.STSClientV1,
},
HostID: "host-id",
Clients: &clients.TestCloudClients{},
HostID: "host-id",
onProcessedTask: func(iamTask, error) {
taskChan <- struct{}{}
},
awsClients: fakeAWSClients{
iamClient: iamClient,
rdsClient: clt,
stsClient: stsClient,
},
})
require.NoError(t, err)
@@ -291,8 +290,10 @@ func TestAWSIAMNoPermissions(t *testing.T) {
t.Cleanup(cancel)
// Create unauthorized mocks for AWS services.
stsClient := &mocks.STSClientV1{
ARN: "arn:aws:iam::123456789012:role/test-role",
stsClient := &mocks.STSClient{
STSClientV1: mocks.STSClientV1{
ARN: "arn:aws:iam::123456789012:role/test-role",
},
}
tests := []struct {
name string
@@ -301,70 +302,64 @@ func TestAWSIAMNoPermissions(t *testing.T) {
awsClients awsClientProvider
}{
{
name: "RDS database",
meta: types.AWS{Region: "localhost", AccountID: "123456789012", RDS: types.RDS{InstanceID: "postgres-rds", ResourceID: "postgres-rds-resource-id"}},
clients: &clients.TestCloudClients{
STS: stsClient,
},
name: "RDS database",
meta: types.AWS{Region: "localhost", AccountID: "123456789012", RDS: types.RDS{InstanceID: "postgres-rds", ResourceID: "postgres-rds-resource-id"}},
clients: &clients.TestCloudClients{},
awsClients: fakeAWSClients{
iamClient: &mocks.IAMMock{Unauth: true},
rdsClient: &mocks.RDSClient{Unauth: true},
stsClient: stsClient,
},
},
{
name: "Aurora cluster",
meta: types.AWS{Region: "localhost", AccountID: "123456789012", RDS: types.RDS{ClusterID: "postgres-aurora", ResourceID: "postgres-aurora-resource-id"}},
clients: &clients.TestCloudClients{
STS: stsClient,
},
name: "Aurora cluster",
meta: types.AWS{Region: "localhost", AccountID: "123456789012", RDS: types.RDS{ClusterID: "postgres-aurora", ResourceID: "postgres-aurora-resource-id"}},
clients: &clients.TestCloudClients{},
awsClients: fakeAWSClients{
iamClient: &mocks.IAMMock{Unauth: true},
rdsClient: &mocks.RDSClient{Unauth: true},
stsClient: stsClient,
},
},
{
name: "RDS database missing metadata",
meta: types.AWS{Region: "localhost", RDS: types.RDS{ClusterID: "postgres-aurora"}},
clients: &clients.TestCloudClients{
STS: stsClient,
},
name: "RDS database missing metadata",
meta: types.AWS{Region: "localhost", RDS: types.RDS{ClusterID: "postgres-aurora"}},
clients: &clients.TestCloudClients{},
awsClients: fakeAWSClients{
iamClient: &mocks.IAMMock{Unauth: true},
rdsClient: &mocks.RDSClient{Unauth: true},
stsClient: stsClient,
},
},
{
name: "Redshift cluster",
meta: types.AWS{Region: "localhost", AccountID: "123456789012", Redshift: types.Redshift{ClusterID: "redshift-cluster-1"}},
clients: &clients.TestCloudClients{
STS: stsClient,
},
name: "Redshift cluster",
meta: types.AWS{Region: "localhost", AccountID: "123456789012", Redshift: types.Redshift{ClusterID: "redshift-cluster-1"}},
clients: &clients.TestCloudClients{},
awsClients: fakeAWSClients{
iamClient: &mocks.IAMMock{Unauth: true},
stsClient: stsClient,
},
},
{
name: "ElastiCache",
meta: types.AWS{Region: "localhost", AccountID: "123456789012", ElastiCache: types.ElastiCache{ReplicationGroupID: "some-group"}},
clients: &clients.TestCloudClients{
STS: stsClient,
},
name: "ElastiCache",
meta: types.AWS{Region: "localhost", AccountID: "123456789012", ElastiCache: types.ElastiCache{ReplicationGroupID: "some-group"}},
clients: &clients.TestCloudClients{},
awsClients: fakeAWSClients{
iamClient: &mocks.IAMMock{Unauth: true},
stsClient: stsClient,
},
},
{
name: "IAM UnmodifiableEntityException",
meta: types.AWS{Region: "localhost", AccountID: "123456789012", Redshift: types.Redshift{ClusterID: "redshift-cluster-1"}},
clients: &clients.TestCloudClients{
STS: stsClient,
},
name: "IAM UnmodifiableEntityException",
meta: types.AWS{Region: "localhost", AccountID: "123456789012", Redshift: types.Redshift{ClusterID: "redshift-cluster-1"}},
clients: &clients.TestCloudClients{},
awsClients: fakeAWSClients{
iamClient: &mocks.IAMMock{
Error: &iamtypes.UnmodifiableEntityException{
Message: aws.String("Cannot perform the operation on the protected role"),
},
},
stsClient: stsClient,
},
},
}
@@ -377,11 +372,7 @@ func TestAWSIAMNoPermissions(t *testing.T) {
Clients: test.clients,
HostID: "host-id",
AWSConfigProvider: &mocks.AWSConfigProvider{
STSClient: &mocks.STSClient{
STSClientV1: mocks.STSClientV1{
ARN: "arn:aws:iam::123456789012:role/test-role",
},
},
STSClient: stsClient,
},
awsClients: test.awsClients,
})
+11
View File
@@ -36,6 +36,7 @@ import (
redshifttypes "github.com/aws/aws-sdk-go-v2/service/redshift/types"
rss "github.com/aws/aws-sdk-go-v2/service/redshiftserverless"
rsstypes "github.com/aws/aws-sdk-go-v2/service/redshiftserverless/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/gravitational/trace"
"github.com/gravitational/teleport"
@@ -93,6 +94,11 @@ type rssClient interface {
GetWorkgroup(ctx context.Context, params *rss.GetWorkgroupInput, optFns ...func(*rss.Options)) (*rss.GetWorkgroupOutput, error)
}
// stsClient defines a subset of the AWS STS client API.
type stsClient interface {
GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error)
}
// awsClientProvider is an AWS SDK client provider.
type awsClientProvider interface {
getElastiCacheClient(cfg aws.Config, optFns ...func(*elasticache.Options)) elasticacheClient
@@ -102,6 +108,7 @@ type awsClientProvider interface {
getRDSClient(cfg aws.Config, optFns ...func(*rds.Options)) rdsClient
getRedshiftClient(cfg aws.Config, optFns ...func(*redshift.Options)) redshiftClient
getRedshiftServerlessClient(cfg aws.Config, optFns ...func(*rss.Options)) rssClient
getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient
}
type defaultAWSClients struct{}
@@ -134,6 +141,10 @@ func (defaultAWSClients) getRedshiftServerlessClient(cfg aws.Config, optFns ...f
return rss.NewFromConfig(cfg, optFns...)
}
func (defaultAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient {
return sts.NewFromConfig(cfg, optFns...)
}
// MetadataConfig is the cloud metadata service config.
type MetadataConfig struct {
// Clients is an interface for retrieving cloud clients.
+6
View File
@@ -35,6 +35,7 @@ import (
redshifttypes "github.com/aws/aws-sdk-go-v2/service/redshift/types"
rss "github.com/aws/aws-sdk-go-v2/service/redshiftserverless"
rsstypes "github.com/aws/aws-sdk-go-v2/service/redshiftserverless/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
@@ -511,6 +512,7 @@ type fakeAWSClients struct {
rdsClient rdsClient
redshiftClient redshiftClient
rssClient rssClient
stsClient stsClient
}
func (f fakeAWSClients) getElastiCacheClient(cfg aws.Config, optFns ...func(*elasticache.Options)) elasticacheClient {
@@ -540,3 +542,7 @@ func (f fakeAWSClients) getRedshiftClient(aws.Config, ...func(*redshift.Options)
func (f fakeAWSClients) getRedshiftServerlessClient(aws.Config, ...func(*rss.Options)) rssClient {
return f.rssClient
}
func (f fakeAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient {
return f.stsClient
}
+1 -1
View File
@@ -80,7 +80,7 @@ func NewDiscoveryResourceChecker(cfg DiscoveryResourceCheckerConfig) (DiscoveryR
return nil, trace.Wrap(err)
}
credentialsChecker, err := newCrednentialsChecker(cfg)
credentialsChecker, err := newCredentialsChecker(cfg)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -30,6 +30,7 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/cloud"
"github.com/gravitational/teleport/lib/cloud/aws"
"github.com/gravitational/teleport/lib/cloud/awsconfig"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
)
@@ -40,13 +41,15 @@ import (
// Note that this checker warns the user with suggestions on how to configure
// the credentials correctly instead of returning errors.
type credentialsChecker struct {
cloudClients cloud.Clients
resourceMatchers []services.ResourceMatcher
logger *slog.Logger
cache *utils.FnCache
awsConfigProvider awsconfig.Provider
awsClients awsClientProvider
azureClients cloud.AzureClients
resourceMatchers []services.ResourceMatcher
logger *slog.Logger
cache *utils.FnCache
}
func newCrednentialsChecker(cfg DiscoveryResourceCheckerConfig) (*credentialsChecker, error) {
func newCredentialsChecker(cfg DiscoveryResourceCheckerConfig) (*credentialsChecker, error) {
cache, err := utils.NewFnCache(utils.FnCacheConfig{
TTL: 10 * time.Minute,
Context: cfg.Context,
@@ -56,10 +59,12 @@ func newCrednentialsChecker(cfg DiscoveryResourceCheckerConfig) (*credentialsChe
}
return &credentialsChecker{
cloudClients: cfg.Clients,
resourceMatchers: cfg.ResourceMatchers,
logger: cfg.Logger,
cache: cache,
awsConfigProvider: cfg.AWSConfigProvider,
awsClients: defaultAWSClients{},
azureClients: cfg.Clients,
resourceMatchers: cfg.ResourceMatchers,
logger: cfg.Logger,
cache: cache,
}, nil
}
@@ -111,18 +116,19 @@ func (c *credentialsChecker) getAWSIdentity(ctx context.Context, meta *types.AWS
}
identity, err := utils.FnCacheGet(ctx, c.cache, types.CloudAWS, func(ctx context.Context) (aws.Identity, error) {
client, err := c.cloudClients.GetAWSSTSClient(ctx, "", cloud.WithAmbientCredentials())
awsCfg, err := c.awsConfigProvider.GetConfig(ctx, meta.Region, awsconfig.WithAmbientCredentials())
if err != nil {
return nil, trace.Wrap(err)
}
return aws.GetIdentityWithClient(ctx, client)
client := c.awsClients.getSTSClient(awsCfg)
return aws.GetIdentityWithClientV2(ctx, client)
})
return identity, trace.Wrap(err)
}
func (c *credentialsChecker) checkAzure(ctx context.Context, database types.Database) {
allSubIDs, err := utils.FnCacheGet(ctx, c.cache, types.CloudAzure, func(ctx context.Context) ([]string, error) {
client, err := c.cloudClients.GetAzureSubscriptionClient()
client, err := c.azureClients.GetAzureSubscriptionClient()
if err != nil {
return nil, trace.Wrap(err)
}
+14 -2
View File
@@ -38,6 +38,7 @@ import (
rdsauth "github.com/aws/aws-sdk-go-v2/feature/rds/auth"
"github.com/aws/aws-sdk-go-v2/service/redshift"
rss "github.com/aws/aws-sdk-go-v2/service/redshiftserverless"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/aws-sdk-go/aws/credentials"
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
"github.com/gravitational/trace"
@@ -135,10 +136,16 @@ type rssClient interface {
GetCredentials(context.Context, *rss.GetCredentialsInput, ...func(*rss.Options)) (*rss.GetCredentialsOutput, error)
}
// stsClient defines a subset of the AWS STS client API.
type stsClient interface {
GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error)
}
// awsClientProvider is an AWS SDK client provider.
type awsClientProvider interface {
getRedshiftClient(cfg aws.Config, optFns ...func(*redshift.Options)) redshiftClient
getRedshiftServerlessClient(cfg aws.Config, optFns ...func(*rss.Options)) rssClient
getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient
}
type defaultAWSClients struct{}
@@ -151,6 +158,10 @@ func (defaultAWSClients) getRedshiftServerlessClient(cfg aws.Config, optFns ...f
return rss.NewFromConfig(cfg, optFns...)
}
func (defaultAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient {
return sts.NewFromConfig(cfg, optFns...)
}
// AuthConfig is the database access authenticator configuration.
type AuthConfig struct {
// AuthClient is the cluster auth client.
@@ -1141,12 +1152,13 @@ func (a *dbAuth) buildAWSRoleARNFromDatabaseUser(ctx context.Context, database t
awsAccountID = assumeRoleARN.AccountID
default:
a.cfg.Logger.DebugContext(ctx, "Fetching AWS Account ID to build role ARN")
stsClient, err := a.cfg.Clients.GetAWSSTSClient(ctx, dbAWS.Region, cloud.WithAmbientCredentials())
awsCfg, err := a.cfg.AWSConfigProvider.GetConfig(ctx, dbAWS.Region, awsconfig.WithAmbientCredentials())
if err != nil {
return "", trace.Wrap(err)
}
clt := a.cfg.awsClients.getSTSClient(awsCfg)
identity, err := awslib.GetIdentityWithClient(ctx, stsClient)
identity, err := awslib.GetIdentityWithClientV2(ctx, clt)
if err != nil {
return "", trace.Wrap(err)
}
+35 -24
View File
@@ -34,6 +34,7 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/redshift"
rss "github.com/aws/aws-sdk-go-v2/service/redshiftserverless"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
@@ -591,7 +592,7 @@ func TestAuthGetAWSTokenWithAssumedRole(t *testing.T) {
require.Equal(t, "some-user", query.Get("User"))
require.Equal(t, "host", query.Get("X-Amz-SignedHeaders"))
require.Equal(t, "token", query.Get("X-Amz-Security-Token"))
require.Equal(t, "arn:aws:iam::123456789012:role/RedisRole/20010203/ca-central-1/elasticache/aws4_request",
require.Equal(t, "FAKEACCESSKEYID/20010203/ca-central-1/elasticache/aws4_request",
query.Get("X-Amz-Credential"))
},
checkSTS: func(t *testing.T, stsMock *mocks.STSClient) {
@@ -622,6 +623,7 @@ func TestAuthGetAWSTokenWithAssumedRole(t *testing.T) {
rssClient: &mocks.RedshiftServerlessClient{
GetCredentialsOutput: mocks.RedshiftServerlessGetCredentialsOutput("IAM:some-user", "some-password", clock),
},
stsClient: fakeSTS,
},
})
require.NoError(t, err)
@@ -643,58 +645,55 @@ func TestGetAWSIAMCreds(t *testing.T) {
for name, tt := range map[string]struct {
db types.Database
stsMock *mocks.STSClientV1
stsMock *mocks.STSClient
username string
expectedKeyId string
expectedAssumedRoles []string
expectedExternalIDs []string
expectErr require.ErrorAssertionFunc
wantErrContains string
}{
"username is full role ARN": {
db: newMongoAtlasDatabase(t, types.AWS{}),
stsMock: &mocks.STSClientV1{},
stsMock: &mocks.STSClient{},
username: "arn:aws:iam::123456789012:role/role-name",
expectedKeyId: "arn:aws:iam::123456789012:role/role-name",
expectedAssumedRoles: []string{"arn:aws:iam::123456789012:role/role-name"},
expectedExternalIDs: []string{""},
expectErr: require.NoError,
},
"username is partial role ARN": {
db: newMongoAtlasDatabase(t, types.AWS{}),
stsMock: &mocks.STSClientV1{
// This is the role returned by the STS GetCallerIdentity.
ARN: "arn:aws:iam::222222222222:role/teleport-service-role",
stsMock: &mocks.STSClient{
STSClientV1: mocks.STSClientV1{
// This is the role returned by the STS GetCallerIdentity.
ARN: "arn:aws:iam::222222222222:role/teleport-service-role",
},
},
username: "role/role-name",
expectedKeyId: "arn:aws:iam::222222222222:role/role-name",
expectedAssumedRoles: []string{"arn:aws:iam::222222222222:role/role-name"},
expectedExternalIDs: []string{""},
expectErr: require.NoError,
},
"unable to fetch account ID": {
db: newMongoAtlasDatabase(t, types.AWS{}),
stsMock: &mocks.STSClientV1{
ARN: "",
stsMock: &mocks.STSClient{
Unauth: true,
},
username: "role/role-name",
expectErr: require.Error,
username: "role/role-name",
wantErrContains: "unauthorized",
},
"chained IAM role": {
db: newMongoAtlasDatabase(t, types.AWS{
ExternalID: "123123",
AssumeRoleARN: "arn:aws:iam::222222222222:role/teleport-service-role-external",
}),
stsMock: &mocks.STSClientV1{
ARN: "arn:aws:iam::111111111111:role/teleport-service-role",
stsMock: &mocks.STSClient{
STSClientV1: mocks.STSClientV1{
ARN: "arn:aws:iam::111111111111:role/teleport-service-role",
},
},
username: "role/role-name",
expectedKeyId: "arn:aws:iam::222222222222:role/role-name",
username: "role/role-name",
expectedAssumedRoles: []string{
"arn:aws:iam::222222222222:role/teleport-service-role-external",
"arn:aws:iam::222222222222:role/role-name",
},
expectedExternalIDs: []string{"123123", ""},
expectErr: require.NoError,
},
} {
t.Run(name, func(t *testing.T) {
@@ -703,15 +702,22 @@ func TestGetAWSIAMCreds(t *testing.T) {
AuthClient: new(authClientMock),
AccessPoint: new(accessPointMock),
Clients: &cloud.TestCloudClients{
STS: tt.stsMock,
STS: &tt.stsMock.STSClientV1,
},
AWSConfigProvider: &mocks.AWSConfigProvider{},
awsClients: fakeAWSClients{
stsClient: tt.stsMock,
},
})
require.NoError(t, err)
keyId, _, _, err := auth.GetAWSIAMCreds(ctx, tt.db, tt.username)
tt.expectErr(t, err)
require.Equal(t, tt.expectedKeyId, keyId)
if tt.wantErrContains != "" {
require.Error(t, err)
require.ErrorContains(t, err, tt.wantErrContains)
return
}
require.Equal(t, "FAKEACCESSKEYID", keyId)
require.ElementsMatch(t, tt.expectedAssumedRoles, tt.stsMock.GetAssumedRoleARNs())
require.ElementsMatch(t, tt.expectedExternalIDs, tt.stsMock.GetAssumedRoleExternalIDs())
})
@@ -1025,6 +1031,7 @@ func (m *imdsMock) GetType() types.InstanceMetadataType {
type fakeAWSClients struct {
redshiftClient redshiftClient
rssClient rssClient
stsClient stsClient
}
func (f fakeAWSClients) getRedshiftClient(aws.Config, ...func(*redshift.Options)) redshiftClient {
@@ -1034,3 +1041,7 @@ func (f fakeAWSClients) getRedshiftClient(aws.Config, ...func(*redshift.Options)
func (f fakeAWSClients) getRedshiftServerlessClient(cfg aws.Config, optFns ...func(*rss.Options)) rssClient {
return f.rssClient
}
func (f fakeAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient {
return f.stsClient
}
+17 -33
View File
@@ -29,8 +29,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/rds"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/gravitational/trace"
"golang.org/x/sync/errgroup"
@@ -106,6 +105,11 @@ type iamClient interface {
ListSAMLProviders(context.Context, *iam.ListSAMLProvidersInput, ...func(*iam.Options)) (*iam.ListSAMLProvidersOutput, error)
}
// stsClient defines a subset of the AWS STS client API.
type stsClient interface {
GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error)
}
// awsClientProvider provides AWS service API clients.
type awsClientProvider interface {
// getIAMClient provides an [iamClient].
@@ -114,6 +118,8 @@ type awsClientProvider interface {
getRDSClient(cfg aws.Config, optFns ...func(*rds.Options)) rdsClient
// getS3Client provides an [s3Client].
getS3Client(cfg aws.Config, optFns ...func(*s3.Options)) s3Client
// getSTSClient provides an [stsClient].
getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient
}
type defaultAWSClients struct{}
@@ -130,6 +136,10 @@ func (defaultAWSClients) getS3Client(cfg aws.Config, optFns ...func(*s3.Options)
return s3.NewFromConfig(cfg, optFns...)
}
func (defaultAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient {
return sts.NewFromConfig(cfg, optFns...)
}
// AssumeRole is the configuration for assuming an AWS role.
type AssumeRole struct {
// RoleARN is the ARN of the role to assume.
@@ -355,36 +365,9 @@ func (a *Fetcher) poll(ctx context.Context, features Features) (*Resources, erro
return result, trace.NewAggregate(errs...)
}
// getAWSOptions returns a list of AWSAssumeRoleOptionFn to be used when
// creating AWS clients.
func (a *Fetcher) getAWSOptions() []cloud.AWSOptionsFn {
opts := []cloud.AWSOptionsFn{
cloud.WithCredentialsMaybeIntegration(a.Config.Integration),
}
if a.Config.AssumeRole != nil {
opts = append(opts, cloud.WithAssumeRole(a.Config.AssumeRole.RoleARN, a.Config.AssumeRole.ExternalID))
}
const maxRetries = 10
opts = append(opts,
cloud.WithMaxRetries(maxRetries),
cloud.WithRetryer(
client.DefaultRetryer{
NumMaxRetries: maxRetries,
MinRetryDelay: time.Second,
MinThrottleDelay: time.Second,
MaxRetryDelay: 300 * time.Second,
MaxThrottleDelay: 300 * time.Second,
},
),
)
return opts
}
// getAWSV2Options returns a list of options to be used when
// getAWSOptions returns a list of options to be used when
// creating AWS clients with the v2 sdk.
func (a *Fetcher) getAWSV2Options() []awsconfig.OptionsFn {
func (a *Fetcher) getAWSOptions() []awsconfig.OptionsFn {
opts := []awsconfig.OptionsFn{
awsconfig.WithCredentialsMaybeIntegration(a.Config.Integration),
}
@@ -404,7 +387,7 @@ func (a *Fetcher) getAWSV2Options() []awsconfig.OptionsFn {
}
func (a *Fetcher) getAccountId(ctx context.Context) (string, error) {
stsClient, err := a.CloudClients.GetAWSSTSClient(
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because groups are global */
a.getAWSOptions()...,
@@ -412,9 +395,10 @@ func (a *Fetcher) getAccountId(ctx context.Context) (string, error) {
if err != nil {
return "", trace.Wrap(err)
}
stsClient := a.awsClients.getSTSClient(awsCfg)
input := &sts.GetCallerIdentityInput{}
req, err := stsClient.GetCallerIdentityWithContext(ctx, input)
req, err := stsClient.GetCallerIdentity(ctx, input)
if err != nil {
return "", trace.Wrap(err)
}
+2 -2
View File
@@ -89,7 +89,7 @@ func (a *Fetcher) fetchAWSEC2Instances(ctx context.Context) ([]*accessgraphv1alp
return h.Region == region && h.AccountId == a.AccountID
},
)
ec2Client, err := a.GetEC2Client(ctx, region, a.getAWSV2Options()...)
ec2Client, err := a.GetEC2Client(ctx, region, a.getAWSOptions()...)
if err != nil {
collectHosts(prevIterationEc2, trace.Wrap(err))
return nil
@@ -155,7 +155,7 @@ func (a *Fetcher) fetchInstanceProfiles(ctx context.Context) ([]*accessgraphv1al
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because users and groups are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return existing, trace.Wrap(err)
+1 -1
View File
@@ -103,7 +103,7 @@ func (a *Fetcher) fetchAWSSEKSClusters(ctx context.Context) (fetchAWSEKSClusters
for _, region := range a.Regions {
region := region
eG.Go(func() error {
eksClient, err := a.GetEKSClient(ctx, region, a.getAWSV2Options()...)
eksClient, err := a.GetEKSClient(ctx, region, a.getAWSOptions()...)
if err != nil {
collectClusters(nil, nil, nil, trace.Wrap(err))
return nil
@@ -94,7 +94,7 @@ func (a *Fetcher) fetchGroups(ctx context.Context) ([]*accessgraphv1alpha.AWSGro
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because groups are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return a.lastResult.Groups, trace.Wrap(err)
@@ -144,7 +144,7 @@ func (a *Fetcher) fetchGroupInlinePolicies(ctx context.Context, group *accessgra
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because users and groups are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return nil, trace.Wrap(err)
@@ -201,7 +201,7 @@ func (a *Fetcher) fetchGroupAttachedPolicies(ctx context.Context, group *accessg
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because users and groups are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return nil, trace.Wrap(err)
+2 -2
View File
@@ -37,7 +37,7 @@ func (a *Fetcher) pollAWSSAMLProviders(ctx context.Context, result *Resources, c
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because saml providers are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
collectErr(trace.Wrap(err, "failed to get AWS IAM client"))
@@ -138,7 +138,7 @@ func (a *Fetcher) pollAWSOIDCProviders(ctx context.Context, result *Resources, c
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because oidc providers are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
collectErr(trace.Wrap(err, "failed to get AWS IAM client"))
@@ -53,7 +53,7 @@ func (a *Fetcher) fetchPolicies(ctx context.Context) ([]*accessgraphv1alpha.AWSP
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because users and groups are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return nil, trace.Wrap(err)
+1 -1
View File
@@ -81,7 +81,7 @@ func (a *Fetcher) fetchAWSRDSDatabases(ctx context.Context) (
for _, region := range a.Regions {
region := region
eG.Go(func() error {
awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, region, a.getAWSV2Options()...)
awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, region, a.getAWSOptions()...)
if err != nil {
collectDBs(nil, trace.Wrap(err))
return nil
@@ -27,6 +27,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/rds"
rdstypes "github.com/aws/aws-sdk-go-v2/service/rds/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
@@ -221,6 +222,7 @@ type fakeAWSClients struct {
iamClient iamClient
rdsClient rdsClient
s3Client s3Client
stsClient stsClient
}
func (f fakeAWSClients) getIAMClient(cfg aws.Config, optFns ...func(*iam.Options)) iamClient {
@@ -234,3 +236,7 @@ func (f fakeAWSClients) getRDSClient(cfg aws.Config, optFns ...func(*rds.Options
func (f fakeAWSClients) getS3Client(cfg aws.Config, optFns ...func(*s3.Options)) s3Client {
return f.s3Client
}
func (f fakeAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient {
return f.stsClient
}
+3 -3
View File
@@ -89,7 +89,7 @@ func (a *Fetcher) fetchRoles(ctx context.Context) ([]*accessgraphv1alpha.AWSRole
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because roles are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return nil, trace.Wrap(err)
@@ -126,7 +126,7 @@ func (a *Fetcher) fetchRoleInlinePolicies(ctx context.Context, role *accessgraph
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because users and groups are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return nil, trace.Wrap(err)
@@ -175,7 +175,7 @@ func (a *Fetcher) fetchRoleAttachedPolicies(ctx context.Context, role *accessgra
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because users and groups are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return nil, trace.Wrap(err)
+2 -2
View File
@@ -218,7 +218,7 @@ func (a *Fetcher) getS3BucketDetails(ctx context.Context, bucket s3types.Bucket,
var errs []error
var details s3Details
awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, bucketRegion, a.getAWSV2Options()...)
awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, bucketRegion, a.getAWSOptions()...)
if err != nil {
errs = append(errs,
trace.Wrap(err, "failed to create s3 client for bucket %q", aws.ToString(bucket.Name)),
@@ -300,7 +300,7 @@ func (a *Fetcher) listS3Buckets(ctx context.Context) ([]s3types.Bucket, func(*st
}
// use any region to list buckets
awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, region, a.getAWSV2Options()...)
awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, region, a.getAWSOptions()...)
if err != nil {
return nil, nil, trace.Wrap(err)
}
+4 -4
View File
@@ -108,7 +108,7 @@ func (a *Fetcher) fetchUsers(ctx context.Context) ([]*accessgraphv1alpha.AWSUser
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because users are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return nil, trace.Wrap(err)
@@ -174,7 +174,7 @@ func (a *Fetcher) fetchUserInlinePolicies(ctx context.Context, user *accessgraph
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because users and groups are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return nil, trace.Wrap(err)
@@ -231,7 +231,7 @@ func (a *Fetcher) fetchUserAttachedPolicies(ctx context.Context, user *accessgra
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because users and groups are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return nil, trace.Wrap(err)
@@ -280,7 +280,7 @@ func (a *Fetcher) fetchGroupsForUser(ctx context.Context, user *accessgraphv1alp
awsCfg, err := a.AWSConfigProvider.GetConfig(
ctx,
"", /* region is empty because users and groups are global */
a.getAWSV2Options()...,
a.getAWSOptions()...,
)
if err != nil {
return nil, trace.Wrap(err)