Merge pull request #8638 from hashicorp/f-aws-assume-role

provider/aws: Add support for AssumeRole prior to operations
This commit is contained in:
James Nugent 2016-09-03 13:04:03 -07:00 committed by GitHub
commit 94ca84e772
6 changed files with 377 additions and 183 deletions

View File

@ -1,6 +1,7 @@
package aws package aws
import ( import (
"errors"
"fmt" "fmt"
"log" "log"
"os" "os"
@ -11,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
awsCredentials "github.com/aws/aws-sdk-go/aws/credentials" awsCredentials "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/iam"
@ -75,7 +77,7 @@ func GetAccountId(iamconn *iam.IAM, stsconn *sts.STS, authProviderName string) (
} }
if len(outRoles.Roles) < 1 { 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) return parseAccountIdFromArn(*outRoles.Roles[0].Arn)
@ -92,7 +94,7 @@ func parseAccountIdFromArn(arn string) (string, error) {
// This function is responsible for reading credentials from the // This function is responsible for reading credentials from the
// environment in the case that they're not explicitly specified // environment in the case that they're not explicitly specified
// in the Terraform configuration. // in the Terraform configuration.
func GetCredentials(c *Config) *awsCredentials.Credentials { func GetCredentials(c *Config) (*awsCredentials.Credentials, error) {
// build a chain provider, lazy-evaulated by aws-sdk // build a chain provider, lazy-evaulated by aws-sdk
providers := []awsCredentials.Provider{ providers := []awsCredentials.Provider{
&awsCredentials.StaticProvider{Value: awsCredentials.Value{ &awsCredentials.StaticProvider{Value: awsCredentials.Value{
@ -126,7 +128,7 @@ func GetCredentials(c *Config) *awsCredentials.Credentials {
providers = append(providers, &ec2rolecreds.EC2RoleProvider{ providers = append(providers, &ec2rolecreds.EC2RoleProvider{
Client: metadataClient, 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") " API endpoint, EC2RoleProvider added to the auth chain")
} else { } else {
if usedEndpoint == "" { if usedEndpoint == "" {
@ -137,7 +139,68 @@ func GetCredentials(c *Config) *awsCredentials.Credentials {
} }
} }
return awsCredentials.NewChainCredentials(providers) // 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" {
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 { func setOptionalEndpoint(cfg *aws.Config) string {

View File

@ -51,7 +51,7 @@ func TestAWSGetAccountId_shouldBeValid_EC2RoleHasPriority(t *testing.T) {
defer awsTs() defer awsTs()
iamEndpoints := []*iamEndpoint{ iamEndpoints := []*iamEndpoint{
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"},
Response: &iamResponse{200, iamResponse_GetUser_valid, "text/xml"}, 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) { func TestAWSGetAccountId_shouldBeValid_fromIamUser(t *testing.T) {
iamEndpoints := []*iamEndpoint{ iamEndpoints := []*iamEndpoint{
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"},
Response: &iamResponse{200, iamResponse_GetUser_valid, "text/xml"}, 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) { func TestAWSGetAccountId_shouldBeValid_fromGetCallerIdentity(t *testing.T) {
iamEndpoints := []*iamEndpoint{ iamEndpoints := []*iamEndpoint{
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"},
Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"}, Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"},
}, },
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=GetCallerIdentity&Version=2011-06-15"}, Request: &iamRequest{"POST", "/", "Action=GetCallerIdentity&Version=2011-06-15"},
Response: &iamResponse{200, stsResponse_GetCallerIdentity_valid, "text/xml"}, 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) { func TestAWSGetAccountId_shouldBeValid_fromIamListRoles(t *testing.T) {
iamEndpoints := []*iamEndpoint{ iamEndpoints := []*iamEndpoint{
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"},
Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"}, Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"},
}, },
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=GetCallerIdentity&Version=2011-06-15"}, Request: &iamRequest{"POST", "/", "Action=GetCallerIdentity&Version=2011-06-15"},
Response: &iamResponse{403, stsResponse_GetCallerIdentity_unauthorized, "text/xml"}, Response: &iamResponse{403, stsResponse_GetCallerIdentity_unauthorized, "text/xml"},
}, },
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"}, Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"},
Response: &iamResponse{200, iamResponse_ListRoles_valid, "text/xml"}, 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) { func TestAWSGetAccountId_shouldBeValid_federatedRole(t *testing.T) {
iamEndpoints := []*iamEndpoint{ iamEndpoints := []*iamEndpoint{
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"},
Response: &iamResponse{400, iamResponse_GetUser_federatedFailure, "text/xml"}, Response: &iamResponse{400, iamResponse_GetUser_federatedFailure, "text/xml"},
}, },
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"}, Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"},
Response: &iamResponse{200, iamResponse_ListRoles_valid, "text/xml"}, 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) { func TestAWSGetAccountId_shouldError_unauthorizedFromIam(t *testing.T) {
iamEndpoints := []*iamEndpoint{ iamEndpoints := []*iamEndpoint{
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"},
Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"}, Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"},
}, },
&iamEndpoint{ {
Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"}, Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"},
Response: &iamResponse{403, iamResponse_ListRoles_unauthorized, "text/xml"}, Response: &iamResponse{403, iamResponse_ListRoles_unauthorized, "text/xml"},
}, },
@ -218,15 +218,20 @@ func TestAWSGetCredentials_shouldError(t *testing.T) {
defer resetEnv() defer resetEnv()
cfg := Config{} cfg := Config{}
c := GetCredentials(&cfg) c, err := GetCredentials(&cfg)
_, err := c.Get()
if awsErr, ok := err.(awserr.Error); ok { if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() != "NoCredentialProviders" { 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.Fatal("Expected NoCredentialProviders error")
} }
} }
if err == nil { 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")
} }
} }
@ -251,14 +256,19 @@ func TestAWSGetCredentials_shouldBeStatic(t *testing.T) {
Token: c.Token, Token: c.Token,
} }
creds := GetCredentials(&cfg) creds, err := GetCredentials(&cfg)
if creds == nil { if err != nil {
t.Fatalf("Expected a static creds provider to be returned") t.Fatalf("Error gettings creds: %s", err)
} }
if creds == nil {
t.Fatal("Expected a static creds provider to be returned")
}
v, err := creds.Get() v, err := creds.Get()
if err != nil { if err != nil {
t.Fatalf("Error gettings creds: %s", err) t.Fatalf("Error gettings creds: %s", err)
} }
if v.AccessKeyID != c.Key { if v.AccessKeyID != c.Key {
t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID) t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID)
} }
@ -286,9 +296,12 @@ func TestAWSGetCredentials_shouldIAM(t *testing.T) {
// An empty config, no key supplied // An empty config, no key supplied
cfg := Config{} cfg := Config{}
creds := GetCredentials(&cfg) creds, err := GetCredentials(&cfg)
if err != nil {
t.Fatalf("Error gettings creds: %s", err)
}
if creds == nil { if creds == nil {
t.Fatalf("Expected a static creds provider to be returned") t.Fatal("Expected a static creds provider to be returned")
} }
v, err := creds.Get() v, err := creds.Get()
@ -335,10 +348,14 @@ func TestAWSGetCredentials_shouldIgnoreIAM(t *testing.T) {
Token: c.Token, Token: c.Token,
} }
creds := GetCredentials(&cfg) creds, err := GetCredentials(&cfg)
if creds == nil { if err != nil {
t.Fatalf("Expected a static creds provider to be returned") t.Fatalf("Error gettings creds: %s", err)
} }
if creds == nil {
t.Fatal("Expected a static creds provider to be returned")
}
v, err := creds.Get() v, err := creds.Get()
if err != nil { if err != nil {
t.Fatalf("Error gettings creds: %s", err) t.Fatalf("Error gettings creds: %s", err)
@ -362,7 +379,14 @@ func TestAWSGetCredentials_shouldErrorWithInvalidEndpoint(t *testing.T) {
ts := invalidAwsEnv(t) ts := invalidAwsEnv(t)
defer ts() defer ts()
creds := GetCredentials(&Config{}) creds, err := GetCredentials(&Config{})
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() v, err := creds.Get()
if err == nil { if err == nil {
t.Fatal("Expected error returned when getting creds w/ invalid EC2 endpoint") t.Fatal("Expected error returned when getting creds w/ invalid EC2 endpoint")
@ -380,11 +404,17 @@ func TestAWSGetCredentials_shouldIgnoreInvalidEndpoint(t *testing.T) {
ts := invalidAwsEnv(t) ts := invalidAwsEnv(t)
defer ts() defer ts()
creds := GetCredentials(&Config{AccessKey: "accessKey", SecretKey: "secretKey"}) creds, err := GetCredentials(&Config{AccessKey: "accessKey", SecretKey: "secretKey"})
if err != nil {
t.Fatalf("Error gettings creds: %s", err)
}
v, err := creds.Get() v, err := creds.Get()
if err != nil { if err != nil {
t.Fatalf("Getting static credentials w/ invalid EC2 endpoint failed: %s", err) 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" { if v.ProviderName != "StaticProvider" {
t.Fatalf("Expected provider name to be %q, %q given", "StaticProvider", v.ProviderName) t.Fatalf("Expected provider name to be %q, %q given", "StaticProvider", v.ProviderName)
@ -406,10 +436,14 @@ func TestAWSGetCredentials_shouldCatchEC2RoleProvider(t *testing.T) {
ts := awsEnv(t) ts := awsEnv(t)
defer ts() defer ts()
creds := GetCredentials(&Config{}) creds, err := GetCredentials(&Config{})
if creds == nil { if err != nil {
t.Fatalf("Expected an EC2Role creds provider to be returned") t.Fatalf("Error gettings creds: %s", err)
} }
if creds == nil {
t.Fatal("Expected an EC2Role creds provider to be returned")
}
v, err := creds.Get() v, err := creds.Get()
if err != nil { if err != nil {
t.Fatalf("Expected no error when getting creds: %s", err) t.Fatalf("Expected no error when getting creds: %s", err)
@ -452,10 +486,14 @@ func TestAWSGetCredentials_shouldBeShared(t *testing.T) {
t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err) t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err)
} }
creds := GetCredentials(&Config{Profile: "myprofile", CredsFilename: file.Name()}) creds, err := GetCredentials(&Config{Profile: "myprofile", CredsFilename: file.Name()})
if creds == nil { if err != nil {
t.Fatalf("Expected a provider chain to be returned") t.Fatalf("Error gettings creds: %s", err)
} }
if creds == nil {
t.Fatal("Expected a provider chain to be returned")
}
v, err := creds.Get() v, err := creds.Get()
if err != nil { if err != nil {
t.Fatalf("Error gettings creds: %s", err) t.Fatalf("Error gettings creds: %s", err)
@ -479,10 +517,14 @@ func TestAWSGetCredentials_shouldBeENV(t *testing.T) {
defer resetEnv() defer resetEnv()
cfg := Config{} cfg := Config{}
creds := GetCredentials(&cfg) creds, err := GetCredentials(&cfg)
if err != nil {
t.Fatalf("Error gettings creds: %s", err)
}
if creds == nil { if creds == nil {
t.Fatalf("Expected a static creds provider to be returned") t.Fatalf("Expected a static creds provider to be returned")
} }
v, err := creds.Get() v, err := creds.Get()
if err != nil { if err != nil {
t.Fatalf("Error gettings creds: %s", err) t.Fatalf("Error gettings creds: %s", err)

View File

@ -2,6 +2,7 @@ package aws
import ( import (
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@ -54,7 +55,6 @@ import (
"github.com/aws/aws-sdk-go/service/sts" "github.com/aws/aws-sdk-go/service/sts"
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
"github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -68,6 +68,10 @@ type Config struct {
Region string Region string
MaxRetries int MaxRetries int
AssumeRoleARN string
AssumeRoleExternalID string
AssumeRoleSessionName string
AllowedAccountIds []interface{} AllowedAccountIds []interface{}
ForbiddenAccountIds []interface{} ForbiddenAccountIds []interface{}
@ -135,34 +139,33 @@ type AWSClient struct {
func (c *Config) Client() (interface{}, error) { func (c *Config) Client() (interface{}, error) {
// Get the auth and region. This can fail if keys/regions were not // Get the auth and region. This can fail if keys/regions were not
// specified and we're attempting to use the environment. // specified and we're attempting to use the environment.
var errs []error
log.Println("[INFO] Building AWS region structure") log.Println("[INFO] Building AWS region structure")
err := c.ValidateRegion() err := c.ValidateRegion()
if err != nil { if err != nil {
errs = append(errs, err) return nil, err
} }
var client AWSClient var client AWSClient
if len(errs) == 0 {
// store AWS region in client struct, for region specific operations such as // store AWS region in client struct, for region specific operations such as
// bucket storage in S3 // bucket storage in S3
client.region = c.Region client.region = c.Region
log.Println("[INFO] Building AWS auth structure") log.Println("[INFO] Building AWS auth structure")
creds := GetCredentials(c) creds, err := GetCredentials(c)
if err != nil {
return nil, err
}
// Call Get to check for credential provider. If nothing found, we'll get an // Call Get to check for credential provider. If nothing found, we'll get an
// error, and we can present it nicely to the user // error, and we can present it nicely to the user
cp, err := creds.Get() cp, err := creds.Get()
if err != nil { if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" { if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" {
errs = append(errs, fmt.Errorf(`No valid credential sources found for AWS Provider. 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 Please see https://terraform.io/docs/providers/aws/index.html for more information on
providing credentials for the AWS Provider`)) 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}
return nil, fmt.Errorf("Error loading credentials for AWS Provider: %s", err)
} }
log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName) log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName)
@ -216,8 +219,7 @@ func (c *Config) Client() (interface{}, error) {
if !c.SkipCredsValidation { if !c.SkipCredsValidation {
err = c.ValidateCredentials(client.stsconn) err = c.ValidateCredentials(client.stsconn)
if err != nil { if err != nil {
errs = append(errs, err) return nil, err
return nil, &multierror.Error{Errors: errs}
} }
} }
@ -230,7 +232,7 @@ func (c *Config) Client() (interface{}, error) {
authErr := c.ValidateAccountId(client.accountid) authErr := c.ValidateAccountId(client.accountid)
if authErr != nil { if authErr != nil {
errs = append(errs, authErr) return nil, err
} }
client.apigateway = apigateway.New(sess) client.apigateway = apigateway.New(sess)
@ -272,11 +274,6 @@ func (c *Config) Client() (interface{}, error) {
client.snsconn = sns.New(sess) client.snsconn = sns.New(sess)
client.sqsconn = sqs.New(sess) client.sqsconn = sqs.New(sess)
client.ssmconn = ssm.New(sess) client.ssmconn = ssm.New(sess)
}
if len(errs) > 0 {
return nil, &multierror.Error{Errors: errs}
}
return &client, nil return &client, nil
} }
@ -321,7 +318,7 @@ func (c *Config) ValidateAccountId(accountId string) error {
return nil return nil
} }
log.Printf("[INFO] Validating account ID") log.Println("[INFO] Validating account ID")
if c.ForbiddenAccountIds != nil { if c.ForbiddenAccountIds != nil {
for _, id := range c.ForbiddenAccountIds { for _, id := range c.ForbiddenAccountIds {

View File

@ -3,6 +3,7 @@ package aws
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"log"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/mutexkv" "github.com/hashicorp/terraform/helper/mutexkv"
@ -18,42 +19,44 @@ func Provider() terraform.ResourceProvider {
// The actual provider // The actual provider
return &schema.Provider{ return &schema.Provider{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"access_key": &schema.Schema{ "access_key": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",
Description: descriptions["access_key"], Description: descriptions["access_key"],
}, },
"secret_key": &schema.Schema{ "secret_key": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",
Description: descriptions["secret_key"], Description: descriptions["secret_key"],
}, },
"profile": &schema.Schema{ "profile": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",
Description: descriptions["profile"], Description: descriptions["profile"],
}, },
"shared_credentials_file": &schema.Schema{ "assume_role": assumeRoleSchema(),
"shared_credentials_file": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",
Description: descriptions["shared_credentials_file"], Description: descriptions["shared_credentials_file"],
}, },
"token": &schema.Schema{ "token": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",
Description: descriptions["token"], Description: descriptions["token"],
}, },
"region": &schema.Schema{ "region": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{ DefaultFunc: schema.MultiEnvDefaultFunc([]string{
@ -64,14 +67,14 @@ func Provider() terraform.ResourceProvider {
InputDefault: "us-east-1", InputDefault: "us-east-1",
}, },
"max_retries": &schema.Schema{ "max_retries": {
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Default: 11, Default: 11,
Description: descriptions["max_retries"], Description: descriptions["max_retries"],
}, },
"allowed_account_ids": &schema.Schema{ "allowed_account_ids": {
Type: schema.TypeSet, Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString}, Elem: &schema.Schema{Type: schema.TypeString},
Optional: true, Optional: true,
@ -79,7 +82,7 @@ func Provider() terraform.ResourceProvider {
Set: schema.HashString, Set: schema.HashString,
}, },
"forbidden_account_ids": &schema.Schema{ "forbidden_account_ids": {
Type: schema.TypeSet, Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString}, Elem: &schema.Schema{Type: schema.TypeString},
Optional: true, Optional: true,
@ -87,14 +90,14 @@ func Provider() terraform.ResourceProvider {
Set: schema.HashString, Set: schema.HashString,
}, },
"dynamodb_endpoint": &schema.Schema{ "dynamodb_endpoint": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",
Description: descriptions["dynamodb_endpoint"], Description: descriptions["dynamodb_endpoint"],
}, },
"kinesis_endpoint": &schema.Schema{ "kinesis_endpoint": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",
@ -103,35 +106,35 @@ func Provider() terraform.ResourceProvider {
"endpoints": endpointsSchema(), "endpoints": endpointsSchema(),
"insecure": &schema.Schema{ "insecure": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: false, Default: false,
Description: descriptions["insecure"], Description: descriptions["insecure"],
}, },
"skip_credentials_validation": &schema.Schema{ "skip_credentials_validation": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: false, Default: false,
Description: descriptions["skip_credentials_validation"], Description: descriptions["skip_credentials_validation"],
}, },
"skip_requesting_account_id": &schema.Schema{ "skip_requesting_account_id": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: false, Default: false,
Description: descriptions["skip_requesting_account_id"], Description: descriptions["skip_requesting_account_id"],
}, },
"skip_metadata_api_check": &schema.Schema{ "skip_metadata_api_check": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: false, Default: false,
Description: descriptions["skip_metadata_api_check"], Description: descriptions["skip_metadata_api_check"],
}, },
"s3_force_path_style": &schema.Schema{ "s3_force_path_style": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: false, Default: false,
@ -393,6 +396,14 @@ func init() {
"i.e., http://s3.amazonaws.com/BUCKET/KEY. By default, the S3 client will\n" + "i.e., http://s3.amazonaws.com/BUCKET/KEY. By default, the S3 client will\n" +
"use virtual hosted bucket addressing when possible\n" + "use virtual hosted bucket addressing when possible\n" +
"(http://BUCKET.s3.amazonaws.com/KEY). Specific to the Amazon S3 service.", "(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.",
} }
} }
@ -414,6 +425,18 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
S3ForcePathStyle: d.Get("s3_force_path_style").(bool), 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) endpointsSet := d.Get("endpoints").(*schema.Set)
for _, endpointsSetI := range endpointsSet.List() { for _, endpointsSetI := range endpointsSet.List() {
@ -438,33 +461,72 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
// This is a global MutexKV for use within this plugin. // This is a global MutexKV for use within this plugin.
var awsMutexKV = mutexkv.NewMutexKV() 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 { func endpointsSchema() *schema.Schema {
return &schema.Schema{ return &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Elem: &schema.Resource{ Elem: &schema.Resource{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"iam": &schema.Schema{ "iam": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",
Description: descriptions["iam_endpoint"], Description: descriptions["iam_endpoint"],
}, },
"ec2": &schema.Schema{ "ec2": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",
Description: descriptions["ec2_endpoint"], Description: descriptions["ec2_endpoint"],
}, },
"elb": &schema.Schema{ "elb": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",
Description: descriptions["elb_endpoint"], Description: descriptions["elb_endpoint"],
}, },
"s3": &schema.Schema{ "s3": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "", Default: "",

View File

@ -60,7 +60,7 @@ func s3Factory(conf map[string]string) (Client, error) {
kmsKeyID := conf["kms_key_id"] kmsKeyID := conf["kms_key_id"]
var errs []error var errs []error
creds := terraformAws.GetCredentials(&terraformAws.Config{ creds, err := terraformAws.GetCredentials(&terraformAws.Config{
AccessKey: conf["access_key"], AccessKey: conf["access_key"],
SecretKey: conf["secret_key"], SecretKey: conf["secret_key"],
Token: conf["token"], Token: conf["token"],
@ -69,7 +69,7 @@ func s3Factory(conf map[string]string) (Client, error) {
}) })
// Call Get to check for credential provider. If nothing found, we'll get an // Call Get to check for credential provider. If nothing found, we'll get an
// error, and we can present it nicely to the user // error, and we can present it nicely to the user
_, err := creds.Get() _, err = creds.Get()
if err != nil { if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" { if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" {
errs = append(errs, fmt.Errorf(`No valid credential sources found for AWS S3 remote. errs = append(errs, fmt.Errorf(`No valid credential sources found for AWS S3 remote.

View File

@ -111,6 +111,23 @@ You can provide custom metadata API endpoint via `AWS_METADATA_ENDPOINT` variabl
which expects the endpoint URL including the version which expects the endpoint URL including the version
and defaults to `http://169.254.169.254:80/latest`. 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
using the supplied credentials.
Usage:
```
provider "aws" {
assume_role {
role_arn = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME"
session_name = "SESSION_NAME"
external_id = "EXTERNAL_ID"
}
}
```
## Argument Reference ## Argument Reference
The following arguments are supported in the `provider` block: The following arguments are supported in the `provider` block:
@ -130,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 * `profile` - (Optional) This is the AWS profile name as set in the shared credentials
file. 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. * `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. If this is not set and a profile is specified, ~/.aws/credentials will be used.
@ -187,7 +207,17 @@ The following arguments are supported in the `provider` block:
S3 client will use virtual hosted bucket addressing when possible S3 client will use virtual hosted bucket addressing when possible
(http://BUCKET.s3.amazonaws.com/KEY). Specific to the Amazon S3 service. (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 * `iam` - (Optional) Use this to override the default endpoint
URL constructed from the `region`. It's typically used to connect to URL constructed from the `region`. It's typically used to connect to