diff --git a/builtin/providers/aws/auth_helpers.go b/builtin/providers/aws/auth_helpers.go
new file mode 100644
index 000000000..914c7e971
--- /dev/null
+++ b/builtin/providers/aws/auth_helpers.go
@@ -0,0 +1,134 @@
+package aws
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/awserr"
+ 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/ec2metadata"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/iam"
+ "github.com/hashicorp/go-cleanhttp"
+)
+
+func GetAccountId(iamconn *iam.IAM, authProviderName string) (string, error) {
+ // If we have creds from instance profile, we can use metadata API
+ if authProviderName == ec2rolecreds.ProviderName {
+ log.Println("[DEBUG] Trying to get account ID via AWS Metadata API")
+
+ cfg := &aws.Config{}
+ setOptionalEndpoint(cfg)
+ metadataClient := ec2metadata.New(session.New(cfg))
+ info, err := metadataClient.IAMInfo()
+ if err != nil {
+ // This can be triggered when no IAM Role is assigned
+ // or AWS just happens to return invalid response
+ return "", fmt.Errorf("Failed getting EC2 IAM info: %s", err)
+ }
+
+ return parseAccountIdFromArn(info.InstanceProfileArn)
+ }
+
+ // Then try IAM GetUser
+ log.Println("[DEBUG] Trying to get account ID via iam:GetUser")
+ outUser, err := iamconn.GetUser(nil)
+ if err == nil {
+ return parseAccountIdFromArn(*outUser.User.Arn)
+ }
+
+ // Then try IAM ListRoles
+ awsErr, ok := err.(awserr.Error)
+ // AccessDenied and ValidationError can be raised
+ // if credentials belong to federated profile, so we ignore these
+ if !ok || (awsErr.Code() != "AccessDenied" && awsErr.Code() != "ValidationError") {
+ return "", fmt.Errorf("Failed getting account ID via 'iam:GetUser': %s", err)
+ }
+
+ log.Printf("[DEBUG] Getting account ID via iam:GetUser failed: %s", err)
+ log.Println("[DEBUG] Trying to get account ID via iam:ListRoles instead")
+ outRoles, err := iamconn.ListRoles(&iam.ListRolesInput{
+ MaxItems: aws.Int64(int64(1)),
+ })
+ if err != nil {
+ return "", fmt.Errorf("Failed getting account ID via 'iam:ListRoles': %s", err)
+ }
+
+ if len(outRoles.Roles) < 1 {
+ return "", fmt.Errorf("Failed getting account ID via 'iam:ListRoles': No roles available")
+ }
+
+ return parseAccountIdFromArn(*outRoles.Roles[0].Arn)
+}
+
+func parseAccountIdFromArn(arn string) (string, error) {
+ parts := strings.Split(arn, ":")
+ if len(parts) < 5 {
+ return "", fmt.Errorf("Unable to parse ID from invalid ARN: %q", arn)
+ }
+ return parts[4], nil
+}
+
+// This function is responsible for reading credentials from the
+// environment in the case that they're not explicitly specified
+// in the Terraform configuration.
+func GetCredentials(key, secret, token, profile, credsfile string) *awsCredentials.Credentials {
+ // build a chain provider, lazy-evaulated by aws-sdk
+ providers := []awsCredentials.Provider{
+ &awsCredentials.StaticProvider{Value: awsCredentials.Value{
+ AccessKeyID: key,
+ SecretAccessKey: secret,
+ SessionToken: token,
+ }},
+ &awsCredentials.EnvProvider{},
+ &awsCredentials.SharedCredentialsProvider{
+ Filename: credsfile,
+ Profile: profile,
+ },
+ }
+
+ // Build isolated HTTP client to avoid issues with globally-shared settings
+ client := cleanhttp.DefaultClient()
+
+ // Keep the timeout low as we don't want to wait in non-EC2 environments
+ client.Timeout = 100 * time.Millisecond
+ cfg := &aws.Config{
+ HTTPClient: client,
+ }
+ usedEndpoint := setOptionalEndpoint(cfg)
+
+ // Real AWS should reply to a simple metadata request.
+ // We check it actually does to ensure something else didn't just
+ // happen to be listening on the same IP:Port
+ metadataClient := ec2metadata.New(session.New(cfg))
+ if metadataClient.Available() {
+ providers = append(providers, &ec2rolecreds.EC2RoleProvider{
+ Client: metadataClient,
+ })
+ log.Printf("[INFO] AWS EC2 instance detected via default metadata" +
+ " API endpoint, EC2RoleProvider added to the auth chain")
+ } else {
+ if usedEndpoint == "" {
+ usedEndpoint = "default location"
+ }
+ log.Printf("[WARN] Ignoring AWS metadata API endpoint at %s "+
+ "as it doesn't return any instance-id", usedEndpoint)
+ }
+
+ return awsCredentials.NewChainCredentials(providers)
+}
+
+func setOptionalEndpoint(cfg *aws.Config) string {
+ endpoint := os.Getenv("AWS_METADATA_URL")
+ if endpoint != "" {
+ log.Printf("[INFO] Setting custom metadata endpoint: %q", endpoint)
+ cfg.Endpoint = aws.String(endpoint)
+ return endpoint
+ }
+ return ""
+}
diff --git a/builtin/providers/aws/auth_helpers_test.go b/builtin/providers/aws/auth_helpers_test.go
new file mode 100644
index 000000000..b3f134039
--- /dev/null
+++ b/builtin/providers/aws/auth_helpers_test.go
@@ -0,0 +1,757 @@
+package aws
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/awserr"
+ 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/session"
+ "github.com/aws/aws-sdk-go/service/iam"
+)
+
+func TestAWSGetAccountId_shouldBeValid_fromEC2Role(t *testing.T) {
+ resetEnv := unsetEnv(t)
+ defer resetEnv()
+ // capture the test server's close method, to call after the test returns
+ awsTs := awsEnv(t)
+ defer awsTs()
+
+ iamEndpoints := []*iamEndpoint{}
+ ts, iamConn := getMockedAwsIamApi(iamEndpoints)
+ defer ts()
+
+ id, err := GetAccountId(iamConn, ec2rolecreds.ProviderName)
+ if err != nil {
+ t.Fatalf("Getting account ID from EC2 metadata API failed: %s", err)
+ }
+
+ expectedAccountId := "123456789013"
+ if id != expectedAccountId {
+ t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id)
+ }
+}
+
+func TestAWSGetAccountId_shouldBeValid_EC2RoleHasPriority(t *testing.T) {
+ resetEnv := unsetEnv(t)
+ defer resetEnv()
+ // capture the test server's close method, to call after the test returns
+ awsTs := awsEnv(t)
+ defer awsTs()
+
+ iamEndpoints := []*iamEndpoint{
+ &iamEndpoint{
+ Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"},
+ Response: &iamResponse{200, iamResponse_GetUser_valid, "text/xml"},
+ },
+ }
+ ts, iamConn := getMockedAwsIamApi(iamEndpoints)
+ defer ts()
+
+ id, err := GetAccountId(iamConn, ec2rolecreds.ProviderName)
+ if err != nil {
+ t.Fatalf("Getting account ID from EC2 metadata API failed: %s", err)
+ }
+
+ expectedAccountId := "123456789013"
+ if id != expectedAccountId {
+ t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id)
+ }
+}
+
+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"},
+ },
+ }
+ ts, iamConn := getMockedAwsIamApi(iamEndpoints)
+ defer ts()
+
+ id, err := GetAccountId(iamConn, "")
+ if err != nil {
+ t.Fatalf("Getting account ID via GetUser failed: %s", err)
+ }
+
+ expectedAccountId := "123456789012"
+ if id != expectedAccountId {
+ t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id)
+ }
+}
+
+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=ListRoles&MaxItems=1&Version=2010-05-08"},
+ Response: &iamResponse{200, iamResponse_ListRoles_valid, "text/xml"},
+ },
+ }
+ ts, iamConn := getMockedAwsIamApi(iamEndpoints)
+ defer ts()
+
+ id, err := GetAccountId(iamConn, "")
+ if err != nil {
+ t.Fatalf("Getting account ID via ListRoles failed: %s", err)
+ }
+
+ expectedAccountId := "123456789012"
+ if id != expectedAccountId {
+ t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id)
+ }
+}
+
+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"},
+ },
+ }
+ ts, iamConn := getMockedAwsIamApi(iamEndpoints)
+ defer ts()
+
+ id, err := GetAccountId(iamConn, "")
+ if err != nil {
+ t.Fatalf("Getting account ID via ListRoles failed: %s", err)
+ }
+
+ expectedAccountId := "123456789012"
+ if id != expectedAccountId {
+ t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id)
+ }
+}
+
+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"},
+ },
+ }
+ ts, iamConn := getMockedAwsIamApi(iamEndpoints)
+ defer ts()
+
+ id, err := GetAccountId(iamConn, "")
+ if err == nil {
+ t.Fatal("Expected error when getting account ID")
+ }
+
+ if id != "" {
+ t.Fatalf("Expected no account ID, given: %s", id)
+ }
+}
+
+func TestAWSParseAccountIdFromArn(t *testing.T) {
+ validArn := "arn:aws:iam::101636750127:instance-profile/aws-elasticbeanstalk-ec2-role"
+ expectedId := "101636750127"
+ id, err := parseAccountIdFromArn(validArn)
+ if err != nil {
+ t.Fatalf("Expected no error when parsing valid ARN: %s", err)
+ }
+ if id != expectedId {
+ t.Fatalf("Parsed id doesn't match with expected (%q != %q)", id, expectedId)
+ }
+
+ invalidArn := "blablah"
+ id, err = parseAccountIdFromArn(invalidArn)
+ if err == nil {
+ t.Fatalf("Expected error when parsing invalid ARN (%q)", invalidArn)
+ }
+}
+
+func TestAWSGetCredentials_shouldError(t *testing.T) {
+ resetEnv := unsetEnv(t)
+ defer resetEnv()
+ cfg := Config{}
+
+ c := GetCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename)
+ _, err := c.Get()
+ if awsErr, ok := err.(awserr.Error); ok {
+ if awsErr.Code() != "NoCredentialProviders" {
+ t.Fatalf("Expected NoCredentialProviders error")
+ }
+ }
+ if err == nil {
+ t.Fatalf("Expected an error with empty env, keys, and IAM in AWS Config")
+ }
+}
+
+func TestAWSGetCredentials_shouldBeStatic(t *testing.T) {
+ simple := []struct {
+ Key, Secret, Token string
+ }{
+ {
+ Key: "test",
+ Secret: "secret",
+ }, {
+ Key: "test",
+ Secret: "test",
+ Token: "test",
+ },
+ }
+
+ for _, c := range simple {
+ cfg := Config{
+ AccessKey: c.Key,
+ SecretKey: c.Secret,
+ Token: c.Token,
+ }
+
+ creds := GetCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename)
+ 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)
+ }
+ if v.AccessKeyID != c.Key {
+ t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID)
+ }
+ if v.SecretAccessKey != c.Secret {
+ t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", c.Secret, v.SecretAccessKey)
+ }
+ if v.SessionToken != c.Token {
+ t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", c.Token, v.SessionToken)
+ }
+ }
+}
+
+// TestAWSGetCredentials_shouldIAM is designed to test the scenario of running Terraform
+// from an EC2 instance, without environment variables or manually supplied
+// credentials.
+func TestAWSGetCredentials_shouldIAM(t *testing.T) {
+ // clear AWS_* environment variables
+ resetEnv := unsetEnv(t)
+ defer resetEnv()
+
+ // capture the test server's close method, to call after the test returns
+ ts := awsEnv(t)
+ defer ts()
+
+ // An empty config, no key supplied
+ cfg := Config{}
+
+ creds := GetCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename)
+ 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)
+ }
+ if v.AccessKeyID != "somekey" {
+ t.Fatalf("AccessKeyID mismatch, expected: (somekey), got (%s)", v.AccessKeyID)
+ }
+ if v.SecretAccessKey != "somesecret" {
+ t.Fatalf("SecretAccessKey mismatch, expected: (somesecret), got (%s)", v.SecretAccessKey)
+ }
+ if v.SessionToken != "sometoken" {
+ t.Fatalf("SessionToken mismatch, expected: (sometoken), got (%s)", v.SessionToken)
+ }
+}
+
+// TestAWSGetCredentials_shouldIAM is designed to test the scenario of running Terraform
+// from an EC2 instance, without environment variables or manually supplied
+// credentials.
+func TestAWSGetCredentials_shouldIgnoreIAM(t *testing.T) {
+ resetEnv := unsetEnv(t)
+ defer resetEnv()
+ // capture the test server's close method, to call after the test returns
+ ts := awsEnv(t)
+ defer ts()
+ simple := []struct {
+ Key, Secret, Token string
+ }{
+ {
+ Key: "test",
+ Secret: "secret",
+ }, {
+ Key: "test",
+ Secret: "test",
+ Token: "test",
+ },
+ }
+
+ for _, c := range simple {
+ cfg := Config{
+ AccessKey: c.Key,
+ SecretKey: c.Secret,
+ Token: c.Token,
+ }
+
+ creds := GetCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename)
+ 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)
+ }
+ if v.AccessKeyID != c.Key {
+ t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID)
+ }
+ if v.SecretAccessKey != c.Secret {
+ t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", c.Secret, v.SecretAccessKey)
+ }
+ if v.SessionToken != c.Token {
+ t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", c.Token, v.SessionToken)
+ }
+ }
+}
+
+func TestAWSGetCredentials_shouldErrorWithInvalidEndpoint(t *testing.T) {
+ resetEnv := unsetEnv(t)
+ defer resetEnv()
+ // capture the test server's close method, to call after the test returns
+ ts := invalidAwsEnv(t)
+ defer ts()
+
+ creds := GetCredentials("", "", "", "", "")
+ v, err := creds.Get()
+ if err == nil {
+ t.Fatal("Expected error returned when getting creds w/ invalid EC2 endpoint")
+ }
+
+ if v.ProviderName != "" {
+ t.Fatalf("Expected provider name to be empty, %q given", v.ProviderName)
+ }
+}
+
+func TestAWSGetCredentials_shouldIgnoreInvalidEndpoint(t *testing.T) {
+ resetEnv := unsetEnv(t)
+ defer resetEnv()
+ // capture the test server's close method, to call after the test returns
+ ts := invalidAwsEnv(t)
+ defer ts()
+
+ creds := GetCredentials("accessKey", "secretKey", "", "", "")
+ v, err := creds.Get()
+ if err != nil {
+ t.Fatalf("Getting static credentials w/ invalid EC2 endpoint failed: %s", err)
+ }
+
+ if v.ProviderName != "StaticProvider" {
+ t.Fatalf("Expected provider name to be %q, %q given", "StaticProvider", v.ProviderName)
+ }
+
+ if v.AccessKeyID != "accessKey" {
+ t.Fatalf("Static Access Key %q doesn't match: %s", "accessKey", v.AccessKeyID)
+ }
+
+ if v.SecretAccessKey != "secretKey" {
+ t.Fatalf("Static Secret Key %q doesn't match: %s", "secretKey", v.SecretAccessKey)
+ }
+}
+
+func TestAWSGetCredentials_shouldCatchEC2RoleProvider(t *testing.T) {
+ resetEnv := unsetEnv(t)
+ defer resetEnv()
+ // capture the test server's close method, to call after the test returns
+ ts := awsEnv(t)
+ defer ts()
+
+ creds := GetCredentials("", "", "", "", "")
+ if creds == nil {
+ t.Fatalf("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)
+ }
+ expectedProvider := "EC2RoleProvider"
+ if v.ProviderName != expectedProvider {
+ t.Fatalf("Expected provider name to be %q, %q given",
+ expectedProvider, v.ProviderName)
+ }
+}
+
+var credentialsFileContents = `[myprofile]
+aws_access_key_id = accesskey
+aws_secret_access_key = secretkey
+`
+
+func TestAWSGetCredentials_shouldBeShared(t *testing.T) {
+ file, err := ioutil.TempFile(os.TempDir(), "terraform_aws_cred")
+ if err != nil {
+ t.Fatalf("Error writing temporary credentials file: %s", err)
+ }
+ _, err = file.WriteString(credentialsFileContents)
+ if err != nil {
+ t.Fatalf("Error writing temporary credentials to file: %s", err)
+ }
+ err = file.Close()
+ if err != nil {
+ t.Fatalf("Error closing temporary credentials file: %s", err)
+ }
+
+ defer os.Remove(file.Name())
+
+ resetEnv := unsetEnv(t)
+ defer resetEnv()
+
+ if err := os.Setenv("AWS_PROFILE", "myprofile"); err != nil {
+ t.Fatalf("Error resetting env var AWS_PROFILE: %s", err)
+ }
+ if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", file.Name()); err != nil {
+ t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err)
+ }
+
+ creds := GetCredentials("", "", "", "myprofile", file.Name())
+ if creds == nil {
+ t.Fatalf("Expected a provider chain to be returned")
+ }
+ v, err := creds.Get()
+ if err != nil {
+ t.Fatalf("Error gettings creds: %s", err)
+ }
+
+ if v.AccessKeyID != "accesskey" {
+ t.Fatalf("AccessKeyID mismatch, expected (%s), got (%s)", "accesskey", v.AccessKeyID)
+ }
+
+ if v.SecretAccessKey != "secretkey" {
+ t.Fatalf("SecretAccessKey mismatch, expected (%s), got (%s)", "accesskey", v.AccessKeyID)
+ }
+}
+
+func TestAWSGetCredentials_shouldBeENV(t *testing.T) {
+ // need to set the environment variables to a dummy string, as we don't know
+ // what they may be at runtime without hardcoding here
+ s := "some_env"
+ resetEnv := setEnv(s, t)
+
+ defer resetEnv()
+
+ cfg := Config{}
+ creds := GetCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename)
+ 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)
+ }
+ if v.AccessKeyID != s {
+ t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", s, v.AccessKeyID)
+ }
+ if v.SecretAccessKey != s {
+ t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", s, v.SecretAccessKey)
+ }
+ if v.SessionToken != s {
+ t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", s, v.SessionToken)
+ }
+}
+
+// unsetEnv unsets enviornment variables for testing a "clean slate" with no
+// credentials in the environment
+func unsetEnv(t *testing.T) func() {
+ // Grab any existing AWS keys and preserve. In some tests we'll unset these, so
+ // we need to have them and restore them after
+ e := getEnv()
+ if err := os.Unsetenv("AWS_ACCESS_KEY_ID"); err != nil {
+ t.Fatalf("Error unsetting env var AWS_ACCESS_KEY_ID: %s", err)
+ }
+ if err := os.Unsetenv("AWS_SECRET_ACCESS_KEY"); err != nil {
+ t.Fatalf("Error unsetting env var AWS_SECRET_ACCESS_KEY: %s", err)
+ }
+ if err := os.Unsetenv("AWS_SESSION_TOKEN"); err != nil {
+ t.Fatalf("Error unsetting env var AWS_SESSION_TOKEN: %s", err)
+ }
+ if err := os.Unsetenv("AWS_PROFILE"); err != nil {
+ t.Fatalf("Error unsetting env var AWS_TOKEN: %s", err)
+ }
+ if err := os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE"); err != nil {
+ t.Fatalf("Error unsetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err)
+ }
+
+ return func() {
+ // re-set all the envs we unset above
+ if err := os.Setenv("AWS_ACCESS_KEY_ID", e.Key); err != nil {
+ t.Fatalf("Error resetting env var AWS_ACCESS_KEY_ID: %s", err)
+ }
+ if err := os.Setenv("AWS_SECRET_ACCESS_KEY", e.Secret); err != nil {
+ t.Fatalf("Error resetting env var AWS_SECRET_ACCESS_KEY: %s", err)
+ }
+ if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil {
+ t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err)
+ }
+ if err := os.Setenv("AWS_PROFILE", e.Profile); err != nil {
+ t.Fatalf("Error resetting env var AWS_PROFILE: %s", err)
+ }
+ if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", e.CredsFilename); err != nil {
+ t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err)
+ }
+ }
+}
+
+func setEnv(s string, t *testing.T) func() {
+ e := getEnv()
+ // Set all the envs to a dummy value
+ if err := os.Setenv("AWS_ACCESS_KEY_ID", s); err != nil {
+ t.Fatalf("Error setting env var AWS_ACCESS_KEY_ID: %s", err)
+ }
+ if err := os.Setenv("AWS_SECRET_ACCESS_KEY", s); err != nil {
+ t.Fatalf("Error setting env var AWS_SECRET_ACCESS_KEY: %s", err)
+ }
+ if err := os.Setenv("AWS_SESSION_TOKEN", s); err != nil {
+ t.Fatalf("Error setting env var AWS_SESSION_TOKEN: %s", err)
+ }
+ if err := os.Setenv("AWS_PROFILE", s); err != nil {
+ t.Fatalf("Error setting env var AWS_PROFILE: %s", err)
+ }
+ if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", s); err != nil {
+ t.Fatalf("Error setting env var AWS_SHARED_CREDENTIALS_FLE: %s", err)
+ }
+
+ return func() {
+ // re-set all the envs we unset above
+ if err := os.Setenv("AWS_ACCESS_KEY_ID", e.Key); err != nil {
+ t.Fatalf("Error resetting env var AWS_ACCESS_KEY_ID: %s", err)
+ }
+ if err := os.Setenv("AWS_SECRET_ACCESS_KEY", e.Secret); err != nil {
+ t.Fatalf("Error resetting env var AWS_SECRET_ACCESS_KEY: %s", err)
+ }
+ if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil {
+ t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err)
+ }
+ if err := os.Setenv("AWS_PROFILE", e.Profile); err != nil {
+ t.Fatalf("Error setting env var AWS_PROFILE: %s", err)
+ }
+ if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", s); err != nil {
+ t.Fatalf("Error setting env var AWS_SHARED_CREDENTIALS_FLE: %s", err)
+ }
+ }
+}
+
+// awsEnv establishes a httptest server to mock out the internal AWS Metadata
+// service. IAM Credentials are retrieved by the EC2RoleProvider, which makes
+// API calls to this internal URL. By replacing the server with a test server,
+// we can simulate an AWS environment
+func awsEnv(t *testing.T) func() {
+ routes := routes{}
+ if err := json.Unmarshal([]byte(metadataApiRoutes), &routes); err != nil {
+ t.Fatalf("Failed to unmarshal JSON in AWS ENV test: %s", err)
+ }
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ w.Header().Add("Server", "MockEC2")
+ log.Printf("[DEBUG] Mocker server received request to %q", r.RequestURI)
+ for _, e := range routes.Endpoints {
+ if r.RequestURI == e.Uri {
+ fmt.Fprintln(w, e.Body)
+ w.WriteHeader(200)
+ return
+ }
+ }
+ w.WriteHeader(400)
+ }))
+
+ os.Setenv("AWS_METADATA_URL", ts.URL+"/latest")
+ return ts.Close
+}
+
+// invalidAwsEnv establishes a httptest server to simulate behaviour
+// when endpoint doesn't respond as expected
+func invalidAwsEnv(t *testing.T) func() {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(400)
+ }))
+
+ os.Setenv("AWS_METADATA_URL", ts.URL+"/latest")
+ return ts.Close
+}
+
+// getMockedAwsIamApi establishes a httptest server to simulate behaviour
+// of a real AWS' IAM server
+func getMockedAwsIamApi(endpoints []*iamEndpoint) (func(), *iam.IAM) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(r.Body)
+ requestBody := buf.String()
+
+ log.Printf("[DEBUG] Received IAM API %q request to %q: %s",
+ r.Method, r.RequestURI, requestBody)
+
+ for _, e := range endpoints {
+ if r.Method == e.Request.Method && r.RequestURI == e.Request.Uri && requestBody == e.Request.Body {
+ log.Printf("[DEBUG] Mock API responding with %d: %s", e.Response.StatusCode, e.Response.Body)
+
+ w.WriteHeader(e.Response.StatusCode)
+ w.Header().Set("Content-Type", e.Response.ContentType)
+ w.Header().Set("X-Amzn-Requestid", "1b206dd1-f9a8-11e5-becf-051c60f11c4a")
+ w.Header().Set("Date", time.Now().Format(time.RFC1123))
+
+ fmt.Fprintln(w, e.Response.Body)
+ return
+ }
+ }
+
+ w.WriteHeader(400)
+ return
+ }))
+
+ sc := awsCredentials.NewStaticCredentials("accessKey", "secretKey", "")
+
+ sess := session.New(&aws.Config{
+ Credentials: sc,
+ Region: aws.String("us-east-1"),
+ Endpoint: aws.String(ts.URL),
+ CredentialsChainVerboseErrors: aws.Bool(true),
+ })
+ iamConn := iam.New(sess)
+
+ return ts.Close, iamConn
+}
+
+func getEnv() *currentEnv {
+ // Grab any existing AWS keys and preserve. In some tests we'll unset these, so
+ // we need to have them and restore them after
+ return ¤tEnv{
+ Key: os.Getenv("AWS_ACCESS_KEY_ID"),
+ Secret: os.Getenv("AWS_SECRET_ACCESS_KEY"),
+ Token: os.Getenv("AWS_SESSION_TOKEN"),
+ Profile: os.Getenv("AWS_TOKEN"),
+ CredsFilename: os.Getenv("AWS_SHARED_CREDENTIALS_FILE"),
+ }
+}
+
+// struct to preserve the current environment
+type currentEnv struct {
+ Key, Secret, Token, Profile, CredsFilename string
+}
+
+type routes struct {
+ Endpoints []*endpoint `json:"endpoints"`
+}
+type endpoint struct {
+ Uri string `json:"uri"`
+ Body string `json:"body"`
+}
+
+const metadataApiRoutes = `
+{
+ "endpoints": [
+ {
+ "uri": "/latest/meta-data/instance-id",
+ "body": "mock-instance-id"
+ },
+ {
+ "uri": "/latest/meta-data/iam/info",
+ "body": "{\"Code\": \"Success\",\"LastUpdated\": \"2016-03-17T12:27:32Z\",\"InstanceProfileArn\": \"arn:aws:iam::123456789013:instance-profile/my-instance-profile\",\"InstanceProfileId\": \"AIPAABCDEFGHIJKLMN123\"}"
+ },
+ {
+ "uri": "/latest/meta-data/iam/security-credentials",
+ "body": "test_role"
+ },
+ {
+ "uri": "/latest/meta-data/iam/security-credentials/test_role",
+ "body": "{\"Code\":\"Success\",\"LastUpdated\":\"2015-12-11T17:17:25Z\",\"Type\":\"AWS-HMAC\",\"AccessKeyId\":\"somekey\",\"SecretAccessKey\":\"somesecret\",\"Token\":\"sometoken\"}"
+ }
+ ]
+}
+`
+
+type iamEndpoint struct {
+ Request *iamRequest
+ Response *iamResponse
+}
+
+type iamRequest struct {
+ Method string
+ Uri string
+ Body string
+}
+
+type iamResponse struct {
+ StatusCode int
+ Body string
+ ContentType string
+}
+
+const iamResponse_GetUser_valid = `
+
+
+ AIDACKCEVSQ6C2EXAMPLE
+ /division_abc/subdivision_xyz/
+ Bob
+ arn:aws:iam::123456789012:user/division_abc/subdivision_xyz/Bob
+ 2013-10-02T17:01:44Z
+ 2014-10-10T14:37:51Z
+
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+`
+
+const iamResponse_GetUser_unauthorized = `
+
+ Sender
+ AccessDenied
+ User: arn:aws:iam::123456789012:user/Bob is not authorized to perform: iam:GetUser on resource: arn:aws:iam::123456789012:user/Bob
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+`
+
+const iamResponse_GetUser_federatedFailure = `
+
+ Sender
+ ValidationError
+ Must specify userName when calling with non-User credentials
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+`
+
+const iamResponse_ListRoles_valid = `
+
+ true
+ AWceSSsKsazQ4IEplT9o4hURCzBs00iavlEvEXAMPLE
+
+
+ /
+ %7B%22Version%22%3A%222008-10-17%22%2C%22Statement%22%3A%5B%7B%22Sid%22%3A%22%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22ec2.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D
+ AROACKCEVSQ6C2EXAMPLE
+ elasticbeanstalk-role
+ arn:aws:iam::123456789012:role/elasticbeanstalk-role
+ 2013-10-02T17:01:44Z
+
+
+
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+`
+
+const iamResponse_ListRoles_unauthorized = `
+
+ Sender
+ AccessDenied
+ User: arn:aws:iam::123456789012:user/Bob is not authorized to perform: iam:ListRoles on resource: arn:aws:iam::123456789012:role/
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+`
diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go
index f521c755a..82a82e016 100644
--- a/builtin/providers/aws/config.go
+++ b/builtin/providers/aws/config.go
@@ -4,9 +4,7 @@ import (
"fmt"
"log"
"net/http"
- "os"
"strings"
- "time"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-multierror"
@@ -17,9 +15,6 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
- 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/ec2metadata"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/apigateway"
@@ -133,10 +128,10 @@ func (c *Config) Client() (interface{}, error) {
client.region = c.Region
log.Println("[INFO] Building AWS auth structure")
- creds := getCreds(c.AccessKey, c.SecretKey, c.Token, c.Profile, c.CredsFilename)
+ creds := GetCredentials(c.AccessKey, c.SecretKey, c.Token, c.Profile, c.CredsFilename)
// Call Get to check for credential provider. If nothing found, we'll get an
// error, and we can present it nicely to the user
- _, err = creds.Get()
+ 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.
@@ -147,6 +142,9 @@ func (c *Config) Client() (interface{}, error) {
}
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),
@@ -177,6 +175,7 @@ func (c *Config) Client() (interface{}, error) {
err = c.ValidateCredentials(client.iamconn)
if err != nil {
errs = append(errs, err)
+ return nil, &multierror.Error{Errors: errs}
}
// Some services exist only in us-east-1, e.g. because they manage
@@ -216,7 +215,7 @@ func (c *Config) Client() (interface{}, error) {
log.Println("[INFO] Initializing Elastic Beanstalk Connection")
client.elasticbeanstalkconn = elasticbeanstalk.New(sess)
- authErr := c.ValidateAccountId(client.iamconn)
+ authErr := c.ValidateAccountId(client.iamconn, cp.ProviderName)
if authErr != nil {
errs = append(errs, authErr)
}
@@ -323,7 +322,7 @@ func (c *Config) ValidateCredentials(iamconn *iam.IAM) error {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "AccessDenied" || awsErr.Code() == "ValidationError" {
- log.Printf("[WARN] AccessDenied Error with iam.GetUser, assuming IAM profile")
+ log.Printf("[WARN] AccessDenied Error with iam.GetUser, assuming IAM role")
// User may be an IAM instance profile, or otherwise IAM role without the
// GetUser permissions, so fail silently
return nil
@@ -339,31 +338,17 @@ func (c *Config) ValidateCredentials(iamconn *iam.IAM) error {
// ValidateAccountId returns a context-specific error if the configured account
// id is explicitly forbidden or not authorised; and nil if it is authorised.
-func (c *Config) ValidateAccountId(iamconn *iam.IAM) error {
+func (c *Config) ValidateAccountId(iamconn *iam.IAM, authProviderName string) error {
if c.AllowedAccountIds == nil && c.ForbiddenAccountIds == nil {
return nil
}
log.Printf("[INFO] Validating account ID")
-
- out, err := iamconn.GetUser(nil)
-
+ account_id, err := GetAccountId(iamconn, authProviderName)
if err != nil {
- awsErr, _ := err.(awserr.Error)
- if awsErr.Code() == "ValidationError" {
- log.Printf("[WARN] ValidationError with iam.GetUser, assuming its an IAM profile")
- // User may be an IAM instance profile, so fail silently.
- // If it is an IAM instance profile
- // validating account might be superfluous
- return nil
- } else {
- return fmt.Errorf("Failed getting account ID from IAM: %s", err)
- // return error if the account id is explicitly not authorised
- }
+ return err
}
- account_id := strings.Split(*out.User.Arn, ":")[4]
-
if c.ForbiddenAccountIds != nil {
for _, id := range c.ForbiddenAccountIds {
if id == account_id {
@@ -384,59 +369,6 @@ func (c *Config) ValidateAccountId(iamconn *iam.IAM) error {
return nil
}
-// This function is responsible for reading credentials from the
-// environment in the case that they're not explicitly specified
-// in the Terraform configuration.
-func getCreds(key, secret, token, profile, credsfile string) *awsCredentials.Credentials {
- // build a chain provider, lazy-evaulated by aws-sdk
- providers := []awsCredentials.Provider{
- &awsCredentials.StaticProvider{Value: awsCredentials.Value{
- AccessKeyID: key,
- SecretAccessKey: secret,
- SessionToken: token,
- }},
- &awsCredentials.EnvProvider{},
- &awsCredentials.SharedCredentialsProvider{
- Filename: credsfile,
- Profile: profile,
- },
- }
-
- // We only look in the EC2 metadata API if we can connect
- // to the metadata service within a reasonable amount of time
- metadataURL := os.Getenv("AWS_METADATA_URL")
- if metadataURL == "" {
- metadataURL = "http://169.254.169.254:80/latest"
- }
- c := http.Client{
- Timeout: 100 * time.Millisecond,
- }
-
- r, err := c.Get(metadataURL)
- // Flag to determine if we should add the EC2Meta data provider. Default false
- var useIAM bool
- if err == nil {
- // AWS will add a "Server: EC2ws" header value for the metadata request. We
- // check the headers for this value to ensure something else didn't just
- // happent to be listening on that IP:Port
- if r.Header["Server"] != nil && strings.Contains(r.Header["Server"][0], "EC2") {
- useIAM = true
- }
- }
-
- if useIAM {
- log.Printf("[DEBUG] EC2 Metadata service found, adding EC2 Role Credential Provider")
- providers = append(providers, &ec2rolecreds.EC2RoleProvider{
- Client: ec2metadata.New(session.New(&aws.Config{
- Endpoint: aws.String(metadataURL),
- })),
- })
- } else {
- log.Printf("[DEBUG] EC2 Metadata service not found, not adding EC2 Role Credential Provider")
- }
- return awsCredentials.NewChainCredentials(providers)
-}
-
// addTerraformVersionToUserAgent is a named handler that will add Terraform's
// version information to requests made by the AWS SDK.
var addTerraformVersionToUserAgent = request.NamedHandler{
diff --git a/builtin/providers/aws/config_test.go b/builtin/providers/aws/config_test.go
deleted file mode 100644
index 5c58a5729..000000000
--- a/builtin/providers/aws/config_test.go
+++ /dev/null
@@ -1,376 +0,0 @@
-package aws
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "os"
- "testing"
-
- "github.com/aws/aws-sdk-go/aws/awserr"
-)
-
-func TestAWSConfig_shouldError(t *testing.T) {
- resetEnv := unsetEnv(t)
- defer resetEnv()
- cfg := Config{}
-
- c := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename)
- _, err := c.Get()
- if awsErr, ok := err.(awserr.Error); ok {
- if awsErr.Code() != "NoCredentialProviders" {
- t.Fatalf("Expected NoCredentialProviders error")
- }
- }
- if err == nil {
- t.Fatalf("Expected an error with empty env, keys, and IAM in AWS Config")
- }
-}
-
-func TestAWSConfig_shouldBeStatic(t *testing.T) {
- simple := []struct {
- Key, Secret, Token string
- }{
- {
- Key: "test",
- Secret: "secret",
- }, {
- Key: "test",
- Secret: "test",
- Token: "test",
- },
- }
-
- for _, c := range simple {
- cfg := Config{
- AccessKey: c.Key,
- SecretKey: c.Secret,
- Token: c.Token,
- }
-
- creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename)
- 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)
- }
- if v.AccessKeyID != c.Key {
- t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID)
- }
- if v.SecretAccessKey != c.Secret {
- t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", c.Secret, v.SecretAccessKey)
- }
- if v.SessionToken != c.Token {
- t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", c.Token, v.SessionToken)
- }
- }
-}
-
-// TestAWSConfig_shouldIAM is designed to test the scenario of running Terraform
-// from an EC2 instance, without environment variables or manually supplied
-// credentials.
-func TestAWSConfig_shouldIAM(t *testing.T) {
- // clear AWS_* environment variables
- resetEnv := unsetEnv(t)
- defer resetEnv()
-
- // capture the test server's close method, to call after the test returns
- ts := awsEnv(t)
- defer ts()
-
- // An empty config, no key supplied
- cfg := Config{}
-
- creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename)
- 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)
- }
- if v.AccessKeyID != "somekey" {
- t.Fatalf("AccessKeyID mismatch, expected: (somekey), got (%s)", v.AccessKeyID)
- }
- if v.SecretAccessKey != "somesecret" {
- t.Fatalf("SecretAccessKey mismatch, expected: (somesecret), got (%s)", v.SecretAccessKey)
- }
- if v.SessionToken != "sometoken" {
- t.Fatalf("SessionToken mismatch, expected: (sometoken), got (%s)", v.SessionToken)
- }
-}
-
-// TestAWSConfig_shouldIAM is designed to test the scenario of running Terraform
-// from an EC2 instance, without environment variables or manually supplied
-// credentials.
-func TestAWSConfig_shouldIgnoreIAM(t *testing.T) {
- resetEnv := unsetEnv(t)
- defer resetEnv()
- // capture the test server's close method, to call after the test returns
- ts := awsEnv(t)
- defer ts()
- simple := []struct {
- Key, Secret, Token string
- }{
- {
- Key: "test",
- Secret: "secret",
- }, {
- Key: "test",
- Secret: "test",
- Token: "test",
- },
- }
-
- for _, c := range simple {
- cfg := Config{
- AccessKey: c.Key,
- SecretKey: c.Secret,
- Token: c.Token,
- }
-
- creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename)
- 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)
- }
- if v.AccessKeyID != c.Key {
- t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID)
- }
- if v.SecretAccessKey != c.Secret {
- t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", c.Secret, v.SecretAccessKey)
- }
- if v.SessionToken != c.Token {
- t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", c.Token, v.SessionToken)
- }
- }
-}
-
-var credentialsFileContents = `[myprofile]
-aws_access_key_id = accesskey
-aws_secret_access_key = secretkey
-`
-
-func TestAWSConfig_shouldBeShared(t *testing.T) {
- file, err := ioutil.TempFile(os.TempDir(), "terraform_aws_cred")
- if err != nil {
- t.Fatalf("Error writing temporary credentials file: %s", err)
- }
- _, err = file.WriteString(credentialsFileContents)
- if err != nil {
- t.Fatalf("Error writing temporary credentials to file: %s", err)
- }
- err = file.Close()
- if err != nil {
- t.Fatalf("Error closing temporary credentials file: %s", err)
- }
-
- defer os.Remove(file.Name())
-
- resetEnv := unsetEnv(t)
- defer resetEnv()
-
- if err := os.Setenv("AWS_PROFILE", "myprofile"); err != nil {
- t.Fatalf("Error resetting env var AWS_PROFILE: %s", err)
- }
- if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", file.Name()); err != nil {
- t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err)
- }
-
- creds := getCreds("", "", "", "myprofile", file.Name())
- if creds == nil {
- t.Fatalf("Expected a provider chain to be returned")
- }
- v, err := creds.Get()
- if err != nil {
- t.Fatalf("Error gettings creds: %s", err)
- }
-
- if v.AccessKeyID != "accesskey" {
- t.Fatalf("AccessKeyID mismatch, expected (%s), got (%s)", "accesskey", v.AccessKeyID)
- }
-
- if v.SecretAccessKey != "secretkey" {
- t.Fatalf("SecretAccessKey mismatch, expected (%s), got (%s)", "accesskey", v.AccessKeyID)
- }
-}
-
-func TestAWSConfig_shouldBeENV(t *testing.T) {
- // need to set the environment variables to a dummy string, as we don't know
- // what they may be at runtime without hardcoding here
- s := "some_env"
- resetEnv := setEnv(s, t)
-
- defer resetEnv()
-
- cfg := Config{}
- creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename)
- 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)
- }
- if v.AccessKeyID != s {
- t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", s, v.AccessKeyID)
- }
- if v.SecretAccessKey != s {
- t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", s, v.SecretAccessKey)
- }
- if v.SessionToken != s {
- t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", s, v.SessionToken)
- }
-}
-
-// unsetEnv unsets enviornment variables for testing a "clean slate" with no
-// credentials in the environment
-func unsetEnv(t *testing.T) func() {
- // Grab any existing AWS keys and preserve. In some tests we'll unset these, so
- // we need to have them and restore them after
- e := getEnv()
- if err := os.Unsetenv("AWS_ACCESS_KEY_ID"); err != nil {
- t.Fatalf("Error unsetting env var AWS_ACCESS_KEY_ID: %s", err)
- }
- if err := os.Unsetenv("AWS_SECRET_ACCESS_KEY"); err != nil {
- t.Fatalf("Error unsetting env var AWS_SECRET_ACCESS_KEY: %s", err)
- }
- if err := os.Unsetenv("AWS_SESSION_TOKEN"); err != nil {
- t.Fatalf("Error unsetting env var AWS_SESSION_TOKEN: %s", err)
- }
- if err := os.Unsetenv("AWS_PROFILE"); err != nil {
- t.Fatalf("Error unsetting env var AWS_TOKEN: %s", err)
- }
- if err := os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE"); err != nil {
- t.Fatalf("Error unsetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err)
- }
-
- return func() {
- // re-set all the envs we unset above
- if err := os.Setenv("AWS_ACCESS_KEY_ID", e.Key); err != nil {
- t.Fatalf("Error resetting env var AWS_ACCESS_KEY_ID: %s", err)
- }
- if err := os.Setenv("AWS_SECRET_ACCESS_KEY", e.Secret); err != nil {
- t.Fatalf("Error resetting env var AWS_SECRET_ACCESS_KEY: %s", err)
- }
- if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil {
- t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err)
- }
- if err := os.Setenv("AWS_PROFILE", e.Profile); err != nil {
- t.Fatalf("Error resetting env var AWS_PROFILE: %s", err)
- }
- if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", e.CredsFilename); err != nil {
- t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err)
- }
- }
-}
-
-func setEnv(s string, t *testing.T) func() {
- e := getEnv()
- // Set all the envs to a dummy value
- if err := os.Setenv("AWS_ACCESS_KEY_ID", s); err != nil {
- t.Fatalf("Error setting env var AWS_ACCESS_KEY_ID: %s", err)
- }
- if err := os.Setenv("AWS_SECRET_ACCESS_KEY", s); err != nil {
- t.Fatalf("Error setting env var AWS_SECRET_ACCESS_KEY: %s", err)
- }
- if err := os.Setenv("AWS_SESSION_TOKEN", s); err != nil {
- t.Fatalf("Error setting env var AWS_SESSION_TOKEN: %s", err)
- }
- if err := os.Setenv("AWS_PROFILE", s); err != nil {
- t.Fatalf("Error setting env var AWS_PROFILE: %s", err)
- }
- if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", s); err != nil {
- t.Fatalf("Error setting env var AWS_SHARED_CREDENTIALS_FLE: %s", err)
- }
-
- return func() {
- // re-set all the envs we unset above
- if err := os.Setenv("AWS_ACCESS_KEY_ID", e.Key); err != nil {
- t.Fatalf("Error resetting env var AWS_ACCESS_KEY_ID: %s", err)
- }
- if err := os.Setenv("AWS_SECRET_ACCESS_KEY", e.Secret); err != nil {
- t.Fatalf("Error resetting env var AWS_SECRET_ACCESS_KEY: %s", err)
- }
- if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil {
- t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err)
- }
- if err := os.Setenv("AWS_PROFILE", e.Profile); err != nil {
- t.Fatalf("Error setting env var AWS_PROFILE: %s", err)
- }
- if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", s); err != nil {
- t.Fatalf("Error setting env var AWS_SHARED_CREDENTIALS_FLE: %s", err)
- }
- }
-}
-
-// awsEnv establishes a httptest server to mock out the internal AWS Metadata
-// service. IAM Credentials are retrieved by the EC2RoleProvider, which makes
-// API calls to this internal URL. By replacing the server with a test server,
-// we can simulate an AWS environment
-func awsEnv(t *testing.T) func() {
- routes := routes{}
- if err := json.Unmarshal([]byte(aws_routes), &routes); err != nil {
- t.Fatalf("Failed to unmarshal JSON in AWS ENV test: %s", err)
- }
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/plain")
- w.Header().Add("Server", "MockEC2")
- for _, e := range routes.Endpoints {
- if r.RequestURI == e.Uri {
- fmt.Fprintln(w, e.Body)
- }
- }
- }))
-
- os.Setenv("AWS_METADATA_URL", ts.URL+"/latest")
- return ts.Close
-}
-
-func getEnv() *currentEnv {
- // Grab any existing AWS keys and preserve. In some tests we'll unset these, so
- // we need to have them and restore them after
- return ¤tEnv{
- Key: os.Getenv("AWS_ACCESS_KEY_ID"),
- Secret: os.Getenv("AWS_SECRET_ACCESS_KEY"),
- Token: os.Getenv("AWS_SESSION_TOKEN"),
- Profile: os.Getenv("AWS_TOKEN"),
- CredsFilename: os.Getenv("AWS_SHARED_CREDENTIALS_FILE"),
- }
-}
-
-// struct to preserve the current environment
-type currentEnv struct {
- Key, Secret, Token, Profile, CredsFilename string
-}
-
-type routes struct {
- Endpoints []*endpoint `json:"endpoints"`
-}
-type endpoint struct {
- Uri string `json:"uri"`
- Body string `json:"body"`
-}
-
-const aws_routes = `
-{
- "endpoints": [
- {
- "uri": "/latest/meta-data/iam/security-credentials",
- "body": "test_role"
- },
- {
- "uri": "/latest/meta-data/iam/security-credentials/test_role",
- "body": "{\"Code\":\"Success\",\"LastUpdated\":\"2015-12-11T17:17:25Z\",\"Type\":\"AWS-HMAC\",\"AccessKeyId\":\"somekey\",\"SecretAccessKey\":\"somesecret\",\"Token\":\"sometoken\"}"
- }
- ]
-}
-`
diff --git a/website/source/docs/providers/aws/index.html.markdown b/website/source/docs/providers/aws/index.html.markdown
index 7bc328dad..949c67f62 100644
--- a/website/source/docs/providers/aws/index.html.markdown
+++ b/website/source/docs/providers/aws/index.html.markdown
@@ -39,7 +39,7 @@ explained below:
- Static credentials
- Environment variables
- Shared credentials file
-
+- EC2 Role
### Static credentials ###
@@ -96,6 +96,21 @@ provider "aws" {
}
```
+###EC2 Role
+
+If you're running Terraform from an EC2 instance with IAM Instance Profile
+using IAM Role, Terraform will just ask
+[the metadata API](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials)
+endpoint for credentials.
+
+This is a preferred approach over any other when running in EC2 as you can avoid
+hardcoding credentials. Instead these are leased on-the-fly by Terraform
+which reduces the chance of leakage.
+
+You can provide custom metadata API endpoint via `AWS_METADATA_ENDPOINT` variable
+which expects the endpoint URL including the version
+and defaults to `http://169.254.169.254:80/latest`.
+
## Argument Reference
The following arguments are supported in the `provider` block:
@@ -156,4 +171,24 @@ Nested `endpoints` block supports the followings:
* `elb` - (Optional) Use this to override the default endpoint
URL constructed from the `region`. It's typically used to connect to
- custom elb endpoints.
\ No newline at end of file
+ custom elb endpoints.
+
+## Getting the Account ID
+
+If you use either `allowed_account_ids` or `forbidden_account_ids`,
+Terraform uses several approaches to get the actual account ID
+in order to compare it with allowed/forbidden ones.
+
+Approaches differ per auth providers:
+
+ * EC2 instance w/ IAM Instance Profile - [Metadata API](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html)
+ is always used
+ * All other providers (ENV vars, shared creds file, ...)
+ will try two approaches in the following order
+ * `iam:GetUser` - typically useful for IAM Users. It also means
+ that each user needs to be privileged to call `iam:GetUser` for themselves.
+ * `iam:ListRoles` - this is specifically useful for IdP-federated profiles
+ which cannot use `iam:GetUser`. It also means that each federated user
+ need to be _assuming_ an IAM role which allows `iam:ListRoles`.
+ There is currently no better clean way to get account ID
+ out of the API when using federated account unfortunately.