From 305f11420885cb813b66986c274fd336e8db8374 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Fri, 2 Sep 2016 10:36:01 -0700 Subject: [PATCH] provider/aws: Add assume_role block to provider This replaces the previous `role_arn` with a block which looks like this: ``` provider "aws" { // secret key, access key etc assume_role { role_arn = "" session_name = "" external_id = "" } } ``` We also modify the configuration structure and read the values from the block if present into those values and adjust the call to AssumeRole to include the SessionName and ExternalID based on the values set in the configuration block. Finally we clean up the tests and add in missing error checks, and clean up the error handling logic in the Auth helper functions. --- builtin/providers/aws/auth_helpers.go | 86 +++--- builtin/providers/aws/auth_helpers_test.go | 78 +++--- builtin/providers/aws/config.go | 245 +++++++++--------- builtin/providers/aws/provider.go | 72 ++++- .../docs/providers/aws/index.html.markdown | 23 +- 5 files changed, 303 insertions(+), 201 deletions(-) diff --git a/builtin/providers/aws/auth_helpers.go b/builtin/providers/aws/auth_helpers.go index 6e48679bafa2..ce5fa599900d 100644 --- a/builtin/providers/aws/auth_helpers.go +++ b/builtin/providers/aws/auth_helpers.go @@ -1,6 +1,7 @@ package aws import ( + "errors" "fmt" "log" "os" @@ -18,7 +19,6 @@ import ( "github.com/aws/aws-sdk-go/service/sts" "github.com/hashicorp/errwrap" "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/go-multierror" ) func GetAccountId(iamconn *iam.IAM, stsconn *sts.STS, authProviderName string) (string, error) { @@ -77,7 +77,7 @@ func GetAccountId(iamconn *iam.IAM, stsconn *sts.STS, authProviderName string) ( } if len(outRoles.Roles) < 1 { - return "", fmt.Errorf("Failed getting account ID via 'iam:ListRoles': No roles available") + return "", errors.New("Failed getting account ID via 'iam:ListRoles': No roles available") } return parseAccountIdFromArn(*outRoles.Roles[0].Arn) @@ -95,8 +95,6 @@ func parseAccountIdFromArn(arn string) (string, error) { // environment in the case that they're not explicitly specified // in the Terraform configuration. func GetCredentials(c *Config) (*awsCredentials.Credentials, error) { - var errs []error - // build a chain provider, lazy-evaulated by aws-sdk providers := []awsCredentials.Provider{ &awsCredentials.StaticProvider{Value: awsCredentials.Value{ @@ -130,7 +128,7 @@ func GetCredentials(c *Config) (*awsCredentials.Credentials, error) { providers = append(providers, &ec2rolecreds.EC2RoleProvider{ Client: metadataClient, }) - log.Printf("[INFO] AWS EC2 instance detected via default metadata" + + log.Print("[INFO] AWS EC2 instance detected via default metadata" + " API endpoint, EC2RoleProvider added to the auth chain") } else { if usedEndpoint == "" { @@ -141,40 +139,68 @@ func GetCredentials(c *Config) (*awsCredentials.Credentials, error) { } } - if c.RoleArn != "" { - log.Printf("[INFO] attempting to assume role %s", c.RoleArn) + // This is the "normal" flow (i.e. not assuming a role) + if c.AssumeRoleARN == "" { + return awsCredentials.NewChainCredentials(providers), nil + } + + // Otherwise we need to construct and STS client with the main credentials, and verify + // that we can assume the defined role. + log.Printf("[INFO] Attempting to AssumeRole %s (SessionName: %q, ExternalId: %q)", + c.AssumeRoleARN, c.AssumeRoleSessionName, c.AssumeRoleExternalID) - creds := awsCredentials.NewChainCredentials(providers) - cp, err := creds.Get() - if err != nil { - if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" { - errs = append(errs, fmt.Errorf(`No valid credential sources found for AWS Provider. + creds := awsCredentials.NewChainCredentials(providers) + cp, err := creds.Get() + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" { + return nil, errors.New(`No valid credential sources found for AWS Provider. Please see https://terraform.io/docs/providers/aws/index.html for more information on - providing credentials for the AWS Provider`)) - } else { - errs = append(errs, fmt.Errorf("Error loading credentials for AWS Provider: %s", err)) - } - return nil, &multierror.Error{Errors: errs} + providing credentials for the AWS Provider`) } - log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName) + return nil, fmt.Errorf("Error loading credentials for AWS Provider: %s", err) + } + + log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName) - awsConfig := &aws.Config{ - Credentials: creds, - Region: aws.String(c.Region), - MaxRetries: aws.Int(c.MaxRetries), - HTTPClient: cleanhttp.DefaultClient(), - S3ForcePathStyle: aws.Bool(c.S3ForcePathStyle), + awsConfig := &aws.Config{ + Credentials: creds, + Region: aws.String(c.Region), + MaxRetries: aws.Int(c.MaxRetries), + HTTPClient: cleanhttp.DefaultClient(), + S3ForcePathStyle: aws.Bool(c.S3ForcePathStyle), + } + + stsclient := sts.New(session.New(awsConfig)) + assumeRoleProvider := &stscreds.AssumeRoleProvider{ + Client: stsclient, + RoleARN: c.AssumeRoleARN, + } + if c.AssumeRoleSessionName != "" { + assumeRoleProvider.RoleSessionName = c.AssumeRoleSessionName + } + if c.AssumeRoleExternalID != "" { + assumeRoleProvider.ExternalID = aws.String(c.AssumeRoleExternalID) + } + + providers = []awsCredentials.Provider{assumeRoleProvider} + + assumeRoleCreds := awsCredentials.NewChainCredentials(providers) + _, err = assumeRoleCreds.Get() + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" { + return nil, fmt.Errorf("The role %q cannot be assumed.\n\n" + + " There are a number of possible causes of this - the most common are:\n" + + " * The credentials used in order to assume the role are invalid\n" + + " * The credentials do not have appropriate permission to assume the role\n" + + " * The role ARN is not valid", + c.AssumeRoleARN) } - stsclient := sts.New(session.New(awsConfig)) - providers = []awsCredentials.Provider{&stscreds.AssumeRoleProvider{ - Client: stsclient, - RoleARN: c.RoleArn, - }} + return nil, fmt.Errorf("Error loading credentials for AWS Provider: %s", err) } - return awsCredentials.NewChainCredentials(providers), nil + return assumeRoleCreds, nil } func setOptionalEndpoint(cfg *aws.Config) string { diff --git a/builtin/providers/aws/auth_helpers_test.go b/builtin/providers/aws/auth_helpers_test.go index 8c5f60c532dc..f29d3f3e1418 100644 --- a/builtin/providers/aws/auth_helpers_test.go +++ b/builtin/providers/aws/auth_helpers_test.go @@ -51,7 +51,7 @@ func TestAWSGetAccountId_shouldBeValid_EC2RoleHasPriority(t *testing.T) { defer awsTs() iamEndpoints := []*iamEndpoint{ - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Response: &iamResponse{200, iamResponse_GetUser_valid, "text/xml"}, }, @@ -72,7 +72,7 @@ func TestAWSGetAccountId_shouldBeValid_EC2RoleHasPriority(t *testing.T) { func TestAWSGetAccountId_shouldBeValid_fromIamUser(t *testing.T) { iamEndpoints := []*iamEndpoint{ - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Response: &iamResponse{200, iamResponse_GetUser_valid, "text/xml"}, }, @@ -94,11 +94,11 @@ func TestAWSGetAccountId_shouldBeValid_fromIamUser(t *testing.T) { func TestAWSGetAccountId_shouldBeValid_fromGetCallerIdentity(t *testing.T) { iamEndpoints := []*iamEndpoint{ - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"}, }, - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=GetCallerIdentity&Version=2011-06-15"}, Response: &iamResponse{200, stsResponse_GetCallerIdentity_valid, "text/xml"}, }, @@ -119,15 +119,15 @@ func TestAWSGetAccountId_shouldBeValid_fromGetCallerIdentity(t *testing.T) { func TestAWSGetAccountId_shouldBeValid_fromIamListRoles(t *testing.T) { iamEndpoints := []*iamEndpoint{ - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"}, }, - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=GetCallerIdentity&Version=2011-06-15"}, Response: &iamResponse{403, stsResponse_GetCallerIdentity_unauthorized, "text/xml"}, }, - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"}, Response: &iamResponse{200, iamResponse_ListRoles_valid, "text/xml"}, }, @@ -148,11 +148,11 @@ func TestAWSGetAccountId_shouldBeValid_fromIamListRoles(t *testing.T) { func TestAWSGetAccountId_shouldBeValid_federatedRole(t *testing.T) { iamEndpoints := []*iamEndpoint{ - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Response: &iamResponse{400, iamResponse_GetUser_federatedFailure, "text/xml"}, }, - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"}, Response: &iamResponse{200, iamResponse_ListRoles_valid, "text/xml"}, }, @@ -173,11 +173,11 @@ func TestAWSGetAccountId_shouldBeValid_federatedRole(t *testing.T) { func TestAWSGetAccountId_shouldError_unauthorizedFromIam(t *testing.T) { iamEndpoints := []*iamEndpoint{ - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"}, }, - &iamEndpoint{ + { Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"}, Response: &iamResponse{403, iamResponse_ListRoles_unauthorized, "text/xml"}, }, @@ -221,17 +221,17 @@ func TestAWSGetCredentials_shouldError(t *testing.T) { c, err := GetCredentials(&cfg) if awsErr, ok := err.(awserr.Error); ok { if awsErr.Code() != "NoCredentialProviders" { - t.Fatalf("Expected NoCredentialProviders error") + t.Fatal("Expected NoCredentialProviders error") } } _, err = c.Get() if awsErr, ok := err.(awserr.Error); ok { if awsErr.Code() != "NoCredentialProviders" { - t.Fatalf("Expected NoCredentialProviders error") + t.Fatal("Expected NoCredentialProviders error") } } if err == nil { - t.Fatalf("Expected an error with empty env, keys, and IAM in AWS Config") + t.Fatal("Expected an error with empty env, keys, and IAM in AWS Config") } } @@ -257,16 +257,18 @@ func TestAWSGetCredentials_shouldBeStatic(t *testing.T) { } creds, err := GetCredentials(&cfg) - if creds == nil { - t.Fatalf("Expected a static creds provider to be returned") - } if err != nil { t.Fatalf("Error gettings creds: %s", err) } + if creds == nil { + t.Fatal("Expected a static creds provider to be returned") + } + v, err := creds.Get() if err != nil { t.Fatalf("Error gettings creds: %s", err) } + if v.AccessKeyID != c.Key { t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID) } @@ -295,12 +297,13 @@ func TestAWSGetCredentials_shouldIAM(t *testing.T) { cfg := Config{} creds, err := GetCredentials(&cfg) - if creds == nil { - t.Fatalf("Expected a static creds provider to be returned") - } if err != nil { t.Fatalf("Error gettings creds: %s", err) } + if creds == nil { + t.Fatal("Expected a static creds provider to be returned") + } + v, err := creds.Get() if err != nil { t.Fatalf("Error gettings creds: %s", err) @@ -346,12 +349,13 @@ func TestAWSGetCredentials_shouldIgnoreIAM(t *testing.T) { } creds, err := GetCredentials(&cfg) - if creds == nil { - t.Fatalf("Expected a static creds provider to be returned") - } if err != nil { t.Fatalf("Error gettings creds: %s", err) } + if creds == nil { + t.Fatal("Expected a static creds provider to be returned") + } + v, err := creds.Get() if err != nil { t.Fatalf("Error gettings creds: %s", err) @@ -379,6 +383,10 @@ func TestAWSGetCredentials_shouldErrorWithInvalidEndpoint(t *testing.T) { if err != nil { t.Fatalf("Error gettings creds: %s", err) } + if creds == nil { + t.Fatal("Expected a static creds provider to be returned") + } + v, err := creds.Get() if err == nil { t.Fatal("Expected error returned when getting creds w/ invalid EC2 endpoint") @@ -404,6 +412,9 @@ func TestAWSGetCredentials_shouldIgnoreInvalidEndpoint(t *testing.T) { if err != nil { t.Fatalf("Getting static credentials w/ invalid EC2 endpoint failed: %s", err) } + if creds == nil { + t.Fatal("Expected a static creds provider to be returned") + } if v.ProviderName != "StaticProvider" { t.Fatalf("Expected provider name to be %q, %q given", "StaticProvider", v.ProviderName) @@ -426,12 +437,13 @@ func TestAWSGetCredentials_shouldCatchEC2RoleProvider(t *testing.T) { defer ts() creds, err := GetCredentials(&Config{}) - if creds == nil { - t.Fatalf("Expected an EC2Role creds provider to be returned") - } if err != nil { t.Fatalf("Error gettings creds: %s", err) } + if creds == nil { + t.Fatal("Expected an EC2Role creds provider to be returned") + } + v, err := creds.Get() if err != nil { t.Fatalf("Expected no error when getting creds: %s", err) @@ -475,12 +487,13 @@ func TestAWSGetCredentials_shouldBeShared(t *testing.T) { } creds, err := GetCredentials(&Config{Profile: "myprofile", CredsFilename: file.Name()}) - if creds == nil { - t.Fatalf("Expected a provider chain to be returned") - } if err != nil { t.Fatalf("Error gettings creds: %s", err) } + if creds == nil { + t.Fatal("Expected a provider chain to be returned") + } + v, err := creds.Get() if err != nil { t.Fatalf("Error gettings creds: %s", err) @@ -505,12 +518,13 @@ func TestAWSGetCredentials_shouldBeENV(t *testing.T) { cfg := Config{} creds, err := GetCredentials(&cfg) - if creds == nil { - t.Fatalf("Expected a static creds provider to be returned") - } if err != nil { t.Fatalf("Error gettings creds: %s", err) } + if creds == nil { + t.Fatalf("Expected a static creds provider to be returned") + } + v, err := creds.Get() if err != nil { t.Fatalf("Error gettings creds: %s", err) diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index ddd8e49778e6..6ae8a254b724 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -2,6 +2,7 @@ package aws import ( "crypto/tls" + "errors" "fmt" "log" "net/http" @@ -54,7 +55,6 @@ import ( "github.com/aws/aws-sdk-go/service/sts" "github.com/hashicorp/errwrap" "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/terraform" ) @@ -66,9 +66,12 @@ type Config struct { Profile string Token string Region string - RoleArn string MaxRetries int + AssumeRoleARN string + AssumeRoleExternalID string + AssumeRoleSessionName string + AllowedAccountIds []interface{} ForbiddenAccountIds []interface{} @@ -136,152 +139,142 @@ type AWSClient struct { func (c *Config) Client() (interface{}, error) { // Get the auth and region. This can fail if keys/regions were not // specified and we're attempting to use the environment. - var errs []error - log.Println("[INFO] Building AWS region structure") err := c.ValidateRegion() if err != nil { - errs = append(errs, err) + return nil, err } var client AWSClient - if len(errs) == 0 { - // store AWS region in client struct, for region specific operations such as - // bucket storage in S3 - client.region = c.Region + // store AWS region in client struct, for region specific operations such as + // bucket storage in S3 + client.region = c.Region - log.Println("[INFO] Building AWS auth structure") - creds, err := GetCredentials(c) - if err != nil { - return nil, &multierror.Error{Errors: errs} - } - // Call Get to check for credential provider. If nothing found, we'll get an - // error, and we can present it nicely to the user - cp, err := creds.Get() - if err != nil { - if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" { - errs = append(errs, fmt.Errorf(`No valid credential sources found for AWS Provider. + log.Println("[INFO] Building AWS auth structure") + creds, err := GetCredentials(c) + if err != nil { + return nil, err + } + // Call Get to check for credential provider. If nothing found, we'll get an + // error, and we can present it nicely to the user + cp, err := creds.Get() + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" { + return nil, errors.New(`No valid credential sources found for AWS Provider. Please see https://terraform.io/docs/providers/aws/index.html for more information on - providing credentials for the AWS Provider`)) - } else { - errs = append(errs, fmt.Errorf("Error loading credentials for AWS Provider: %s", err)) - } - return nil, &multierror.Error{Errors: errs} + providing credentials for the AWS Provider`) } - log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName) + return nil, fmt.Errorf("Error loading credentials for AWS Provider: %s", err) + } - awsConfig := &aws.Config{ - Credentials: creds, - Region: aws.String(c.Region), - MaxRetries: aws.Int(c.MaxRetries), - HTTPClient: cleanhttp.DefaultClient(), - S3ForcePathStyle: aws.Bool(c.S3ForcePathStyle), - } + log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName) - if logging.IsDebugOrHigher() { - awsConfig.LogLevel = aws.LogLevel(aws.LogDebugWithHTTPBody) - awsConfig.Logger = awsLogger{} - } + awsConfig := &aws.Config{ + Credentials: creds, + Region: aws.String(c.Region), + MaxRetries: aws.Int(c.MaxRetries), + HTTPClient: cleanhttp.DefaultClient(), + S3ForcePathStyle: aws.Bool(c.S3ForcePathStyle), + } - if c.Insecure { - transport := awsConfig.HTTPClient.Transport.(*http.Transport) - transport.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, - } - } + if logging.IsDebugOrHigher() { + awsConfig.LogLevel = aws.LogLevel(aws.LogDebugWithHTTPBody) + awsConfig.Logger = awsLogger{} + } - // Set up base session - sess, err := session.NewSession(awsConfig) - if err != nil { - return nil, errwrap.Wrapf("Error creating AWS session: {{err}}", err) - } - sess.Handlers.Build.PushFrontNamed(addTerraformVersionToUserAgent) - - // Some services exist only in us-east-1, e.g. because they manage - // resources that can span across multiple regions, or because - // signature format v4 requires region to be us-east-1 for global - // endpoints: - // http://docs.aws.amazon.com/general/latest/gr/sigv4_changes.html - usEast1Sess := sess.Copy(&aws.Config{Region: aws.String("us-east-1")}) - - // Some services have user-configurable endpoints - awsEc2Sess := sess.Copy(&aws.Config{Endpoint: aws.String(c.Ec2Endpoint)}) - awsElbSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.ElbEndpoint)}) - awsIamSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.IamEndpoint)}) - awsS3Sess := sess.Copy(&aws.Config{Endpoint: aws.String(c.S3Endpoint)}) - dynamoSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.DynamoDBEndpoint)}) - kinesisSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.KinesisEndpoint)}) - - // These two services need to be set up early so we can check on AccountID - client.iamconn = iam.New(awsIamSess) - client.stsconn = sts.New(sess) - - if !c.SkipCredsValidation { - err = c.ValidateCredentials(client.stsconn) - if err != nil { - errs = append(errs, err) - return nil, &multierror.Error{Errors: errs} - } + if c.Insecure { + transport := awsConfig.HTTPClient.Transport.(*http.Transport) + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, } + } - if !c.SkipRequestingAccountId { - accountId, err := GetAccountId(client.iamconn, client.stsconn, cp.ProviderName) - if err == nil { - client.accountid = accountId - } + // Set up base session + sess, err := session.NewSession(awsConfig) + if err != nil { + return nil, errwrap.Wrapf("Error creating AWS session: {{err}}", err) + } + sess.Handlers.Build.PushFrontNamed(addTerraformVersionToUserAgent) + + // Some services exist only in us-east-1, e.g. because they manage + // resources that can span across multiple regions, or because + // signature format v4 requires region to be us-east-1 for global + // endpoints: + // http://docs.aws.amazon.com/general/latest/gr/sigv4_changes.html + usEast1Sess := sess.Copy(&aws.Config{Region: aws.String("us-east-1")}) + + // Some services have user-configurable endpoints + awsEc2Sess := sess.Copy(&aws.Config{Endpoint: aws.String(c.Ec2Endpoint)}) + awsElbSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.ElbEndpoint)}) + awsIamSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.IamEndpoint)}) + awsS3Sess := sess.Copy(&aws.Config{Endpoint: aws.String(c.S3Endpoint)}) + dynamoSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.DynamoDBEndpoint)}) + kinesisSess := sess.Copy(&aws.Config{Endpoint: aws.String(c.KinesisEndpoint)}) + + // These two services need to be set up early so we can check on AccountID + client.iamconn = iam.New(awsIamSess) + client.stsconn = sts.New(sess) + + if !c.SkipCredsValidation { + err = c.ValidateCredentials(client.stsconn) + if err != nil { + return nil, err } + } - authErr := c.ValidateAccountId(client.accountid) - if authErr != nil { - errs = append(errs, authErr) + if !c.SkipRequestingAccountId { + accountId, err := GetAccountId(client.iamconn, client.stsconn, cp.ProviderName) + if err == nil { + client.accountid = accountId } - - client.apigateway = apigateway.New(sess) - client.appautoscalingconn = applicationautoscaling.New(sess) - client.autoscalingconn = autoscaling.New(sess) - client.cfconn = cloudformation.New(sess) - client.cloudfrontconn = cloudfront.New(sess) - client.cloudtrailconn = cloudtrail.New(sess) - client.cloudwatchconn = cloudwatch.New(sess) - client.cloudwatcheventsconn = cloudwatchevents.New(sess) - client.cloudwatchlogsconn = cloudwatchlogs.New(sess) - client.codecommitconn = codecommit.New(usEast1Sess) - client.codedeployconn = codedeploy.New(sess) - client.dsconn = directoryservice.New(sess) - client.dynamodbconn = dynamodb.New(dynamoSess) - client.ec2conn = ec2.New(awsEc2Sess) - client.ecrconn = ecr.New(sess) - client.ecsconn = ecs.New(sess) - client.efsconn = efs.New(sess) - client.elasticacheconn = elasticache.New(sess) - client.elasticbeanstalkconn = elasticbeanstalk.New(sess) - client.elastictranscoderconn = elastictranscoder.New(sess) - client.elbconn = elb.New(awsElbSess) - client.elbv2conn = elbv2.New(awsElbSess) - client.emrconn = emr.New(sess) - client.esconn = elasticsearch.New(sess) - client.firehoseconn = firehose.New(sess) - client.glacierconn = glacier.New(sess) - client.kinesisconn = kinesis.New(kinesisSess) - client.kmsconn = kms.New(sess) - client.lambdaconn = lambda.New(sess) - client.opsworksconn = opsworks.New(usEast1Sess) - client.r53conn = route53.New(usEast1Sess) - client.rdsconn = rds.New(sess) - client.redshiftconn = redshift.New(sess) - client.simpledbconn = simpledb.New(sess) - client.s3conn = s3.New(awsS3Sess) - client.sesConn = ses.New(sess) - client.snsconn = sns.New(sess) - client.sqsconn = sqs.New(sess) - client.ssmconn = ssm.New(sess) } - if len(errs) > 0 { - return nil, &multierror.Error{Errors: errs} + authErr := c.ValidateAccountId(client.accountid) + if authErr != nil { + return nil, err } + client.apigateway = apigateway.New(sess) + client.appautoscalingconn = applicationautoscaling.New(sess) + client.autoscalingconn = autoscaling.New(sess) + client.cfconn = cloudformation.New(sess) + client.cloudfrontconn = cloudfront.New(sess) + client.cloudtrailconn = cloudtrail.New(sess) + client.cloudwatchconn = cloudwatch.New(sess) + client.cloudwatcheventsconn = cloudwatchevents.New(sess) + client.cloudwatchlogsconn = cloudwatchlogs.New(sess) + client.codecommitconn = codecommit.New(usEast1Sess) + client.codedeployconn = codedeploy.New(sess) + client.dsconn = directoryservice.New(sess) + client.dynamodbconn = dynamodb.New(dynamoSess) + client.ec2conn = ec2.New(awsEc2Sess) + client.ecrconn = ecr.New(sess) + client.ecsconn = ecs.New(sess) + client.efsconn = efs.New(sess) + client.elasticacheconn = elasticache.New(sess) + client.elasticbeanstalkconn = elasticbeanstalk.New(sess) + client.elastictranscoderconn = elastictranscoder.New(sess) + client.elbconn = elb.New(awsElbSess) + client.elbv2conn = elbv2.New(awsElbSess) + client.emrconn = emr.New(sess) + client.esconn = elasticsearch.New(sess) + client.firehoseconn = firehose.New(sess) + client.glacierconn = glacier.New(sess) + client.kinesisconn = kinesis.New(kinesisSess) + client.kmsconn = kms.New(sess) + client.lambdaconn = lambda.New(sess) + client.opsworksconn = opsworks.New(usEast1Sess) + client.r53conn = route53.New(usEast1Sess) + client.rdsconn = rds.New(sess) + client.redshiftconn = redshift.New(sess) + client.simpledbconn = simpledb.New(sess) + client.s3conn = s3.New(awsS3Sess) + client.sesConn = ses.New(sess) + client.snsconn = sns.New(sess) + client.sqsconn = sqs.New(sess) + client.ssmconn = ssm.New(sess) + return &client, nil } @@ -325,7 +318,7 @@ func (c *Config) ValidateAccountId(accountId string) error { return nil } - log.Printf("[INFO] Validating account ID") + log.Println("[INFO] Validating account ID") if c.ForbiddenAccountIds != nil { for _, id := range c.ForbiddenAccountIds { diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 89a5256eb36c..8730cccf05ba 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -3,6 +3,7 @@ package aws import ( "bytes" "fmt" + "log" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/mutexkv" @@ -39,6 +40,8 @@ func Provider() terraform.ResourceProvider { Description: descriptions["profile"], }, + "assume_role": assumeRoleSchema(), + "shared_credentials_file": { Type: schema.TypeString, Optional: true, @@ -64,13 +67,6 @@ func Provider() terraform.ResourceProvider { InputDefault: "us-east-1", }, - "role_arn": { - Type: schema.TypeString, - Optional: true, - Default: "", - Description: descriptions["role_arn"], - }, - "max_retries": { Type: schema.TypeInt, Optional: true, @@ -360,8 +356,6 @@ func init() { "profile": "The profile for API operations. If not set, the default profile\n" + "created with `aws configure` will be used.", - "role_arn": "The role to be assumed using the supplied access_key and secret_key", - "shared_credentials_file": "The path to the shared credentials file. If not set\n" + "this defaults to ~/.aws/credentials.", @@ -402,6 +396,14 @@ func init() { "i.e., http://s3.amazonaws.com/BUCKET/KEY. By default, the S3 client will\n" + "use virtual hosted bucket addressing when possible\n" + "(http://BUCKET.s3.amazonaws.com/KEY). Specific to the Amazon S3 service.", + + "assume_role_role_arn": "The ARN of an IAM role to assume prior to making API calls.", + + "assume_role_session_name": "The session name to use when assuming the role. If ommitted," + + " no session name is passed to the AssumeRole call.", + + "assume_role_external_id": "The external ID to use when assuming the role. If ommitted," + + " no external ID is passed to the AssumeRole call.", } } @@ -413,7 +415,6 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { CredsFilename: d.Get("shared_credentials_file").(string), Token: d.Get("token").(string), Region: d.Get("region").(string), - RoleArn: d.Get("role_arn").(string), MaxRetries: d.Get("max_retries").(int), DynamoDBEndpoint: d.Get("dynamodb_endpoint").(string), KinesisEndpoint: d.Get("kinesis_endpoint").(string), @@ -424,6 +425,18 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { S3ForcePathStyle: d.Get("s3_force_path_style").(bool), } + assumeRoleList := d.Get("assume_role").(*schema.Set).List() + if len(assumeRoleList) == 1 { + assumeRole := assumeRoleList[0].(map[string]interface{}) + config.AssumeRoleARN = assumeRole["role_arn"].(string) + config.AssumeRoleSessionName = assumeRole["session_name"].(string) + config.AssumeRoleExternalID = assumeRole["external_id"].(string) + log.Printf("[INFO] assume_role configuration set: (ARN: %q, SessionID: %q, ExternalID: %q)", + config.AssumeRoleARN, config.AssumeRoleSessionName, config.AssumeRoleExternalID) + } else { + log.Printf("[INFO] No assume_role block read from configuration") + } + endpointsSet := d.Get("endpoints").(*schema.Set) for _, endpointsSetI := range endpointsSet.List() { @@ -448,6 +461,45 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { // This is a global MutexKV for use within this plugin. var awsMutexKV = mutexkv.NewMutexKV() +func assumeRoleSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "role_arn": { + Type: schema.TypeString, + Optional: true, + Description: descriptions["assume_role_role_arn"], + }, + + "session_name": { + Type: schema.TypeString, + Optional: true, + Description: descriptions["assume_role_session_name"], + }, + + "external_id": { + Type: schema.TypeString, + Optional: true, + Description: descriptions["assume_role_external_id"], + }, + }, + }, + Set: assumeRoleToHash, + } +} + +func assumeRoleToHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["role_arn"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["session_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["external_id"].(string))) + return hashcode.String(buf.String()) +} + func endpointsSchema() *schema.Schema { return &schema.Schema{ Type: schema.TypeSet, diff --git a/website/source/docs/providers/aws/index.html.markdown b/website/source/docs/providers/aws/index.html.markdown index 31748fe4c4ce..a3ac1bd4a7c7 100644 --- a/website/source/docs/providers/aws/index.html.markdown +++ b/website/source/docs/providers/aws/index.html.markdown @@ -113,14 +113,18 @@ and defaults to `http://169.254.169.254:80/latest`. ###Assume role -If provided with a role arn, terraform will attempt to assume this role +If provided with a role ARN, Terraform will attempt to assume this role using the supplied credentials. Usage: ``` provider "aws" { - role_arn = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" + assume_role { + role_arn = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" + session_name = "SESSION_NAME" + external_id = "EXTERNAL_ID" + } } ``` @@ -143,6 +147,9 @@ The following arguments are supported in the `provider` block: * `profile` - (Optional) This is the AWS profile name as set in the shared credentials file. +* `assume_role` - (Optional) An `assume_role` block (documented below).`Only one + `assume_role` block may be in the configuration. + * `shared_credentials_file` = (Optional) This is the path to the shared credentials file. If this is not set and a profile is specified, ~/.aws/credentials will be used. @@ -200,7 +207,17 @@ The following arguments are supported in the `provider` block: S3 client will use virtual hosted bucket addressing when possible (http://BUCKET.s3.amazonaws.com/KEY). Specific to the Amazon S3 service. -Nested `endpoints` block supports the followings: +The nested `assume_role` block supports the following: + +* `role_arn` - (Required) The ARN of the role to assume. + +* `session_name` - (Optional) The session name to use when making the + AssumeRole call. + +* `external_id` - (Optional) The external ID to use when making the + AssumeRole call. + +Nested `endpoints` block supports the following: * `iam` - (Optional) Use this to override the default endpoint URL constructed from the `region`. It's typically used to connect to