aws: Enable account ID check for assumed roles + EC2 instances
This commit is contained in:
parent
fb91833c80
commit
f1f602cdf6
|
@ -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 ""
|
||||
}
|
|
@ -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 = `<GetUserResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<GetUserResult>
|
||||
<User>
|
||||
<UserId>AIDACKCEVSQ6C2EXAMPLE</UserId>
|
||||
<Path>/division_abc/subdivision_xyz/</Path>
|
||||
<UserName>Bob</UserName>
|
||||
<Arn>arn:aws:iam::123456789012:user/division_abc/subdivision_xyz/Bob</Arn>
|
||||
<CreateDate>2013-10-02T17:01:44Z</CreateDate>
|
||||
<PasswordLastUsed>2014-10-10T14:37:51Z</PasswordLastUsed>
|
||||
</User>
|
||||
</GetUserResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</GetUserResponse>`
|
||||
|
||||
const iamResponse_GetUser_unauthorized = `<ErrorResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<Error>
|
||||
<Type>Sender</Type>
|
||||
<Code>AccessDenied</Code>
|
||||
<Message>User: arn:aws:iam::123456789012:user/Bob is not authorized to perform: iam:GetUser on resource: arn:aws:iam::123456789012:user/Bob</Message>
|
||||
</Error>
|
||||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ErrorResponse>`
|
||||
|
||||
const iamResponse_GetUser_federatedFailure = `<ErrorResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<Error>
|
||||
<Type>Sender</Type>
|
||||
<Code>ValidationError</Code>
|
||||
<Message>Must specify userName when calling with non-User credentials</Message>
|
||||
</Error>
|
||||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ErrorResponse>`
|
||||
|
||||
const iamResponse_ListRoles_valid = `<ListRolesResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<ListRolesResult>
|
||||
<IsTruncated>true</IsTruncated>
|
||||
<Marker>AWceSSsKsazQ4IEplT9o4hURCzBs00iavlEvEXAMPLE</Marker>
|
||||
<Roles>
|
||||
<member>
|
||||
<Path>/</Path>
|
||||
<AssumeRolePolicyDocument>%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</AssumeRolePolicyDocument>
|
||||
<RoleId>AROACKCEVSQ6C2EXAMPLE</RoleId>
|
||||
<RoleName>elasticbeanstalk-role</RoleName>
|
||||
<Arn>arn:aws:iam::123456789012:role/elasticbeanstalk-role</Arn>
|
||||
<CreateDate>2013-10-02T17:01:44Z</CreateDate>
|
||||
</member>
|
||||
</Roles>
|
||||
</ListRolesResult>
|
||||
<ResponseMetadata>
|
||||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
</ListRolesResponse>`
|
||||
|
||||
const iamResponse_ListRoles_unauthorized = `<ErrorResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
|
||||
<Error>
|
||||
<Type>Sender</Type>
|
||||
<Code>AccessDenied</Code>
|
||||
<Message>User: arn:aws:iam::123456789012:user/Bob is not authorized to perform: iam:ListRoles on resource: arn:aws:iam::123456789012:role/</Message>
|
||||
</Error>
|
||||
<RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
|
||||
</ErrorResponse>`
|
|
@ -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,30 +338,16 @@ 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 {
|
||||
|
@ -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{
|
||||
|
|
|
@ -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\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
|
@ -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:
|
||||
|
@ -157,3 +172,23 @@ 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.
|
||||
|
||||
## 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.
|
||||
|
|
Loading…
Reference in New Issue