diff --git a/builtin/providers/aws/auth_helpers.go b/builtin/providers/aws/auth_helpers.go index 6e48679ba..f7521c230 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) - - 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. - 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} - } - - 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), - } - - stsclient := sts.New(session.New(awsConfig)) - providers = []awsCredentials.Provider{&stscreds.AssumeRoleProvider{ - Client: stsclient, - RoleARN: c.RoleArn, - }} + // This is the "normal" flow (i.e. not assuming a role) + if c.AssumeRoleARN == "" { + return awsCredentials.NewChainCredentials(providers), nil } - 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" { + 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`) + } + + 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), + } + + 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) + } + + return nil, fmt.Errorf("Error loading credentials for AWS Provider: %s", err) + } + + 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 8c5f60c53..f29d3f3e1 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 ddd8e4977..6ae8a254b 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) + + awsConfig := &aws.Config{ + Credentials: creds, + Region: aws.String(c.Region), + MaxRetries: aws.Int(c.MaxRetries), + HTTPClient: cleanhttp.DefaultClient(), + S3ForcePathStyle: aws.Bool(c.S3ForcePathStyle), + } + + if logging.IsDebugOrHigher() { + awsConfig.LogLevel = aws.LogLevel(aws.LogDebugWithHTTPBody) + awsConfig.Logger = awsLogger{} + } + + 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) - if c.Insecure { - transport := awsConfig.HTTPClient.Transport.(*http.Transport) - transport.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, - } - } + // 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")}) - // Set up base session - sess, err := session.NewSession(awsConfig) + // 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, errwrap.Wrapf("Error creating AWS session: {{err}}", err) + return nil, 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.SkipRequestingAccountId { - accountId, err := GetAccountId(client.iamconn, client.stsconn, cp.ProviderName) - if err == nil { - client.accountid = accountId - } - } - - authErr := c.ValidateAccountId(client.accountid) - if authErr != nil { - errs = append(errs, authErr) - } - - 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} + if !c.SkipRequestingAccountId { + accountId, err := GetAccountId(client.iamconn, client.stsconn, cp.ProviderName) + if err == nil { + client.accountid = accountId + } } + 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 89a5256eb..8730cccf0 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 31748fe4c..a3ac1bd4a 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