From 1ff9b0e843f291582fbfa7f7a5e154cc9440cba8 Mon Sep 17 00:00:00 2001 From: Mark Peek Date: Thu, 11 Jul 2013 20:09:15 +0000 Subject: [PATCH] Add instance based IAM role authentication This change consolidates authentication decisions into a single new function. First it will try using the passed in key/secret, then environment variables and finally instance based IAM role authentication. Since instance based IAM role authentication requires an additional token for signing the request, the existing API has changed slightly since most use cases construct auth via struct initialization. This will also allow for future changes such as supporting credential files or STS AssumeRole. --- aws/aws.go | 95 +++++++++++++++++++++++++++++++++++++++-- aws/aws_test.go | 28 +++++++++++- ec2/ec2_test.go | 2 +- ec2/sign.go | 3 ++ ec2/sign_test.go | 4 +- exp/mturk/mturk_test.go | 2 +- exp/mturk/sign_test.go | 2 +- exp/sdb/sdb_test.go | 2 +- exp/sdb/sign.go | 3 ++ exp/sdb/sign_test.go | 2 +- exp/sns/sign.go | 3 ++ exp/sns/sns_test.go | 2 +- iam/iam_test.go | 2 +- iam/sign.go | 3 ++ s3/s3_test.go | 2 +- s3/sign_test.go | 2 +- 16 files changed, 141 insertions(+), 16 deletions(-) diff --git a/aws/aws.go b/aws/aws.go index 7da2377c..fdab0122 100644 --- a/aws/aws.go +++ b/aws/aws.go @@ -10,7 +10,11 @@ package aws import ( + "encoding/json" "errors" + "fmt" + "io/ioutil" + "net/http" "os" ) @@ -146,7 +150,7 @@ var Regions = map[string]Region{ } type Auth struct { - AccessKey, SecretKey string + AccessKey, SecretKey, Token string } var unreserved = make([]bool, 128) @@ -160,17 +164,102 @@ func init() { } } +type credentials struct { + Code string + LastUpdated string + Type string + AccessKeyId string + SecretAccessKey string + Token string + Expiration string +} + +func getMetaData(path string) (contents []byte, err error) { + url := "http://169.254.169.254/latest/meta-data/" + path + + resp, err := http.Get(url) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + err = fmt.Errorf("Code %d returned for url %s", resp.StatusCode, url) + return + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + return []byte(body), err +} + +func getInstanceCredentials() (cred credentials, err error) { + credentialPath := "iam/security-credentials/" + + // Get the instance role + role, err := getMetaData(credentialPath) + if err != nil { + return + } + + // Get the instance role credentials + credentialJSON, err := getMetaData(credentialPath + string(role)) + if err != nil { + return + } + + err = json.Unmarshal([]byte(credentialJSON), &cred) + return +} + +// GetAuth creates an Auth based on either passed in credentials, +// environment information or instance based role credentials. +func GetAuth(accessKey string, secretKey string) (auth Auth, err error) { + // First try passed in credentials + if accessKey != "" && secretKey != "" { + return Auth{accessKey, secretKey, ""}, nil + } + + // Next try to get auth from the environment + auth, err = EnvAuth() + if err == nil { + // Found auth, return + return + } + + // Next try getting auth from the instance role + cred, err := getInstanceCredentials() + if err == nil { + // Found auth, return + auth.AccessKey = cred.AccessKeyId + auth.SecretKey = cred.SecretAccessKey + auth.Token = cred.Token + return + } + err = errors.New("No valid AWS authentication found") + return +} + // EnvAuth creates an Auth based on environment information. // The AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment // variables are used. func EnvAuth() (auth Auth, err error) { auth.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID") + if auth.AccessKey == "" { + auth.AccessKey = os.Getenv("AWS_ACCESS_KEY") + } + auth.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY") + if auth.SecretKey == "" { + auth.SecretKey = os.Getenv("AWS_SECRET_KEY") + } if auth.AccessKey == "" { - err = errors.New("AWS_ACCESS_KEY_ID not found in environment") + err = errors.New("AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment") } if auth.SecretKey == "" { - err = errors.New("AWS_SECRET_ACCESS_KEY not found in environment") + err = errors.New("AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment") } return } diff --git a/aws/aws_test.go b/aws/aws_test.go index c0958a2e..55ed56cd 100644 --- a/aws/aws_test.go +++ b/aws/aws_test.go @@ -33,14 +33,14 @@ func (s *S) TearDownTest(c *C) { func (s *S) TestEnvAuthNoSecret(c *C) { os.Clearenv() _, err := aws.EnvAuth() - c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_KEY not found in environment") + c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment") } func (s *S) TestEnvAuthNoAccess(c *C) { os.Clearenv() os.Setenv("AWS_SECRET_ACCESS_KEY", "foo") _, err := aws.EnvAuth() - c.Assert(err, ErrorMatches, "AWS_ACCESS_KEY_ID not found in environment") + c.Assert(err, ErrorMatches, "AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment") } func (s *S) TestEnvAuth(c *C) { @@ -52,6 +52,30 @@ func (s *S) TestEnvAuth(c *C) { c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) } +func (s *S) TestEnvAuthAlt(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_KEY", "secret") + os.Setenv("AWS_ACCESS_KEY", "access") + auth, err := aws.EnvAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestGetAuthStatic(c *C) { + auth, err := aws.GetAuth("access", "secret") + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestGetAuthEnv(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_ACCESS_KEY", "secret") + os.Setenv("AWS_ACCESS_KEY_ID", "access") + auth, err := aws.GetAuth("", "") + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + func (s *S) TestEncode(c *C) { c.Assert(aws.Encode("foo"), Equals, "foo") c.Assert(aws.Encode("/"), Equals, "%2F") diff --git a/ec2/ec2_test.go b/ec2/ec2_test.go index ba1dc9c9..abd2fc19 100644 --- a/ec2/ec2_test.go +++ b/ec2/ec2_test.go @@ -22,7 +22,7 @@ var testServer = testutil.NewHTTPServer() func (s *S) SetUpSuite(c *C) { testServer.Start() - auth := aws.Auth{"abc", "123"} + auth := aws.Auth{"abc", "123", ""} s.ec2 = ec2.New(auth, aws.Region{EC2Endpoint: testServer.URL}) } diff --git a/ec2/sign.go b/ec2/sign.go index 59661050..bffc3c7e 100644 --- a/ec2/sign.go +++ b/ec2/sign.go @@ -18,6 +18,9 @@ func sign(auth aws.Auth, method, path string, params map[string]string, host str params["AWSAccessKeyId"] = auth.AccessKey params["SignatureVersion"] = "2" params["SignatureMethod"] = "HmacSHA256" + if auth.Token != "" { + params["SecurityToken"] = auth.Token + } // AWS specifies that the parameters in a signed request must // be provided in the natural order of the keys. This is distinct diff --git a/ec2/sign_test.go b/ec2/sign_test.go index a61a46a2..fb748d7e 100644 --- a/ec2/sign_test.go +++ b/ec2/sign_test.go @@ -8,7 +8,7 @@ import ( // EC2 ReST authentication docs: http://goo.gl/fQmAN -var testAuth = aws.Auth{"user", "secret"} +var testAuth = aws.Auth{"user", "secret", ""} func (s *S) TestBasicSignature(c *C) { params := map[string]string{} @@ -62,7 +62,7 @@ func (s *S) TestSignatureExample1(c *C) { "Version": "2007-11-07", "Action": "ListDomains", } - ec2.Sign(aws.Auth{"access", "secret"}, "GET", "/", params, "sdb.amazonaws.com") + ec2.Sign(aws.Auth{"access", "secret", ""}, "GET", "/", params, "sdb.amazonaws.com") expected := "okj96/5ucWBSc1uR2zXVfm6mDHtgfNv657rRtt/aunQ=" c.Assert(params["Signature"], Equals, expected) } diff --git a/exp/mturk/mturk_test.go b/exp/mturk/mturk_test.go index 9d31063a..3652be4e 100644 --- a/exp/mturk/mturk_test.go +++ b/exp/mturk/mturk_test.go @@ -23,7 +23,7 @@ var testServer = testutil.NewHTTPServer() func (s *S) SetUpSuite(c *C) { testServer.Start() - auth := aws.Auth{"abc", "123"} + auth := aws.Auth{"abc", "123", ""} u, err := url.Parse(testServer.URL) if err != nil { panic(err.Error()) diff --git a/exp/mturk/sign_test.go b/exp/mturk/sign_test.go index 42e18529..c7f5f32e 100644 --- a/exp/mturk/sign_test.go +++ b/exp/mturk/sign_test.go @@ -8,7 +8,7 @@ import ( // Mechanical Turk REST authentication docs: http://goo.gl/wrzfn -var testAuth = aws.Auth{"user", "secret"} +var testAuth = aws.Auth{"user", "secret", ""} // == fIJy9wCApBNL2R4J2WjJGtIBFX4= func (s *S) TestBasicSignature(c *C) { diff --git a/exp/sdb/sdb_test.go b/exp/sdb/sdb_test.go index 9b6840e1..ef611421 100644 --- a/exp/sdb/sdb_test.go +++ b/exp/sdb/sdb_test.go @@ -22,7 +22,7 @@ var testServer = testutil.NewHTTPServer() func (s *S) SetUpSuite(c *C) { testServer.Start() - auth := aws.Auth{"abc", "123"} + auth := aws.Auth{"abc", "123", ""} s.sdb = sdb.New(auth, aws.Region{SDBEndpoint: testServer.URL}) } diff --git a/exp/sdb/sign.go b/exp/sdb/sign.go index e88a0075..0f9c2348 100644 --- a/exp/sdb/sign.go +++ b/exp/sdb/sign.go @@ -30,6 +30,9 @@ func sign(auth aws.Auth, method, path string, params url.Values, headers http.He params["AWSAccessKeyId"] = []string{auth.AccessKey} params["SignatureVersion"] = []string{"2"} params["SignatureMethod"] = []string{"HmacSHA256"} + if auth.Token != "" { + params["SecurityToken"] = auth.Token + } // join up all the incoming params var sarray []string diff --git a/exp/sdb/sign_test.go b/exp/sdb/sign_test.go index 2963b35d..a634d2b5 100644 --- a/exp/sdb/sign_test.go +++ b/exp/sdb/sign_test.go @@ -8,7 +8,7 @@ import ( // SimpleDB ReST authentication docs: http://goo.gl/CaY81 -var testAuth = aws.Auth{"access-key-id-s8eBOWuU", "secret-access-key-UkQjTLd9"} +var testAuth = aws.Auth{"access-key-id-s8eBOWuU", "secret-access-key-UkQjTLd9", ""} func (s *S) TestSignExampleDomainCreate(c *C) { method := "GET" diff --git a/exp/sns/sign.go b/exp/sns/sign.go index 2a9ab4dd..d53b3842 100644 --- a/exp/sns/sign.go +++ b/exp/sns/sign.go @@ -25,6 +25,9 @@ func sign(auth aws.Auth, method, path string, params url.Values, headers http.He params["AWSAccessKeyId"] = []string{auth.AccessKey} params["SignatureVersion"] = []string{"2"} params["SignatureMethod"] = []string{"HmacSHA256"} + if auth.Token != "" { + params["SecurityToken"] = auth.Token + } var sarry []string for k, v := range params { diff --git a/exp/sns/sns_test.go b/exp/sns/sns_test.go index 5a0ead85..0d2274df 100644 --- a/exp/sns/sns_test.go +++ b/exp/sns/sns_test.go @@ -22,7 +22,7 @@ var testServer = testutil.NewHTTPServer() func (s *S) SetUpSuite(c *C) { testServer.Start() - auth := aws.Auth{"abc", "123"} + auth := aws.Auth{"abc", "123", ""} s.sns = sns.New(auth, aws.Region{SNSEndpoint: testServer.URL}) } diff --git a/iam/iam_test.go b/iam/iam_test.go index 0be58b6a..98bc95b5 100644 --- a/iam/iam_test.go +++ b/iam/iam_test.go @@ -23,7 +23,7 @@ var testServer = testutil.NewHTTPServer() func (s *S) SetUpSuite(c *C) { testServer.Start() - auth := aws.Auth{"abc", "123"} + auth := aws.Auth{"abc", "123", ""} s.iam = iam.New(auth, aws.Region{IAMEndpoint: testServer.URL}) } diff --git a/iam/sign.go b/iam/sign.go index 549c0a17..bb1fa3f5 100644 --- a/iam/sign.go +++ b/iam/sign.go @@ -18,6 +18,9 @@ func sign(auth aws.Auth, method, path string, params map[string]string, host str params["AWSAccessKeyId"] = auth.AccessKey params["SignatureVersion"] = "2" params["SignatureMethod"] = "HmacSHA256" + if auth.Token != "" { + params["SecurityToken"] = auth.Token + } var sarray []string for k, v := range params { diff --git a/s3/s3_test.go b/s3/s3_test.go index fc07fd88..37e2d93e 100644 --- a/s3/s3_test.go +++ b/s3/s3_test.go @@ -27,7 +27,7 @@ var testServer = testutil.NewHTTPServer() func (s *S) SetUpSuite(c *C) { testServer.Start() - auth := aws.Auth{"abc", "123"} + auth := aws.Auth{"abc", "123", ""} s.s3 = s3.New(auth, aws.Region{Name: "faux-region-1", S3Endpoint: testServer.URL}) } diff --git a/s3/sign_test.go b/s3/sign_test.go index c10d62df..b7c33e3f 100644 --- a/s3/sign_test.go +++ b/s3/sign_test.go @@ -8,7 +8,7 @@ import ( // S3 ReST authentication docs: http://goo.gl/G1LrK -var testAuth = aws.Auth{"0PN5J17HBGZHT7JJ3X82", "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o"} +var testAuth = aws.Auth{"0PN5J17HBGZHT7JJ3X82", "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o", ""} func (s *S) TestSignExampleObjectGet(c *C) { method := "GET"