Skip to content

Commit

Permalink
Merge pull request #1170 from hashicorp/f-role-chaining
Browse files Browse the repository at this point in the history
Adds IAM role chaining
  • Loading branch information
gdavison authored Sep 11, 2024
2 parents 3ec7b78 + 8516da5 commit 688693c
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 422 deletions.
313 changes: 155 additions & 158 deletions aws_config_test.go

Large diffs are not rendered by default.

102 changes: 54 additions & 48 deletions credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ AWS Error: %w`, err)
return nil, "", diags.Append(c.NewNoValidCredentialSourcesError(err))
}

if c.AssumeRole == nil {
if len(c.AssumeRole) == 0 {
return cfg.Credentials, creds.Source, diags
}

Expand Down Expand Up @@ -157,67 +157,73 @@ func assumeRoleCredentialsProvider(ctx context.Context, awsConfig aws.Config, c

logger := logging.RetrieveLogger(ctx)

ar := c.AssumeRole
var creds aws.CredentialsProvider

if ar.RoleARN == "" {
return nil, diags.AddError(
"Cannot assume IAM Role",
"IAM Role ARN not set",
)
}
total := len(c.AssumeRole)
for i, ar := range c.AssumeRole {
if ar.RoleARN == "" {
return nil, diags.AddError(
"Cannot assume IAM Role",
fmt.Sprintf("IAM Role ARN not set in assume role %d of %d", i+1, total),
)
}

logger.Info(ctx, "Assuming IAM Role", map[string]any{
"tf_aws.assume_role.role_arn": ar.RoleARN,
"tf_aws.assume_role.session_name": ar.SessionName,
"tf_aws.assume_role.external_id": ar.ExternalID,
"tf_aws.assume_role.source_identity": ar.SourceIdentity,
})
logger.Info(ctx, "Assuming IAM Role", map[string]any{
"tf_aws.assume_role.index": i,
"tf_aws.assume_role.role_arn": ar.RoleARN,
"tf_aws.assume_role.session_name": ar.SessionName,
"tf_aws.assume_role.external_id": ar.ExternalID,
"tf_aws.assume_role.source_identity": ar.SourceIdentity,
})

// When assuming a role, we need to first authenticate the base credentials above, then assume the desired role
client := stsClient(ctx, awsConfig, c)
// When assuming a role, we need to first authenticate the base credentials above, then assume the desired role
client := stsClient(ctx, awsConfig, c)

appCreds := stscreds.NewAssumeRoleProvider(client, ar.RoleARN, func(opts *stscreds.AssumeRoleOptions) {
opts.RoleSessionName = ar.SessionName
opts.Duration = ar.Duration
appCreds := stscreds.NewAssumeRoleProvider(client, ar.RoleARN, func(opts *stscreds.AssumeRoleOptions) {
opts.RoleSessionName = ar.SessionName
opts.Duration = ar.Duration

if ar.ExternalID != "" {
opts.ExternalID = aws.String(ar.ExternalID)
}
if ar.ExternalID != "" {
opts.ExternalID = aws.String(ar.ExternalID)
}

if ar.Policy != "" {
opts.Policy = aws.String(ar.Policy)
}
if ar.Policy != "" {
opts.Policy = aws.String(ar.Policy)
}

if len(ar.PolicyARNs) > 0 {
opts.PolicyARNs = getPolicyDescriptorTypes(ar.PolicyARNs)
}
if len(ar.PolicyARNs) > 0 {
opts.PolicyARNs = getPolicyDescriptorTypes(ar.PolicyARNs)
}

if len(ar.Tags) > 0 {
var tags []types.Tag
for k, v := range ar.Tags {
tag := types.Tag{
Key: aws.String(k),
Value: aws.String(v),
if len(ar.Tags) > 0 {
var tags []types.Tag
for k, v := range ar.Tags {
tag := types.Tag{
Key: aws.String(k),
Value: aws.String(v),
}
tags = append(tags, tag)
}
tags = append(tags, tag)
}

opts.Tags = tags
}
opts.Tags = tags
}

if len(ar.TransitiveTagKeys) > 0 {
opts.TransitiveTagKeys = ar.TransitiveTagKeys
}
if len(ar.TransitiveTagKeys) > 0 {
opts.TransitiveTagKeys = ar.TransitiveTagKeys
}

if ar.SourceIdentity != "" {
opts.SourceIdentity = aws.String(ar.SourceIdentity)
if ar.SourceIdentity != "" {
opts.SourceIdentity = aws.String(ar.SourceIdentity)
}
})
_, err := appCreds.Retrieve(ctx)
if err != nil {
return nil, diags.Append(newCannotAssumeRoleError(ar, err))
}
})
_, err := appCreds.Retrieve(ctx)
if err != nil {
return nil, diags.Append(c.NewCannotAssumeRoleError(err))
creds = aws.NewCredentialsCache(appCreds)
awsConfig.Credentials = creds
}
return aws.NewCredentialsCache(appCreds), nil
return creds, nil
}

func getPolicyDescriptorTypes(policyARNs []string) []types.PolicyDescriptorType {
Expand Down
53 changes: 50 additions & 3 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,63 @@
package awsbase

import (
"fmt"

"github.com/hashicorp/aws-sdk-go-base/v2/diag"
"github.com/hashicorp/aws-sdk-go-base/v2/internal/config"
)

// CannotAssumeRoleError occurs when AssumeRole cannot complete.
type CannotAssumeRoleError = config.CannotAssumeRoleError
// cannotAssumeRoleError occurs when AssumeRole cannot complete.
type cannotAssumeRoleError struct {
ar config.AssumeRole
err error
}

func (e cannotAssumeRoleError) Severity() diag.Severity {
return diag.SeverityError
}

func (e cannotAssumeRoleError) Summary() string {
return "Cannot assume IAM Role"
}

func (e cannotAssumeRoleError) Detail() string {
return fmt.Sprintf(`IAM Role (%s) cannot be assumed.
There are a number of possible causes of this - the most common are:
* The credentials used in order to assume the role are invalid
* The credentials do not have appropriate permission to assume the role
* The role ARN is not valid
Error: %s
`, e.ar.RoleARN, e.err)
}

func (e cannotAssumeRoleError) Equal(other diag.Diagnostic) bool {
ed, ok := other.(cannotAssumeRoleError)
if !ok {
return false
}

return ed.Summary() == e.Summary() && ed.Detail() == e.Detail()
}

func (e cannotAssumeRoleError) Err() error {
return e.err
}

func newCannotAssumeRoleError(ar AssumeRole, err error) cannotAssumeRoleError {
return cannotAssumeRoleError{
ar: ar,
err: err,
}
}

var _ diag.DiagnosticWithErr = cannotAssumeRoleError{}

// IsCannotAssumeRoleError returns true if the error contains the CannotAssumeRoleError type.
func IsCannotAssumeRoleError(diag diag.Diagnostic) bool {
_, ok := diag.(CannotAssumeRoleError)
_, ok := diag.(cannotAssumeRoleError)
return ok
}

Expand Down
4 changes: 2 additions & 2 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestIsCannotAssumeRoleError(t *testing.T) {
},
{
Name: "Top-level CannotAssumeRoleError",
Diag: CannotAssumeRoleError{},
Diag: cannotAssumeRoleError{},
Expected: true,
},
}
Expand Down Expand Up @@ -53,7 +53,7 @@ func TestIsNoValidCredentialSourcesError(t *testing.T) {
},
{
Name: "Top-level CannotAssumeRoleError",
Diag: CannotAssumeRoleError{},
Diag: cannotAssumeRoleError{},
},
{
Name: "Top-level NoValidCredentialSourcesError",
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type Config struct {
AccessKey string
AllowedAccountIds []string
APNInfo *APNInfo
AssumeRole *AssumeRole
AssumeRole []AssumeRole
AssumeRoleWithWebIdentity *AssumeRoleWithWebIdentity
Backoff retry.BackoffDelayer
CallerDocumentationURL string
Expand Down
52 changes: 0 additions & 52 deletions internal/config/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,58 +9,6 @@ import (
"github.com/hashicorp/aws-sdk-go-base/v2/diag"
)

// CannotAssumeRoleError occurs when AssumeRole cannot complete.
type CannotAssumeRoleError struct {
Config *Config
err error
}

func (e CannotAssumeRoleError) Severity() diag.Severity {
return diag.SeverityError
}

func (e CannotAssumeRoleError) Summary() string {
return "Cannot assume IAM Role"
}

func (e CannotAssumeRoleError) Detail() string {
if e.Config == nil || e.Config.AssumeRole == nil {
return fmt.Sprintf("Error: %s", e.err)
}

return fmt.Sprintf(`IAM Role (%s) cannot be assumed.
There are a number of possible causes of this - the most common are:
* The credentials used in order to assume the role are invalid
* The credentials do not have appropriate permission to assume the role
* The role ARN is not valid
Error: %s
`, e.Config.AssumeRole.RoleARN, e.err)
}

func (e CannotAssumeRoleError) Equal(other diag.Diagnostic) bool {
ed, ok := other.(CannotAssumeRoleError)
if !ok {
return false
}

return ed.Summary() == e.Summary() && ed.Detail() == e.Detail()
}

func (e CannotAssumeRoleError) Err() error {
return e.err
}

func (c *Config) NewCannotAssumeRoleError(err error) CannotAssumeRoleError {
return CannotAssumeRoleError{
Config: c,
err: err,
}
}

var _ diag.DiagnosticWithErr = CannotAssumeRoleError{}

// CannotAssumeRoleWithWebIdentityError occurs when AssumeRoleWithWebIdentity cannot complete.
type CannotAssumeRoleWithWebIdentityError struct {
Config *Config
Expand Down
2 changes: 2 additions & 0 deletions servicemocks/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (

MockStsAssumeRoleAccessKey = `AssumeRoleAccessKey`
MockStsAssumeRoleArn = `arn:aws:iam::555555555555:role/AssumeRole`
MockStsAssumeRoleArn2 = `arn:aws:iam::555555555555:role/AssumeRole2`
MockStsAssumeRoleExternalId = `AssumeRoleExternalId`
MockStsAssumeRoleInvalidResponseBodyInvalidClientTokenId = `<ErrorResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<Error>
Expand All @@ -53,6 +54,7 @@ const (
MockStsAssumeRolePolicyArn = `arn:aws:iam::555555555555:policy/AssumeRolePolicy1`
MockStsAssumeRoleSecretKey = `AssumeRoleSecretKey`
MockStsAssumeRoleSessionName = `AssumeRoleSessionName`
MockStsAssumeRoleSessionName2 = `AssumeRoleSessionName2`
MockStsAssumeRoleSessionToken = `AssumeRoleSessionToken`
MockStsAssumeRoleSourceIdentity = `AssumeRoleSourceIdentity`
MockStsAssumeRoleTagKey = `AssumeRoleTagKey`
Expand Down
Loading

0 comments on commit 688693c

Please sign in to comment.