Skip to content

Commit

Permalink
Fix handling of AdRoll's hologram clients (#17)
Browse files Browse the repository at this point in the history
* Fix handling of AdRoll's hologram clients

This is a port of hashicorp/terraform#12951 into the new repository.

Partially fixes hashicorp/terraform#12704 in the case of hologram
clients, but doesn't fix the regression when SkipRequestingAccountId is
set.

* Refactoring of tests
  • Loading branch information
joelthompson authored and radeksimko committed Jun 15, 2017
1 parent ab45724 commit 8f7ea2f
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 71 deletions.
59 changes: 36 additions & 23 deletions aws/auth_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import (
"github.com/aws/aws-sdk-go/service/sts"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-multierror"
)

func GetAccountInfo(iamconn *iam.IAM, stsconn *sts.STS, authProviderName string) (string, string, error) {
var errors 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")
Expand All @@ -35,29 +37,34 @@ func GetAccountInfo(iamconn *iam.IAM, stsconn *sts.STS, authProviderName string)

metadataClient := ec2metadata.New(sess)
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)
if err == nil {
return parseAccountInfoFromArn(info.InstanceProfileArn)
}

return parseAccountInfoFromArn(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 parseAccountInfoFromArn(*outUser.User.Arn)
}

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" && awsErr.Code() != "InvalidClientTokenId") {
return "", "", fmt.Errorf("Failed getting account ID via 'iam:GetUser': %s", err)
log.Printf("[DEBUG] Failed to get account info from metadata service: %s", err)
errors = multierror.Append(errors, err)
// We can end up here if there's an issue with the instance metadata service
// or if we're getting credentials from AdRoll's Hologram (in which case IAMInfo will
// error out). In any event, if we can't get account info here, we should try
// the other methods available.
// If we have creds from something that looks like an IAM instance profile, but
// we were unable to retrieve account info from the instance profile, it's probably
// a safe assumption that we're not an IAM user
} else {
// Creds aren't from an IAM instance profile, so try try iam:GetUser
log.Println("[DEBUG] Trying to get account ID via iam:GetUser")
outUser, err := iamconn.GetUser(nil)
if err == nil {
return parseAccountInfoFromArn(*outUser.User.Arn)
}
errors = multierror.Append(errors, err)
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" && awsErr.Code() != "InvalidClientTokenId") {
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.Printf("[DEBUG] Getting account ID via iam:GetUser failed: %s", err)

// Then try STS GetCallerIdentity
log.Println("[DEBUG] Trying to get account ID via sts:GetCallerIdentity")
Expand All @@ -66,18 +73,24 @@ func GetAccountInfo(iamconn *iam.IAM, stsconn *sts.STS, authProviderName string)
return parseAccountInfoFromArn(*outCallerIdentity.Arn)
}
log.Printf("[DEBUG] Getting account ID via sts:GetCallerIdentity failed: %s", err)
errors = multierror.Append(errors, err)

// Then try IAM ListRoles
log.Println("[DEBUG] Trying to get account ID via iam:ListRoles")
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)
log.Printf("[DEBUG] Failed to get account ID via iam:ListRoles: %s", err)
errors = multierror.Append(errors, err)
return "", "", fmt.Errorf("Failed getting account ID via all available methods. Errors: %s", errors)
}

if len(outRoles.Roles) < 1 {
return "", "", errors.New("Failed getting account ID via 'iam:ListRoles': No roles available")
err = fmt.Errorf("Failed to get account ID via iam:ListRoles: No roles available")
log.Printf("[DEBUG] %s", err)
errors = multierror.Append(errors, err)
return "", "", fmt.Errorf("Failed getting account ID via all available methods. Errors: %s", errors)
}

return parseAccountInfoFromArn(*outRoles.Roles[0].Arn)
Expand Down
128 changes: 80 additions & 48 deletions aws/auth_helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package aws

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
Expand All @@ -11,7 +10,9 @@ import (
"testing"

"github.com/aws/aws-sdk-go/aws/awserr"
"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"
"github.com/aws/aws-sdk-go/service/sts"
)
Expand All @@ -20,7 +21,7 @@ func TestAWSGetAccountInfo_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)
awsTs := awsMetadataApiMock(append(securityCredentialsEndpoints, instanceIdEndpoint, iamInfoEndpoint))
defer awsTs()

closeEmpty, emptySess, err := getMockedAwsApiSession("zero", []*awsMockEndpoint{})
Expand Down Expand Up @@ -52,7 +53,7 @@ func TestAWSGetAccountInfo_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)
awsTs := awsMetadataApiMock(append(securityCredentialsEndpoints, instanceIdEndpoint, iamInfoEndpoint))
defer awsTs()

iamEndpoints := []*awsMockEndpoint{
Expand Down Expand Up @@ -153,23 +154,42 @@ func TestAWSGetAccountInfo_shouldBeValid_fromGetCallerIdentity(t *testing.T) {
t.Fatal(err)
}

iamConn := iam.New(iamSess)
stsConn := sts.New(stsSess)
testGetAccountInfo(t, iamSess, stsSess, credentials.SharedCredsProviderName)
}

part, id, err := GetAccountInfo(iamConn, stsConn, "")
func TestAWSGetAccountInfo_shouldBeValid_EC2RoleFallsBackToCallerIdentity(t *testing.T) {
// This mimics the metadata service mocked by Hologram (https://github.com/AdRoll/hologram)
resetEnv := unsetEnv(t)
defer resetEnv()

awsTs := awsMetadataApiMock(securityCredentialsEndpoints)
defer awsTs()

iamEndpoints := []*awsMockEndpoint{
{
Request: &awsMockRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"},
Response: &awsMockResponse{403, iamResponse_GetUser_unauthorized, "text/xml"},
},
}
closeIam, iamSess, err := getMockedAwsApiSession("IAM", iamEndpoints)
defer closeIam()
if err != nil {
t.Fatalf("Getting account ID via GetUser failed: %s", err)
t.Fatal(err)
}

expectedPart := "aws"
if part != expectedPart {
t.Fatalf("Expected partition: %s, given: %s", expectedPart, part)
stsEndpoints := []*awsMockEndpoint{
{
Request: &awsMockRequest{"POST", "/", "Action=GetCallerIdentity&Version=2011-06-15"},
Response: &awsMockResponse{200, stsResponse_GetCallerIdentity_valid, "text/xml"},
},
}

expectedAccountId := "123456789012"
if id != expectedAccountId {
t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id)
closeSts, stsSess, err := getMockedAwsApiSession("STS", stsEndpoints)
defer closeSts()
if err != nil {
t.Fatal(err)
}

testGetAccountInfo(t, iamSess, stsSess, ec2rolecreds.ProviderName)
}

func TestAWSGetAccountInfo_shouldBeValid_fromIamListRoles(t *testing.T) {
Expand Down Expand Up @@ -401,7 +421,7 @@ func TestAWSGetCredentials_shouldIAM(t *testing.T) {
defer resetEnv()

// capture the test server's close method, to call after the test returns
ts := awsEnv(t)
ts := awsMetadataApiMock(append(securityCredentialsEndpoints, instanceIdEndpoint, iamInfoEndpoint))
defer ts()

// An empty config, no key supplied
Expand Down Expand Up @@ -437,7 +457,7 @@ 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)
ts := awsMetadataApiMock(append(securityCredentialsEndpoints, instanceIdEndpoint, iamInfoEndpoint))
defer ts()
simple := []struct {
Key, Secret, Token string
Expand Down Expand Up @@ -544,7 +564,7 @@ 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)
ts := awsMetadataApiMock(append(securityCredentialsEndpoints, instanceIdEndpoint, iamInfoEndpoint))
defer ts()

creds, err := GetCredentials(&Config{})
Expand Down Expand Up @@ -651,6 +671,27 @@ func TestAWSGetCredentials_shouldBeENV(t *testing.T) {
}
}

func testGetAccountInfo(t *testing.T, iamSess, stsSess *session.Session, credProviderName string) {

iamConn := iam.New(iamSess)
stsConn := sts.New(stsSess)

part, id, err := GetAccountInfo(iamConn, stsConn, credProviderName)
if err != nil {
t.Fatalf("Getting account ID failed: %s", err)
}

expectedPart := "aws"
if part != expectedPart {
t.Fatalf("Expected partition: %s, given: %s", expectedPart, part)
}

expectedAccountId := "123456789012"
if id != expectedAccountId {
t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id)
}
}

// unsetEnv unsets environment variables for testing a "clean slate" with no
// credentials in the environment
func unsetEnv(t *testing.T) func() {
Expand Down Expand Up @@ -732,20 +773,16 @@ func setEnv(s string, t *testing.T) func() {
}
}

// awsEnv establishes a httptest server to mock out the internal AWS Metadata
// awsMetadataApiMock 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)
}
func awsMetadataApiMock(endpoints []*endpoint) func() {
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 {
for _, e := range endpoints {
if r.RequestURI == e.Uri {
fmt.Fprintln(w, e.Body)
w.WriteHeader(200)
Expand Down Expand Up @@ -787,36 +824,31 @@ 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\"}"
}
]
var instanceIdEndpoint = &endpoint{
Uri: "/latest/meta-data/instance-id",
Body: "mock-instance-id",
}

var securityCredentialsEndpoints = []*endpoint{
&endpoint{
Uri: "/latest/meta-data/iam/security-credentials",
Body: "test_role",
},
&endpoint{
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\"}",
},
}

var iamInfoEndpoint = &endpoint{
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\"}",
}
`

const iamResponse_GetUser_valid = `<GetUserResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<GetUserResult>
Expand Down

0 comments on commit 8f7ea2f

Please sign in to comment.