Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix handling of AdRoll's hologram clients #17

Merged
merged 2 commits into from
Jun 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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