Support multi-account teleport discovery bootstrap (#56693)

This change updates teleport discovery bootstrap to be able to
bootstrap policies/ssm documents in multiple accounts at once if
the required assume_role_arns are set in the discovery service's matcher config.
This commit is contained in:
Andrew Burke
2025-07-21 18:53:44 +00:00
committed by GitHub
parent f110b9c608
commit ed556b4d75
5 changed files with 686 additions and 232 deletions
+286 -130
View File
@@ -288,6 +288,9 @@ type awsConfigurator struct {
// actions list of the configurator actions, those are populated on the
// `build` function.
actions []configurators.ConfiguratorAction
// targetAccounts is a list of AWS account IDs that will be affected by
// configuration.
targetAccounts []string
}
type ConfiguratorConfig struct {
@@ -295,17 +298,73 @@ type ConfiguratorConfig struct {
Flags configurators.BootstrapFlags
// ServiceConfig Teleport database service config.
ServiceConfig *servicecfg.Config
// Policies instance of the `Policies` that the actions use.
Policies awslib.Policies
// Identity is the current AWS credentials chain identity.
Identity awslib.Identity
// awsConfigs is a cache of AWS configs.
awsConfigs *awsconfig.Cache
// identity is a cached AWS identity.
identity awslib.Identity
// getPolicies gets the AWS policy client for the specified assume role ARN
// and external ID. assumeRoleARN and externalID may be empty.
// Overridden in tests.
getPolicies func(ctx context.Context, assumeRoleARN, externalID string) (awslib.Policies, error)
// getIAMClient gets the AWS IAM client for the specified assume role ARN
// and external ID. assumeRoleARN and externalID may be empty.
// Overridden in tests.
getIAMClient func(ctx context.Context, assumeRoleARN, externalID string) (iamClient, error)
// getSSMClient gets the AWS SSM client for the specified assume role ARN,
// external ID, and region. assumeRoleARN and externalID may be empty.
// Overridden in tests.
getSSMClient func(ctx context.Context, region, assumeRoleARN, externalID string) (ssmClient, error)
}
// awsCfg is the configuration used for AWS service clients.
awsCfg *aws.Config
// iamClient is an AWS IAM client.
iamClient iamClient
// ssmClients is a mapping of region -> AWS SSM client
ssmClients map[string]ssmClient
// getAWSConfig gets the cached AWS config for the specified assume role ARN
// and external ID. assumeRoleARN and externalID may be empty.
func (c *ConfiguratorConfig) getAWSConfig(ctx context.Context, assumeRoleARN, externalID string) (aws.Config, error) {
if c.Flags.Manual {
return aws.Config{}, trace.BadParameter("GetAWSConfig not allowed in manual mode")
}
cfg, err := c.awsConfigs.GetConfig(
ctx,
"", /* get region from env > profile > fallback func */
awsconfig.WithFallbackRegionResolver(func(ctx context.Context) (string, error) {
return getFallbackRegion(ctx, os.Stdout, nil), nil
}),
awsconfig.WithAmbientCredentials(),
awsconfig.WithAssumeRole(assumeRoleARN, externalID),
)
if err != nil {
return aws.Config{}, trace.Wrap(err)
}
return cfg, nil
}
// getIdentity gets the cached AWS identity for the specified assume role ARN
// and external ID. assumeRoleARN and externalID may be empty.
func (c *ConfiguratorConfig) getIdentity(ctx context.Context, assumeRoleARN, externalID string) (awslib.Identity, error) {
// Assumed roles can be determined from the ARN.
if assumeRoleARN != "" {
identity, err := awslib.IdentityFromArn(assumeRoleARN)
return identity, trace.Wrap(err)
}
// Return a placeholder in manual mode.
if c.Flags.Manual {
identity, err := awslib.IdentityFromArn(buildIAMARN(targetIdentityARNSectionPlaceholder, targetIdentityARNSectionPlaceholder, "user", defaultAttachUser))
return identity, trace.Wrap(err)
}
// Check cache.
if c.identity != nil {
return c.identity, nil
}
// Fetch identity.
awsCfg, err := c.getAWSConfig(ctx, assumeRoleARN, externalID)
if err != nil {
return nil, trace.Wrap(err)
}
identity, err := awslib.GetIdentityWithClient(ctx, getSTSClient(awsCfg))
if err != nil {
return nil, trace.Wrap(err)
}
c.identity = identity
return identity, nil
}
type iamClient interface {
@@ -359,94 +418,114 @@ To avoid seeing this warning, please provide a region in your AWS config or thro
// CheckAndSetDefaults checks and set configuration default values.
func (c *ConfiguratorConfig) CheckAndSetDefaults() error {
ctx := context.Background()
if c.ServiceConfig == nil {
return trace.BadParameter("config file is required")
}
// When running the command in manual mode, we want to have zero dependency
// with AWS configurations (like awscli or environment variables), so that
// the user can run this command and generate the instructions without any
// pre-requisite.
if !c.Flags.Manual {
var err error
if c.awsCfg == nil {
cfg, err := awsconfig.GetConfig(
ctx,
"", /* get region from env > profile > fallback func */
awsconfig.WithFallbackRegionResolver(func(ctx context.Context) (string, error) {
return getFallbackRegion(ctx, os.Stdout, nil), nil
}),
awsconfig.WithAmbientCredentials(),
awsconfig.WithAssumeRole(c.Flags.AssumeRoleARN, c.Flags.ExternalID),
)
if err != nil {
return trace.Wrap(err)
}
c.awsCfg = &cfg
if c.awsConfigs == nil {
cache, err := awsconfig.NewCache()
if err != nil {
return trace.Wrap(err)
}
if c.iamClient == nil {
c.iamClient = iamutils.NewFromConfig(*c.awsCfg, func(o *iam.Options) {
c.awsConfigs = cache
}
if c.getPolicies == nil {
c.getPolicies = func(ctx context.Context, assumeRoleARN, externalID string) (awslib.Policies, error) {
awsCfg, err := c.getAWSConfig(ctx, assumeRoleARN, externalID)
if err != nil {
return nil, trace.Wrap(err)
}
identity, err := c.getIdentity(ctx, assumeRoleARN, externalID)
if err != nil {
return nil, trace.Wrap(err)
}
iamClient := iamutils.NewFromConfig(awsCfg, func(o *iam.Options) {
o.TracerProvider = smithyoteltracing.Adapt(otel.GetTracerProvider())
})
partition := identity.GetPartition()
accountID := identity.GetAccountID()
return awslib.NewPolicies(partition, accountID, iamClient), nil
}
if c.Identity == nil {
c.Identity, err = awslib.GetIdentityWithClient(ctx, getSTSClient(*c.awsCfg))
}
if c.getIAMClient == nil {
c.getIAMClient = func(ctx context.Context, assumeRoleARN, externalID string) (iamClient, error) {
awsCfg, err := c.getAWSConfig(ctx, assumeRoleARN, externalID)
if err != nil {
return trace.Wrap(err)
return nil, trace.Wrap(err)
}
return iamutils.NewFromConfig(awsCfg, func(o *iam.Options) {
o.TracerProvider = smithyoteltracing.Adapt(otel.GetTracerProvider())
}), nil
}
if c.ssmClients == nil {
c.ssmClients = make(map[string]ssmClient)
for _, matcher := range c.ServiceConfig.Discovery.AWSMatchers {
if !slices.Contains(matcher.Types, types.AWSMatcherEC2) {
continue
}
for _, region := range matcher.Regions {
if _, ok := c.ssmClients[region]; ok {
continue
}
withRegion := func(o *ssm.Options) {
o.Region = region
}
c.ssmClients[region] = ssm.NewFromConfig(*c.awsCfg, withRegion, func(o *ssm.Options) {
o.TracerProvider = smithyoteltracing.Adapt(otel.GetTracerProvider())
})
}
}
if c.getSSMClient == nil {
c.getSSMClient = func(ctx context.Context, region, assumeRoleARN, externalID string) (ssmClient, error) {
awsCfg, err := c.getAWSConfig(ctx, assumeRoleARN, externalID)
if err != nil {
return nil, trace.Wrap(err)
}
}
if c.Policies == nil {
partition := c.Identity.GetPartition()
accountID := c.Identity.GetAccountID()
iamClient := iamutils.NewFromConfig(*c.awsCfg, func(o *iam.Options) {
return ssm.NewFromConfig(awsCfg, func(o *ssm.Options) {
o.Region = region
o.TracerProvider = smithyoteltracing.Adapt(otel.GetTracerProvider())
})
c.Policies = awslib.NewPolicies(partition, accountID, iamClient)
}), nil
}
}
return nil
}
// getDistinctAssumedRoles gets a list of the distinct roles that can be assumed
// from the AWS matchers. If there are no AWS matchers, one AssumeRole
// is returned for the current identity.
func (c *ConfiguratorConfig) getDistinctAssumedRoles() []types.AssumeRole {
defaultAssumeRole := types.AssumeRole{
RoleARN: c.Flags.AssumeRoleARN,
ExternalID: c.Flags.ExternalID,
}
matchers := awsMatchersFromConfig(c.Flags, c.ServiceConfig)
if len(matchers) == 0 {
return []types.AssumeRole{defaultAssumeRole}
}
assumedRoles := make([]types.AssumeRole, 0, len(matchers))
for _, matcher := range matchers {
if ar := matcher.AssumeRole; ar == nil {
assumedRoles = append(assumedRoles, defaultAssumeRole)
} else {
assumedRoles = append(assumedRoles, *ar)
}
}
return apiutils.DeduplicateAny(assumedRoles, func(ar1, ar2 types.AssumeRole) bool {
return ar1.RoleARN == ar2.RoleARN && ar1.ExternalID == ar2.ExternalID
})
}
// NewAWSConfigurator creates an instance of awsConfigurator and builds its
// actions.
func NewAWSConfigurator(config ConfiguratorConfig) (configurators.Configurator, error) {
func NewAWSConfigurator(ctx context.Context, config ConfiguratorConfig) (configurators.Configurator, error) {
err := config.CheckAndSetDefaults()
if err != nil {
return nil, trace.Wrap(err)
}
actions, err := buildActions(config)
actions, err := buildActions(ctx, config)
if err != nil {
return nil, trace.Wrap(err)
}
return &awsConfigurator{config, actions}, nil
assumedRoles := config.getDistinctAssumedRoles()
targetAccounts := make([]string, 0, len(assumedRoles))
for _, ar := range assumedRoles {
identity, err := config.getIdentity(ctx, ar.RoleARN, ar.ExternalID)
if err != nil {
return nil, trace.Wrap(err)
}
targetAccounts = append(targetAccounts, identity.GetAccountID())
}
return &awsConfigurator{
config: config,
actions: actions,
targetAccounts: apiutils.Deduplicate(targetAccounts),
}, nil
}
// IsEmpty checks if the configurator has no actions.
@@ -461,7 +540,7 @@ func (a *awsConfigurator) Name() string {
// Description returns a brief description of the configurator.
func (a *awsConfigurator) Description() string {
return "Configure AWS for " + a.config.Flags.Service.Name()
return fmt.Sprintf("Configure AWS for %s for accounts: [%s]", a.config.Flags.Service.Name(), strings.Join(a.targetAccounts, ", "))
}
// Actions list of configurator actions.
@@ -478,11 +557,13 @@ type awsPolicyCreator struct {
policy *awslib.Policy
// formattedPolicy human-readable representation of the policy document.
formattedPolicy string
// accountID is the account that the policy will be created in.
accountID string
}
// Description returns what the action will perform.
func (a *awsPolicyCreator) Description() string {
return fmt.Sprintf("Create IAM Policy %q", a.policy.Name)
return fmt.Sprintf("[%s] Create IAM Policy %q", a.accountID, a.policy.Name)
}
// Details returns the policy document that will be created.
@@ -515,7 +596,7 @@ type awsPoliciesAttacher struct {
// Description returns what the action will perform.
func (a *awsPoliciesAttacher) Description() string {
return fmt.Sprintf("Attach IAM policies to %q", a.target.GetName())
return fmt.Sprintf("[%s] Attach IAM policies to %q", a.target.GetAccountID(), a.target.GetName())
}
// Details attacher doesn't have any extra detail, this function returns an
@@ -543,8 +624,8 @@ func (a *awsPoliciesAttacher) Execute(ctx context.Context, actionCtx *configurat
return nil
}
func buildDiscoveryActions(config ConfiguratorConfig, targetCfg targetConfig) ([]configurators.ConfiguratorAction, error) {
actions, err := buildCommonActions(config, targetCfg)
func buildDiscoveryActions(ctx context.Context, config ConfiguratorConfig, targetCfg targetConfig) ([]configurators.ConfiguratorAction, error) {
actions, err := buildCommonActions(ctx, config, targetCfg)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -554,11 +635,16 @@ func buildDiscoveryActions(config ConfiguratorConfig, targetCfg targetConfig) ([
return nil, err
}
actions = append(actions, buildSSMDocumentCreators(config.ssmClients, targetCfg, proxyAddr)...)
ssmActions, err := buildSSMDocumentCreators(ctx, config, targetCfg, proxyAddr)
if err != nil {
return nil, trace.Wrap(err)
}
actions = append(actions, ssmActions...)
return actions, nil
}
func buildCommonActions(config ConfiguratorConfig, targetCfg targetConfig) ([]configurators.ConfiguratorAction, error) {
func buildCommonActions(ctx context.Context, config ConfiguratorConfig, targetCfg targetConfig) ([]configurators.ConfiguratorAction, error) {
// Generate policies.
policy, err := buildPolicyDocument(config.Flags, targetCfg)
if err != nil {
@@ -577,81 +663,132 @@ func buildCommonActions(config ConfiguratorConfig, targetCfg targetConfig) ([]co
return nil, trace.Wrap(err)
}
var actions []configurators.ConfiguratorAction
var policies awslib.Policies
if !config.Flags.Manual {
policies, err = config.getPolicies(ctx, targetCfg.assumeRole.RoleARN, targetCfg.assumeRole.ExternalID)
if err != nil {
return nil, trace.Wrap(err)
}
}
// Create IAM Policy.
actions = append(actions, &awsPolicyCreator{
policies: config.Policies,
policies: policies,
policy: policy,
formattedPolicy: formattedPolicy,
accountID: targetCfg.identity.GetAccountID(),
})
// Attach the policy to the target.
actions = append(actions, &awsPoliciesAttacher{policies: config.Policies, target: targetCfg.identity})
actions = append(actions, &awsPoliciesAttacher{policies: policies, target: targetCfg.identity})
return actions, nil
}
// buildActions generates the policy documents and configurator actions.
func buildActions(config ConfiguratorConfig) ([]configurators.ConfiguratorAction, error) {
// Identity is going to be empty (`nil`) when running the command on
// `Manual` mode, place a wildcard to keep the generated policies valid.
accountID := targetIdentityARNSectionPlaceholder
partitionID := targetIdentityARNSectionPlaceholder
if config.Identity != nil {
accountID = config.Identity.GetAccountID()
partitionID = config.Identity.GetPartition()
func buildActions(ctx context.Context, config ConfiguratorConfig) ([]configurators.ConfiguratorAction, error) {
var allActions []configurators.ConfiguratorAction
for _, assumeRole := range config.getDistinctAssumedRoles() {
target, err := policiesTarget(ctx, config, assumeRole)
if err != nil {
var unreachableErr unreachablePolicyTargetError
if errors.As(err, &unreachableErr) {
fmt.Printf("⚠️ Skipping matchers with identity %q: %s\n", unreachableErr.from.GetName(), unreachableErr.Error())
continue
}
return nil, trace.Wrap(err)
}
targetCfg, err := getTargetConfig(config.Flags, config.ServiceConfig, target, assumeRole)
if err != nil {
return nil, trace.Wrap(err)
}
var actions []configurators.ConfiguratorAction
if config.Flags.Service.IsDiscovery() {
actions, err = buildDiscoveryActions(ctx, config, targetCfg)
} else {
actions, err = buildCommonActions(ctx, config, targetCfg)
}
if err != nil {
return nil, trace.Wrap(err)
}
allActions = append(allActions, actions...)
}
// Define the target and target type.
target, err := policiesTarget(config.Flags, accountID, partitionID, config.Identity, config.iamClient)
if err != nil {
return nil, trace.Wrap(err)
}
return allActions, nil
}
targetCfg, err := getTargetConfig(config.Flags, config.ServiceConfig, target)
if err != nil {
return nil, trace.Wrap(err)
}
// unreachablePolicyTargetError indicates that a target identity could not be
// accessed from another identity (typically due to them being in different
// accounts).
type unreachablePolicyTargetError struct {
target awslib.Identity
from awslib.Identity
}
if config.Flags.Service.IsDiscovery() {
return buildDiscoveryActions(config, targetCfg)
}
return buildCommonActions(config, targetCfg)
func (e unreachablePolicyTargetError) Error() string {
return fmt.Sprintf(
"%q is unreachable from %q",
e.target, e.from,
)
}
// policiesTarget defines which target and its type the policies will be
// attached to.
func policiesTarget(flags configurators.BootstrapFlags, accountID string, partitionID string, identity awslib.Identity, iamClient iamClient) (awslib.Identity, error) {
if flags.AttachToUser != "" {
userArn := flags.AttachToUser
if !arn.IsARN(flags.AttachToUser) {
userArn = buildIAMARN(partitionID, accountID, "user", flags.AttachToUser)
func policiesTarget(ctx context.Context, config ConfiguratorConfig, targetAssumeRole types.AssumeRole) (awslib.Identity, error) {
baseIdentity, err := config.getIdentity(ctx, targetAssumeRole.RoleARN, targetAssumeRole.ExternalID)
if err != nil {
return nil, trace.Wrap(err)
}
defaultPartitionID := baseIdentity.GetPartition()
defaultAccountID := baseIdentity.GetAccountID()
// Attach to user if provided.
attachToUser := config.Flags.AttachToUser
if attachToUser != "" {
userArn := attachToUser
if !arn.IsARN(attachToUser) {
userArn = buildIAMARN(defaultPartitionID, defaultAccountID, "user", attachToUser)
}
return awslib.IdentityFromArn(userArn)
}
if flags.AttachToRole != "" {
roleArn := flags.AttachToRole
if !arn.IsARN(flags.AttachToRole) {
roleArn = buildIAMARN(partitionID, accountID, "role", flags.AttachToRole)
userIdentity, err := awslib.IdentityFromArn(userArn)
if err != nil {
return nil, trace.Wrap(err)
}
return awslib.IdentityFromArn(roleArn)
if defaultAccountID != userIdentity.GetAccountID() {
return nil, unreachablePolicyTargetError{target: userIdentity, from: baseIdentity}
}
return userIdentity, nil
}
if identity == nil {
return awslib.IdentityFromArn(buildIAMARN(partitionID, accountID, "user", defaultAttachUser))
// Attach to role if provided.
attachToRole := config.Flags.AttachToRole
if attachToRole != "" {
roleArn := attachToRole
if !arn.IsARN(attachToRole) {
roleArn = buildIAMARN(defaultPartitionID, defaultAccountID, "role", attachToRole)
}
roleIdentity, err := awslib.IdentityFromArn(roleArn)
if err != nil {
return nil, trace.Wrap(err)
}
if defaultAccountID != roleIdentity.GetAccountID() {
return nil, unreachablePolicyTargetError{target: roleIdentity, from: baseIdentity}
}
return roleIdentity, nil
}
if identity.GetType() == awslib.ResourceTypeAssumedRole {
roleIdentity, err := getRoleARNForAssumedRole(iamClient, identity)
// Attach to current identity.
if baseIdentity.GetType() == awslib.ResourceTypeAssumedRole {
baseIAMClient, err := config.getIAMClient(ctx, targetAssumeRole.RoleARN, targetAssumeRole.ExternalID)
if err != nil {
return nil, trace.Wrap(err)
}
roleIdentity, err := getRoleARNForAssumedRole(baseIAMClient, baseIdentity)
if err != nil {
return nil, trace.Wrap(err)
}
return roleIdentity, nil
}
return identity, nil
return baseIdentity, nil
}
// buildIAMARN constructs an AWS IAM ARN string from the given partition,
@@ -686,6 +823,9 @@ func failedToResolveAssumeRoleARN(roleIdentity string) string {
// This is necessary since the assumed-role ARN does not include the role path,
// so we cannot reliably reconstruct the role ARN from the assumed-role ARN.
func getRoleARNForAssumedRole(iamClient iamClient, identity awslib.Identity) (awslib.Identity, error) {
if iamClient == nil {
return nil, trace.BadParameter("missing iamClient")
}
failedToResolveAssumeRoleARN := failedToResolveAssumeRoleARN(identity.GetName())
out, err := iamClient.GetRole(context.Background(), &iam.GetRoleInput{
@@ -810,22 +950,31 @@ func getProxyAddrFromConfig(cfg *servicecfg.Config, flags configurators.Bootstra
return "", trace.NotFound("proxy address not found, please provide --proxy, or set either teleport.proxy_server or proxy_service.public_addr in the teleport config")
}
func buildSSMDocumentCreators(ssm map[string]ssmClient, targetCfg targetConfig, proxyAddr string) []configurators.ConfiguratorAction {
func buildSSMDocumentCreators(ctx context.Context, config ConfiguratorConfig, targetCfg targetConfig, proxyAddr string) ([]configurators.ConfiguratorAction, error) {
var creators []configurators.ConfiguratorAction
for _, matcher := range targetCfg.awsMatchers {
if !slices.Contains(matcher.Types, types.AWSMatcherEC2) {
continue
}
for _, region := range matcher.Regions {
var ssmClient ssmClient
if !config.Flags.Manual {
var err error
ssmClient, err = config.getSSMClient(ctx, region, targetCfg.assumeRole.RoleARN, targetCfg.assumeRole.ExternalID)
if err != nil {
return nil, trace.Wrap(err)
}
}
ssmCreator := awsSSMDocumentCreator{
ssm: ssm[region],
Name: matcher.SSM.DocumentName,
Contents: awslib.EC2DiscoverySSMDocument(proxyAddr),
ssm: ssmClient,
Name: matcher.SSM.DocumentName,
Contents: awslib.EC2DiscoverySSMDocument(proxyAddr),
accountID: targetCfg.identity.GetAccountID(),
}
creators = append(creators, &ssmCreator)
}
}
return creators
return creators, nil
}
func isEC2AutoDiscoveryEnabled(flags configurators.BootstrapFlags, matchers []types.AWSMatcher) bool {
@@ -1104,14 +1253,15 @@ func buildARN(target awslib.Identity, service, resource string) string {
}
type awsSSMDocumentCreator struct {
Contents string
ssm ssmClient
Name string
Contents string
ssm ssmClient
Name string
accountID string
}
// Description returns what the action will perform.
func (a *awsSSMDocumentCreator) Description() string {
return fmt.Sprintf("Create SSM Document %q", a.Name)
return fmt.Sprintf("[%s] Create SSM Document %q", a.accountID, a.Name)
}
// Details returns the policy document that will be created.
@@ -1121,6 +1271,9 @@ func (a *awsSSMDocumentCreator) Details() string {
// Execute upserts the policy and store its ARN in the action context.
func (a *awsSSMDocumentCreator) Execute(ctx context.Context, actionCtx *configurators.ConfiguratorActionContext) error {
if a.ssm == nil {
return trace.BadParameter("ssm client not initialized")
}
_, err := a.ssm.CreateDocument(ctx, &ssm.CreateDocumentInput{
Content: aws.String(a.Contents),
Name: aws.String(a.Name),
@@ -1146,6 +1299,8 @@ func (a *awsSSMDocumentCreator) Execute(ctx context.Context, actionCtx *configur
type targetConfig struct {
// identity is the target identity.
identity awslib.Identity
// assumeRole is the role that should be assumed while configuring the target, if any.
assumeRole types.AssumeRole
// awsMatchers are the AWS matchers associated with the target identity.
awsMatchers []types.AWSMatcher
// databases are the databases associated with the target identity.
@@ -1157,7 +1312,7 @@ type targetConfig struct {
// getTargetConfig gets the resources that are relevant to the target identity
// from cli flags and file configuration.
func getTargetConfig(flags configurators.BootstrapFlags, cfg *servicecfg.Config, target awslib.Identity) (targetConfig, error) {
func getTargetConfig(flags configurators.BootstrapFlags, cfg *servicecfg.Config, target awslib.Identity, assumeRole types.AssumeRole) (targetConfig, error) {
forcedRoles, err := parseForcedAWSRoles(flags, target)
if err != nil {
return targetConfig{}, trace.Wrap(err)
@@ -1173,6 +1328,7 @@ func getTargetConfig(flags configurators.BootstrapFlags, cfg *servicecfg.Config,
}
return targetConfig{
identity: target,
assumeRole: assumeRole,
awsMatchers: matchersForTarget(awsMatchers, target, targetIsAssumeRole),
databases: databasesForTarget(databases, target, targetIsAssumeRole),
assumesAWSRoles: targetAssumesRoles,
+383 -85
View File
@@ -34,6 +34,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
@@ -52,7 +53,67 @@ var sortStringsTrans = cmp.Transformer("SortStrings", func(in []string) []string
return out
})
func TestGetIdentity(t *testing.T) {
t.Parallel()
tests := []struct {
name string
config ConfiguratorConfig
roleARN string
externalID string
assert assert.ErrorAssertionFunc
expectIdentity awslib.Identity
}{
{
name: "identity from assume role",
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{
Manual: true,
},
identity: identityFromArn(t, "arn:aws:iam::123456789012:role/not-this-one"),
},
roleARN: "arn:aws:iam::123456789012:role/example-role",
externalID: "foobar",
assert: assert.NoError,
expectIdentity: identityFromArn(t, "arn:aws:iam::123456789012:role/example-role"),
},
{
name: "placeholder identity in manual mode",
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{
Manual: true,
},
identity: identityFromArn(t, "arn:aws:iam::123456789012:role/not-this-one"),
},
assert: assert.NoError,
expectIdentity: identityFromArn(t, buildIAMARN(targetIdentityARNSectionPlaceholder, targetIdentityARNSectionPlaceholder, "user", defaultAttachUser)),
},
{
name: "cached identity",
config: ConfiguratorConfig{
identity: identityFromArn(t, "arn:aws:iam::123456789012:role/example-role"),
},
assert: assert.NoError,
expectIdentity: identityFromArn(t, "arn:aws:iam::123456789012:role/example-role"),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
identity, err := tc.config.getIdentity(t.Context(), tc.roleARN, tc.externalID)
tc.assert(t, err)
if tc.expectIdentity == nil {
assert.Nil(t, identity)
return
}
if assert.NotNil(t, identity) {
assert.Equal(t, tc.expectIdentity.String(), identity.String())
assert.Equal(t, tc.expectIdentity.GetType(), identity.GetType())
}
})
}
}
func TestAWSIAMDocuments(t *testing.T) {
t.Parallel()
userTarget, err := awslib.IdentityFromArn("arn:aws:iam::123456789012:user/example-user")
require.NoError(t, err)
@@ -1085,7 +1146,7 @@ func mustGetTargetConfig(t *testing.T, flags configurators.BootstrapFlags, roleT
require.NoError(t, fileConfig.CheckAndSetDefaults())
serviceCfg := servicecfg.MakeDefaultConfig()
require.NoError(t, config.ApplyFileConfig(fileConfig, serviceCfg))
targetCfg, err := getTargetConfig(flags, serviceCfg, roleTarget)
targetCfg, err := getTargetConfig(flags, serviceCfg, roleTarget, types.AssumeRole{})
require.NoError(t, err)
return targetCfg
}
@@ -1099,7 +1160,8 @@ func mustBuildPolicyDocument(t *testing.T, flags configurators.BootstrapFlags, t
}
func TestAWSPolicyCreator(t *testing.T) {
ctx := context.Background()
t.Parallel()
ctx := t.Context()
tests := map[string]struct {
returnError bool
@@ -1137,7 +1199,8 @@ func TestAWSPolicyCreator(t *testing.T) {
}
func TestAWSPoliciesAttacher(t *testing.T) {
ctx := context.Background()
t.Parallel()
ctx := t.Context()
userTarget, err := awslib.IdentityFromArn("arn:aws:iam::1234567:user/example-user")
require.NoError(t, err)
@@ -1196,33 +1259,56 @@ func TestAWSPoliciesAttacher(t *testing.T) {
}
}
func makeIAMClientGetter(expectedRoleARN, expectedExternalID string, clt iamClient) func(ctx context.Context, assumeRoleARN, externalID string) (iamClient, error) {
return func(ctx context.Context, assumeRoleARN, externalID string) (iamClient, error) {
if assumeRoleARN != expectedRoleARN || externalID != expectedExternalID {
return nil, trace.NotFound("no IAM client for assume role %q with external ID %q", expectedRoleARN, expectedExternalID)
}
return clt, nil
}
}
func makePoliciesGetter(expectedRoleARN, expectedExternalID string, policies awslib.Policies) func(ctx context.Context, assumeRoleARN, externalID string) (awslib.Policies, error) {
return func(ctx context.Context, assumeRoleARN, externalID string) (awslib.Policies, error) {
if assumeRoleARN != expectedRoleARN || externalID != expectedExternalID {
return nil, trace.NotFound("no policies client for assume role %q with external ID %q", expectedRoleARN, expectedExternalID)
}
return policies, nil
}
}
func makeSSMClientGetter(expectedRegion, expectedRoleARN, expectedExternalID string, ssm ssmClient) func(ctx context.Context, region, assumeRoleARN, externalID string) (ssmClient, error) {
return func(ctx context.Context, region, assumeRoleARN, externalID string) (ssmClient, error) {
if region != expectedRegion || assumeRoleARN != expectedRoleARN || externalID != expectedExternalID {
return nil, trace.NotFound("no IAM client for assume role %q with external ID %q", expectedRoleARN, expectedExternalID)
}
return ssm, nil
}
}
func TestAWSPoliciesTarget(t *testing.T) {
userIdentity, err := awslib.IdentityFromArn("arn:aws:iam::123456789012:user/example-user")
require.NoError(t, err)
roleIdentity, err := awslib.IdentityFromArn("arn:aws:iam::123456789012:role/example-role")
require.NoError(t, err)
assumedRoleIdentity, err := awslib.IdentityFromArn("arn:aws:sts::123456789012:assumed-role/example-role/i-12345")
require.NoError(t, err)
t.Parallel()
userIdentity := identityFromArn(t, "arn:aws:iam::123456789012:user/example-user")
roleIdentity := identityFromArn(t, "arn:aws:iam::123456789012:role/example-role")
assumedRoleIdentity := identityFromArn(t, "arn:aws:sts::123456789012:assumed-role/example-role/i-12345")
altAssumedRoleIdentity := identityFromArn(t, "arn:aws:sts::123456789012:assumed-role/alternate-role/i-12345")
defaultIdentity := identityFromArn(t, "arn:aws:iam::123456789012:user/me")
tests := map[string]struct {
flags configurators.BootstrapFlags
identity awslib.Identity
accountID string
partitionID string
config ConfiguratorConfig
assumeRole types.AssumeRole
targetType awslib.Identity
targetName string
targetAccountID string
targetPartitionID string
targetString string
iamClient iamClient
wantErrContains string
}{
"UserNameFromFlags": {
flags: configurators.BootstrapFlags{AttachToUser: "example-user"},
accountID: "123456",
partitionID: "aws",
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToUser: "example-user"},
identity: identityFromArn(t, buildIAMARN("aws", "123456", "user", defaultAttachUser)),
},
targetType: awslib.User{},
targetName: "example-user",
targetAccountID: "123456",
@@ -1230,9 +1316,10 @@ func TestAWSPoliciesTarget(t *testing.T) {
targetString: "arn:aws:iam::123456:user/example-user",
},
"UserNameWithPathFromFlags": {
flags: configurators.BootstrapFlags{AttachToUser: "/some/path/example-user"},
accountID: "123456",
partitionID: "aws",
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToUser: "/some/path/example-user"},
identity: identityFromArn(t, buildIAMARN("aws", "123456", "user", defaultAttachUser)),
},
targetType: awslib.User{},
targetName: "example-user",
targetAccountID: "123456",
@@ -1240,17 +1327,31 @@ func TestAWSPoliciesTarget(t *testing.T) {
targetString: "arn:aws:iam::123456:user/some/path/example-user",
},
"UserARNFromFlags": {
flags: configurators.BootstrapFlags{AttachToUser: "arn:aws:iam::123456789012:user/example-user"},
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToUser: "arn:aws:iam::123456789012:user/example-user"},
identity: defaultIdentity,
},
targetType: awslib.User{},
targetName: "example-user",
targetAccountID: "123456789012",
targetPartitionID: "aws",
targetString: "arn:aws:iam::123456789012:user/example-user",
},
"UserARNFromFlagsWrongAccount": {
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToUser: "arn:aws:iam::987654321098:user/alt-user"},
identity: defaultIdentity,
},
wantErrContains: unreachablePolicyTargetError{
target: identityFromArn(t, "arn:aws:iam::987654321098:user/alt-user"),
from: defaultIdentity,
}.Error(),
},
"RoleNameFromFlags": {
flags: configurators.BootstrapFlags{AttachToRole: "example-role"},
accountID: "123456789012",
partitionID: "aws",
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToRole: "example-role"},
identity: identityFromArn(t, buildIAMARN("aws", "123456789012", "user", defaultAttachUser)),
},
targetType: awslib.Role{},
targetName: "example-role",
targetAccountID: "123456789012",
@@ -1258,9 +1359,10 @@ func TestAWSPoliciesTarget(t *testing.T) {
targetString: "arn:aws:iam::123456789012:role/example-role",
},
"RoleNameWithPathFromFlags": {
flags: configurators.BootstrapFlags{AttachToRole: "/some/path/example-role"},
accountID: "123456789012",
partitionID: "aws",
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToRole: "/some/path/example-role"},
identity: identityFromArn(t, buildIAMARN("aws", "123456789012", "user", defaultAttachUser)),
},
targetType: awslib.Role{},
targetName: "example-role",
targetAccountID: "123456789012",
@@ -1268,16 +1370,31 @@ func TestAWSPoliciesTarget(t *testing.T) {
targetString: "arn:aws:iam::123456789012:role/some/path/example-role",
},
"RoleARNFromFlags": {
flags: configurators.BootstrapFlags{AttachToRole: "arn:aws:iam::123456789012:role/example-role"},
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToRole: "arn:aws:iam::123456789012:role/example-role"},
identity: defaultIdentity,
},
targetType: awslib.Role{},
targetName: "example-role",
targetAccountID: "123456789012",
targetPartitionID: "aws",
targetString: "arn:aws:iam::123456789012:role/example-role",
},
"RoleARNFromFlagsWrongAccount": {
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToRole: "arn:aws:iam::987654321098:role/alt-role"},
identity: defaultIdentity,
},
wantErrContains: unreachablePolicyTargetError{
target: identityFromArn(t, "arn:aws:iam::987654321098:role/alt-role"),
from: defaultIdentity,
}.Error(),
},
"UserFromIdentity": {
flags: configurators.BootstrapFlags{},
identity: userIdentity,
config: ConfiguratorConfig{
identity: userIdentity,
},
targetType: awslib.User{},
targetName: userIdentity.GetName(),
targetAccountID: userIdentity.GetAccountID(),
@@ -1285,8 +1402,9 @@ func TestAWSPoliciesTarget(t *testing.T) {
targetString: "arn:aws:iam::123456789012:user/example-user",
},
"RoleFromIdentity": {
flags: configurators.BootstrapFlags{},
identity: roleIdentity,
config: ConfiguratorConfig{
identity: roleIdentity,
},
targetType: awslib.Role{},
targetName: roleIdentity.GetName(),
targetAccountID: roleIdentity.GetAccountID(),
@@ -1294,9 +1412,6 @@ func TestAWSPoliciesTarget(t *testing.T) {
targetString: "arn:aws:iam::123456789012:role/example-role",
},
"DefaultTarget": {
flags: configurators.BootstrapFlags{},
accountID: "*",
partitionID: "*",
targetType: awslib.User{},
targetName: defaultAttachUser,
targetAccountID: "*",
@@ -1304,51 +1419,157 @@ func TestAWSPoliciesTarget(t *testing.T) {
targetString: "arn:*:iam::*:user/username",
},
"AssumedRoleIdentity": {
flags: configurators.BootstrapFlags{},
identity: assumedRoleIdentity,
config: ConfiguratorConfig{
identity: assumedRoleIdentity,
getIAMClient: makeIAMClientGetter("", "", &iamMock{
partition: "aws",
account: "123456789012",
}),
},
targetType: awslib.Role{},
targetName: assumedRoleIdentity.GetName(),
targetAccountID: assumedRoleIdentity.GetAccountID(),
targetPartitionID: assumedRoleIdentity.GetPartition(),
targetString: "arn:aws:iam::123456789012:role/example-role",
iamClient: &iamMock{partition: "aws", account: "123456789012"},
},
"AssumedRoleIdentityForRoleWithPath": {
flags: configurators.BootstrapFlags{},
identity: assumedRoleIdentity,
config: ConfiguratorConfig{
identity: assumedRoleIdentity,
getIAMClient: makeIAMClientGetter("", "", &iamMock{
partition: "aws",
account: "123456789012",
addPath: "/some/path/",
}),
},
targetType: awslib.Role{},
targetName: assumedRoleIdentity.GetName(),
targetAccountID: assumedRoleIdentity.GetAccountID(),
targetPartitionID: assumedRoleIdentity.GetPartition(),
targetString: "arn:aws:iam::123456789012:role/some/path/example-role",
iamClient: &iamMock{partition: "aws", account: "123456789012", addPath: "/some/path/"},
},
"AssumedRoleIdentityWithoutIAMPermissions": {
flags: configurators.BootstrapFlags{},
identity: assumedRoleIdentity,
targetType: awslib.Role{},
targetName: assumedRoleIdentity.GetName(),
targetAccountID: assumedRoleIdentity.GetAccountID(),
targetPartitionID: assumedRoleIdentity.GetPartition(),
targetString: "arn:aws:iam::123456789012:role/example-role",
iamClient: &iamMock{unauthorized: true},
wantErrContains: "policies cannot be attached to an assumed-role",
config: ConfiguratorConfig{
identity: assumedRoleIdentity,
getIAMClient: makeIAMClientGetter("", "", &iamMock{unauthorized: true}),
},
wantErrContains: "policies cannot be attached to an assumed-role",
},
"AssumedRoleIdentityWithRoleFromFlags": {
flags: configurators.BootstrapFlags{AttachToRole: "arn:aws:iam::123456789012:role/some/path/example-role"},
identity: assumedRoleIdentity,
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToRole: "arn:aws:iam::123456789012:role/some/path/example-role"},
identity: assumedRoleIdentity,
getIAMClient: makeIAMClientGetter("", "", &iamMock{unauthorized: true}),
},
targetType: awslib.Role{},
targetName: "example-role",
targetAccountID: "123456789012",
targetPartitionID: "aws",
targetString: "arn:aws:iam::123456789012:role/some/path/example-role",
iamClient: &iamMock{unauthorized: true},
},
"MatcherAssumeRole": {
config: ConfiguratorConfig{
getIAMClient: makeIAMClientGetter(assumedRoleIdentity.String(), "", &iamMock{
account: assumedRoleIdentity.GetAccountID(),
partition: assumedRoleIdentity.GetPartition(),
addPath: "/some/path/",
}),
identity: defaultIdentity,
},
assumeRole: types.AssumeRole{RoleARN: assumedRoleIdentity.String()},
targetType: awslib.Role{},
targetName: roleIdentity.GetName(),
targetAccountID: roleIdentity.GetAccountID(),
targetPartitionID: roleIdentity.GetPartition(),
targetString: "arn:aws:iam::123456789012:role/some/path/example-role",
},
"MatcherAssumeRoleWithUserFlag": {
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToUser: userIdentity.String()},
identity: defaultIdentity,
},
assumeRole: types.AssumeRole{RoleARN: assumedRoleIdentity.String()},
targetType: awslib.User{},
targetName: userIdentity.GetName(),
targetAccountID: userIdentity.GetAccountID(),
targetPartitionID: userIdentity.GetPartition(),
targetString: userIdentity.String(),
},
"MatcherAssumeRoleWithUserFlagPath": {
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToUser: "/some/path/example-user"},
identity: defaultIdentity,
},
assumeRole: types.AssumeRole{RoleARN: assumedRoleIdentity.String()},
targetType: awslib.User{},
targetName: userIdentity.GetName(),
targetAccountID: userIdentity.GetAccountID(),
targetPartitionID: userIdentity.GetPartition(),
targetString: "arn:aws:iam::123456789012:user/some/path/example-user",
},
"MatcherAssumeRoleWithUserFlagWrongAccount": {
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToUser: "arn:aws:iam::5678567856782:user/example-user"},
identity: defaultIdentity,
},
assumeRole: types.AssumeRole{RoleARN: assumedRoleIdentity.String()},
wantErrContains: unreachablePolicyTargetError{
target: identityFromArn(t, "arn:aws:iam::5678567856782:user/example-user"),
from: assumedRoleIdentity,
}.Error(),
},
"MatcherAssumeRoleWithRoleFlag": {
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToRole: roleIdentity.String()},
identity: defaultIdentity,
},
assumeRole: types.AssumeRole{RoleARN: altAssumedRoleIdentity.String()},
targetType: awslib.Role{},
targetName: roleIdentity.GetName(),
targetAccountID: roleIdentity.GetAccountID(),
targetPartitionID: roleIdentity.GetPartition(),
targetString: roleIdentity.String(),
},
"MatcherAssumeRoleWithRoleFlagPath": {
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToRole: "/some/path/example-role"},
identity: defaultIdentity,
},
assumeRole: types.AssumeRole{RoleARN: altAssumedRoleIdentity.String()},
targetType: awslib.Role{},
targetName: roleIdentity.GetName(),
targetAccountID: roleIdentity.GetAccountID(),
targetPartitionID: roleIdentity.GetPartition(),
targetString: "arn:aws:iam::123456789012:role/some/path/example-role",
},
"MatcherAssumeRoleWithRoleFlagWrongAccount": {
config: ConfiguratorConfig{
Flags: configurators.BootstrapFlags{AttachToRole: "arn:aws:iam::567856785678:role/example-role"},
identity: defaultIdentity,
},
assumeRole: types.AssumeRole{RoleARN: altAssumedRoleIdentity.String()},
wantErrContains: unreachablePolicyTargetError{
target: identityFromArn(t, "arn:aws:iam::567856785678:role/example-role"),
from: altAssumedRoleIdentity,
}.Error(),
},
"MatcherAssumeRoleWithoutIAMPermission": {
config: ConfiguratorConfig{
getIAMClient: makeIAMClientGetter(assumedRoleIdentity.String(), "", &iamMock{unauthorized: true}),
identity: defaultIdentity,
},
assumeRole: types.AssumeRole{RoleARN: assumedRoleIdentity.String()},
wantErrContains: failedToResolveAssumeRoleARN(assumedRoleIdentity.GetName()),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
target, err := policiesTarget(test.flags, test.accountID, test.partitionID, test.identity, test.iamClient)
test.config.ServiceConfig = &servicecfg.Config{}
require.NoError(t, test.config.CheckAndSetDefaults())
if test.config.identity == nil {
test.config.identity = identityFromArn(t, buildIAMARN(targetIdentityARNSectionPlaceholder, targetIdentityARNSectionPlaceholder, "user", defaultAttachUser))
}
target, err := policiesTarget(t.Context(), test.config, test.assumeRole)
if test.wantErrContains != "" {
require.ErrorContains(t, err, test.wantErrContains)
return
@@ -1371,13 +1592,17 @@ func identityFromArn(t *testing.T, arn string) awslib.Identity {
}
func TestAWSDocumentConfigurator(t *testing.T) {
t.Parallel()
var err error
ctx := context.Background()
ctx := t.Context()
fileConfig := &config.FileConfig{
Proxy: config.Proxy{
PublicAddr: []string{"proxy.example.org:443"},
},
Discovery: config.Discovery{
Service: config.Service{
EnabledFlag: "yes",
},
AWSMatchers: []config.AWSMatcher{
{
Types: []string{"ec2"},
@@ -1392,29 +1617,26 @@ func TestAWSDocumentConfigurator(t *testing.T) {
require.NoError(t, config.ApplyFileConfig(fileConfig, serviceConfig))
config := ConfiguratorConfig{
awsCfg: &aws.Config{},
iamClient: &iamMock{},
Identity: identityFromArn(t, "arn:aws:iam::1234567:role/example-role"),
ssmClients: map[string]ssmClient{
"eu-central-1": &ssmMock{
t: t,
expectedInput: &ssm.CreateDocumentInput{
Content: aws.String(awslib.EC2DiscoverySSMDocument("https://proxy.example.org:443")),
DocumentType: ssmtypes.DocumentTypeCommand,
DocumentFormat: ssmtypes.DocumentFormatYaml,
Name: aws.String("document"),
}},
},
getIAMClient: makeIAMClientGetter("", "", &iamMock{}),
identity: identityFromArn(t, "arn:aws:iam::1234567:role/example-role"),
getSSMClient: makeSSMClientGetter("eu-central-1", "", "", &ssmMock{
t: t,
expectedInput: &ssm.CreateDocumentInput{
Content: aws.String(awslib.EC2DiscoverySSMDocument("https://proxy.example.org:443")),
DocumentType: ssmtypes.DocumentTypeCommand,
DocumentFormat: ssmtypes.DocumentFormatYaml,
Name: aws.String("document"),
}}),
ServiceConfig: serviceConfig,
Flags: configurators.BootstrapFlags{
Service: configurators.DiscoveryService,
ForceEC2Permissions: true,
},
Policies: &policiesMock{
getPolicies: makePoliciesGetter("", "", &policiesMock{
upsertArn: "policies-arn",
},
}),
}
configurator, err := NewAWSConfigurator(config)
configurator, err := NewAWSConfigurator(t.Context(), config)
require.NoError(t, err)
require.False(t, configurator.IsEmpty())
@@ -1429,25 +1651,25 @@ func TestAWSDocumentConfigurator(t *testing.T) {
// TestAWSConfigurator tests all actions together.
func TestAWSConfigurator(t *testing.T) {
t.Parallel()
var err error
ctx := context.Background()
ctx := t.Context()
config := ConfiguratorConfig{
awsCfg: &aws.Config{},
iamClient: &iamMock{},
Identity: identityFromArn(t, "arn:aws:iam::1234567:role/example-role"),
ssmClients: map[string]ssmClient{"eu-central-1": &ssmMock{}},
getIAMClient: makeIAMClientGetter("", "", &iamMock{}),
identity: identityFromArn(t, "arn:aws:iam::1234567:role/example-role"),
getSSMClient: makeSSMClientGetter("eu-central-1", "", "", &ssmMock{}),
ServiceConfig: &servicecfg.Config{},
Flags: configurators.BootstrapFlags{
AttachToUser: "some-user",
ForceRDSPermissions: true,
},
Policies: &policiesMock{
getPolicies: makePoliciesGetter("", "", &policiesMock{
upsertArn: "policies-arn",
},
}),
}
configurator, err := NewAWSConfigurator(config)
configurator, err := NewAWSConfigurator(t.Context(), config)
require.NoError(t, err)
require.False(t, configurator.IsEmpty())
@@ -1462,7 +1684,7 @@ func TestAWSConfigurator(t *testing.T) {
config.Flags.ForceEC2Permissions = true
config.Flags.Proxy = "proxy.xyz"
configurator, err = NewAWSConfigurator(config)
configurator, err = NewAWSConfigurator(t.Context(), config)
require.NoError(t, err)
require.False(t, configurator.IsEmpty())
@@ -1751,7 +1973,7 @@ func TestExtractTargetConfig(t *testing.T) {
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got, err := getTargetConfig(tt.flags, tt.cfg, tt.target)
got, err := getTargetConfig(tt.flags, tt.cfg, tt.target, types.AssumeRole{})
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
@@ -1888,7 +2110,7 @@ func (m *ssmMock) CreateDocument(ctx context.Context, input *ssm.CreateDocumentI
cmp.Diff(m.expectedInput.Content, input.Content),
"Document content diff (-want +got)")
require.Empty(m.t,
cmp.Diff(m.expectedInput, input, cmpopts.IgnoreFields(ssm.CreateDocumentInput{}, "Content")),
cmp.Diff(m.expectedInput, input, cmpopts.IgnoreUnexported(ssm.CreateDocumentInput{})),
"Document diff (-want +got)")
return nil, nil
@@ -1925,6 +2147,7 @@ func (m mockLocalRegionGetter) GetRegion(context.Context) (string, error) {
}
func Test_getFallbackRegion(t *testing.T) {
t.Parallel()
tests := []struct {
name string
localRegionGetter localRegionGetter
@@ -1948,8 +2171,83 @@ func Test_getFallbackRegion(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
region := getFallbackRegion(context.Background(), io.Discard, test.localRegionGetter)
region := getFallbackRegion(t.Context(), io.Discard, test.localRegionGetter)
require.Equal(t, test.wantRegion, region)
})
}
}
func TestGetDistinctAssumedRoles(t *testing.T) {
t.Parallel()
defaultAssumeRole := types.AssumeRole{
RoleARN: "arn:aws:iam::123456789012:role/example-role",
ExternalID: "foobar",
}
tests := []struct {
name string
matchers []types.AWSMatcher
expectedAssumeRoles []types.AssumeRole
}{
{
name: "empty",
expectedAssumeRoles: []types.AssumeRole{defaultAssumeRole},
},
{
name: "multiple",
matchers: []types.AWSMatcher{
{},
{AssumeRole: &types.AssumeRole{RoleARN: "12345678", ExternalID: "foo"}},
{AssumeRole: &types.AssumeRole{RoleARN: "87654321"}},
},
expectedAssumeRoles: []types.AssumeRole{
defaultAssumeRole,
{RoleARN: "12345678", ExternalID: "foo"},
{RoleARN: "87654321"},
},
},
{
name: "filter out duplicates",
matchers: []types.AWSMatcher{
{},
{AssumeRole: &types.AssumeRole{RoleARN: "12345678"}},
{AssumeRole: &types.AssumeRole{RoleARN: "87654321", ExternalID: "foo"}},
{AssumeRole: &types.AssumeRole{RoleARN: "12345678"}},
{AssumeRole: &types.AssumeRole{RoleARN: "87654321", ExternalID: "foo"}},
{},
},
expectedAssumeRoles: []types.AssumeRole{
defaultAssumeRole,
{RoleARN: "12345678"},
{RoleARN: "87654321", ExternalID: "foo"},
},
},
{
name: "preserve duplicate arn when external id differs",
matchers: []types.AWSMatcher{
{AssumeRole: &types.AssumeRole{RoleARN: "12345678", ExternalID: "foo"}},
{AssumeRole: &types.AssumeRole{RoleARN: "12345678", ExternalID: "bar"}},
},
expectedAssumeRoles: []types.AssumeRole{
{RoleARN: "12345678", ExternalID: "foo"},
{RoleARN: "12345678", ExternalID: "bar"},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
config := ConfiguratorConfig{
Flags: configurators.BootstrapFlags{
Service: configurators.DiscoveryService,
AssumeRoleARN: defaultAssumeRole.RoleARN,
ExternalID: defaultAssumeRole.ExternalID,
},
ServiceConfig: &servicecfg.Config{
Discovery: servicecfg.DiscoveryConfig{
AWSMatchers: tc.matchers,
},
},
}
require.ElementsMatch(t, tc.expectedAssumeRoles, config.getDistinctAssumedRoles())
})
}
}
@@ -19,6 +19,8 @@
package configuratorbuilder
import (
"context"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/config"
@@ -29,7 +31,7 @@ import (
// BuildConfigurators reads the configuration and returns a list of
// configurators. Configurators that are "empty" are not returned.
func BuildConfigurators(flags configurators.BootstrapFlags) ([]configurators.Configurator, error) {
func BuildConfigurators(ctx context.Context, flags configurators.BootstrapFlags) ([]configurators.Configurator, error) {
fileConfig, err := config.ReadFromFile(flags.ConfigPath)
if err != nil {
return nil, trace.Wrap(err)
@@ -39,7 +41,7 @@ func BuildConfigurators(flags configurators.BootstrapFlags) ([]configurators.Con
return nil, trace.Wrap(err)
}
awsConfigurator, err := aws.NewAWSConfigurator(aws.ConfiguratorConfig{
awsConfigurator, err := aws.NewAWSConfigurator(ctx, aws.ConfiguratorConfig{
Flags: flags,
ServiceConfig: serviceCfg,
})
+9 -11
View File
@@ -138,11 +138,10 @@ func makeDatabaseServiceBootstrapFlagsWithDiscoveryServiceConfig(flags configure
// onConfigureDiscoveryBootstrap subcommand that bootstraps configuration for
// discovery agents.
func onConfigureDiscoveryBootstrap(flags configureDiscoveryBootstrapFlags) error {
func onConfigureDiscoveryBootstrap(ctx context.Context, flags configureDiscoveryBootstrapFlags) error {
fmt.Printf("Reading configuration at %q...\n", flags.config.ConfigPath)
ctx := context.TODO()
configurators, err := configuratorbuilder.BuildConfigurators(flags.config)
configurators, err := configuratorbuilder.BuildConfigurators(ctx, flags.config)
if err != nil {
return trace.Wrap(err)
}
@@ -152,7 +151,7 @@ func onConfigureDiscoveryBootstrap(flags configureDiscoveryBootstrapFlags) error
// service config.
if flags.config.Service.IsDiscovery() && flags.databaseServiceRole != "" {
config := makeDatabaseServiceBootstrapFlagsWithDiscoveryServiceConfig(flags)
dbConfigurators, err := configuratorbuilder.BuildConfigurators(config)
dbConfigurators, err := configuratorbuilder.BuildConfigurators(ctx, config)
if err != nil {
return trace.Wrap(err)
}
@@ -245,7 +244,7 @@ type configureDatabaseAWSPrintFlags struct {
// buildAWSConfigurator builds the database configurator used on AWS-specific
// commands.
func buildAWSConfigurator(manual bool, flags configureDatabaseAWSFlags) (configurators.Configurator, error) {
func buildAWSConfigurator(ctx context.Context, manual bool, flags configureDatabaseAWSFlags) (configurators.Configurator, error) {
err := flags.CheckAndSetDefaults()
if err != nil {
return nil, trace.Wrap(err)
@@ -284,7 +283,7 @@ func buildAWSConfigurator(manual bool, flags configureDatabaseAWSFlags) (configu
}
}
configurator, err := awsconfigurators.NewAWSConfigurator(awsconfigurators.ConfiguratorConfig{
configurator, err := awsconfigurators.NewAWSConfigurator(ctx, awsconfigurators.ConfiguratorConfig{
Flags: configuratorFlags,
ServiceConfig: &servicecfg.Config{},
})
@@ -297,8 +296,8 @@ func buildAWSConfigurator(manual bool, flags configureDatabaseAWSFlags) (configu
// onConfigureDatabasesAWSPrint is a subcommand used to print AWS IAM access
// Teleport requires to run databases discovery on AWS.
func onConfigureDatabasesAWSPrint(flags configureDatabaseAWSPrintFlags) error {
configurator, err := buildAWSConfigurator(true, flags.configureDatabaseAWSFlags)
func onConfigureDatabasesAWSPrint(ctx context.Context, flags configureDatabaseAWSPrintFlags) error {
configurator, err := buildAWSConfigurator(ctx, true, flags.configureDatabaseAWSFlags)
if err != nil {
return trace.Wrap(err)
}
@@ -335,9 +334,8 @@ type configureDatabaseAWSCreateFlags struct {
// onConfigureDatabasesAWSCreates is a subcommand used to create AWS IAM access
// for Teleport to run databases discovery on AWS.
func onConfigureDatabasesAWSCreate(flags configureDatabaseAWSCreateFlags) error {
ctx := context.TODO()
configurator, err := buildAWSConfigurator(false, flags.configureDatabaseAWSFlags)
func onConfigureDatabasesAWSCreate(ctx context.Context, flags configureDatabaseAWSCreateFlags) error {
configurator, err := buildAWSConfigurator(ctx, false, flags.configureDatabaseAWSFlags)
if err != nil {
return trace.Wrap(err)
}
+4 -4
View File
@@ -737,19 +737,19 @@ Examples:
case dbConfigureCreate.FullCommand():
err = onDumpDatabaseConfig(dbConfigCreateFlags)
case dbConfigureAWSPrintIAM.FullCommand():
err = onConfigureDatabasesAWSPrint(configureDatabaseAWSPrintFlags)
err = onConfigureDatabasesAWSPrint(ctx, configureDatabaseAWSPrintFlags)
case dbConfigureAWSCreateIAM.FullCommand():
err = onConfigureDatabasesAWSCreate(configureDatabaseAWSCreateFlags)
err = onConfigureDatabasesAWSCreate(ctx, configureDatabaseAWSCreateFlags)
case dbConfigureBootstrap.FullCommand():
configureDiscoveryBootstrapFlags.config.Service = configurators.DatabaseService
err = onConfigureDiscoveryBootstrap(configureDiscoveryBootstrapFlags)
err = onConfigureDiscoveryBootstrap(ctx, configureDiscoveryBootstrapFlags)
case systemdInstall.FullCommand():
err = onDumpSystemdUnitFile(systemdInstallFlags)
case installAutoDiscoverNode.FullCommand():
err = onInstallAutoDiscoverNode(installAutoDiscoverNodeFlags)
case discoveryBootstrapCmd.FullCommand():
configureDiscoveryBootstrapFlags.config.Service = configurators.DiscoveryService
err = onConfigureDiscoveryBootstrap(configureDiscoveryBootstrapFlags)
err = onConfigureDiscoveryBootstrap(ctx, configureDiscoveryBootstrapFlags)
case joinOpenSSH.FullCommand():
err = onJoinOpenSSH(ccf, conf)
case integrationConfDeployServiceCmd.FullCommand():