Skip to content

Commit

Permalink
provider/aws: Refactor AWS Authentication chain
Browse files Browse the repository at this point in the history
  • Loading branch information
catsby committed Dec 11, 2015
1 parent eceb8c8 commit 459fc71
Show file tree
Hide file tree
Showing 3 changed files with 330 additions and 91 deletions.
51 changes: 46 additions & 5 deletions builtin/providers/aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ package aws
import (
"fmt"
"log"
"net/http"
"os"
"strings"
"time"

"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-multierror"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
awsCredentials "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/aws/aws-sdk-go/service/cloudformation"
Expand Down Expand Up @@ -104,9 +109,14 @@ func (c *Config) Client() (interface{}, error) {
client.region = c.Region

log.Println("[INFO] Building AWS auth structure")
// We fetched all credential sources in Provider. If they are
// available, they'll already be in c. See Provider definition.
creds := credentials.NewStaticCredentials(c.AccessKey, c.SecretKey, c.Token)
creds := getCreds(c.AccessKey, c.SecretKey, c.Token)
// 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()
if err != nil {
errs = append(errs, fmt.Errorf("Error loading credentials for AWS Provider: %s", err))
return nil, &multierror.Error{Errors: errs}
}
awsConfig := &aws.Config{
Credentials: creds,
Region: aws.String(c.Region),
Expand All @@ -118,7 +128,7 @@ func (c *Config) Client() (interface{}, error) {
sess := session.New(awsConfig)
client.iamconn = iam.New(sess)

err := c.ValidateCredentials(client.iamconn)
err = c.ValidateCredentials(client.iamconn)
if err != nil {
errs = append(errs, err)
}
Expand Down Expand Up @@ -316,3 +326,34 @@ 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 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{},
}

// 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 = "169.254.169.254:80"
}
c := http.Client{
Timeout: 100 * time.Millisecond,
}
_, err := c.Get(metadataURL)
if err == nil {
providers = append(providers, &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(session.New(&aws.Config{Endpoint: aws.String(metadataURL)}))})
}
return awsCredentials.NewChainCredentials(providers)
}
275 changes: 275 additions & 0 deletions builtin/providers/aws/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package aws

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/aws/aws-sdk-go/aws/awserr"
)

// Grab any existing AWS keys and preserve. In some tests we'll unset these, so
// we need to have them and restore them after
var k = os.Getenv("AWS_ACCESS_KEY_ID")
var s = os.Getenv("AWS_SECRET_ACCESS_KEY")
var to = os.Getenv("AWS_SESSION_TOKEN")

func TestAWSConfig_shouldError(t *testing.T) {
unsetEnv(t)
defer resetEnv(t)
cfg := Config{}

c := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token)
_, 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)
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
unsetEnv(t)
defer resetEnv(t)

// 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)
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) {
unsetEnv(t)
defer resetEnv(t)
// 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)
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 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"
setEnv(s, t)
defer resetEnv(t)

cfg := Config{}
creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token)
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) {
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)
}
}

func resetEnv(t *testing.T) {
// re-set all the envs we unset above
if err := os.Setenv("AWS_ACCESS_KEY_ID", k); err != nil {
t.Fatalf("Error resetting env var AWS_ACCESS_KEY_ID: %s", err)
}
if err := os.Setenv("AWS_SECRET_ACCESS_KEY", s); err != nil {
t.Fatalf("Error resetting env var AWS_SECRET_ACCESS_KEY: %s", err)
}
if err := os.Setenv("AWS_SESSION_TOKEN", to); err != nil {
t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err)
}
}

func setEnv(s string, t *testing.T) {
// 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)
}
}

// 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) {
for _, e := range routes.Endpoints {
if r.RequestURI == e.Uri {
w.Header().Set("Content-Type", e.ContentType)
fmt.Fprintln(w, e.Body)
}
}
}))

os.Setenv("AWS_METADATA_URL", ts.URL)
return ts.Close
}

type routes struct {
Endpoints []*endpoint `json:"endpoints"`
}
type endpoint struct {
Uri string `json:"uri"`
ContentType string `json:"content-type"`
Body string `json:"body"`
}

const aws_routes = `
{
"endpoints": [
{
"uri": "/meta-data/iam/security-credentials",
"content-type": "text/plain",
"body": "test_role"
},
{
"uri": "/meta-data/iam/security-credentials/test_role",
"content-type": "text/plain",
"body": "{\"Code\":\"Success\",\"LastUpdated\":\"2015-12-11T17:17:25Z\",\"Type\":\"AWS-HMAC\",\"AccessKeyId\":\"somekey\",\"SecretAccessKey\":\"somesecret\",\"Token\":\"sometoken\"}"
}
]
}
`
Loading

0 comments on commit 459fc71

Please sign in to comment.