mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-25 01:35:18 -06:00
971a7219f3
Support loading credentials from the AWS_CONTAINER_CREDENTIALS_FULL_URI metadata endpoint which is helpful for AWS SnapStart lambdas Fixes #6893 Signed-off-by: Matthew Bamber <mattb@grabyo.com>
1715 lines
56 KiB
Go
1715 lines
56 KiB
Go
// Copyright 2019 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package rest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/open-policy-agent/opa/internal/providers/aws"
|
|
"github.com/open-policy-agent/opa/logging"
|
|
"github.com/open-policy-agent/opa/util/test"
|
|
)
|
|
|
|
// this is usually private; but we need it here
|
|
type metadataPayload struct {
|
|
Code string
|
|
AccessKeyID string `json:"AccessKeyId"`
|
|
SecretAccessKey string
|
|
Token string
|
|
Expiration time.Time
|
|
}
|
|
|
|
// quick and dirty assertions
|
|
func assertEq(expected string, actual string, t *testing.T) {
|
|
t.Helper()
|
|
if actual != expected {
|
|
t.Error("expected: ", expected, " but got: ", actual)
|
|
}
|
|
}
|
|
func assertIn(candidates []string, actual string, t *testing.T) {
|
|
t.Helper()
|
|
for _, expected := range candidates {
|
|
if actual == expected {
|
|
return
|
|
}
|
|
}
|
|
t.Error("value: '", actual, "' not found in: ", candidates)
|
|
}
|
|
|
|
func assertErr(expected string, actual error, t *testing.T) {
|
|
t.Helper()
|
|
if !strings.Contains(actual.Error(), expected) {
|
|
t.Errorf("Expected error to contain %s, got: %s", expected, actual.Error())
|
|
}
|
|
}
|
|
|
|
func TestEnvironmentCredentialService(t *testing.T) {
|
|
cs := &awsEnvironmentCredentialService{}
|
|
|
|
// wrong path: some required environment is missing
|
|
_, err := cs.credentials(context.Background())
|
|
assertErr("no AWS_ACCESS_KEY_ID set in environment", err, t)
|
|
|
|
t.Setenv("AWS_ACCESS_KEY_ID", "MYAWSACCESSKEYGOESHERE")
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("no AWS_SECRET_ACCESS_KEY set in environment", err, t)
|
|
|
|
t.Setenv("AWS_SECRET_ACCESS_KEY", "MYAWSSECRETACCESSKEYGOESHERE")
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("no AWS_REGION set in environment", err, t)
|
|
|
|
t.Setenv("AWS_REGION", "us-east-1")
|
|
|
|
expectedCreds := aws.Credentials{
|
|
AccessKey: "MYAWSACCESSKEYGOESHERE",
|
|
SecretKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
RegionName: "us-east-1",
|
|
SessionToken: ""}
|
|
|
|
testCases := []struct {
|
|
tokenEnv string
|
|
tokenValue string
|
|
}{
|
|
// happy path: all required environment is present
|
|
{"", ""},
|
|
// happy path: all required environment is present including security token
|
|
{"AWS_SECURITY_TOKEN", "MYSECURITYTOKENGOESHERE"},
|
|
// happy path: all required environment is present including session token that is preferred over security token
|
|
{"AWS_SESSION_TOKEN", "MYSESSIONTOKENGOESHERE"},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
if testCase.tokenEnv != "" {
|
|
t.Setenv(testCase.tokenEnv, testCase.tokenValue)
|
|
}
|
|
expectedCreds.SessionToken = testCase.tokenValue
|
|
|
|
envCreds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Error("unexpected error: " + err.Error())
|
|
}
|
|
|
|
if envCreds != expectedCreds {
|
|
t.Error("expected: ", expectedCreds, " but got: ", envCreds)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProfileCredentialService(t *testing.T) {
|
|
|
|
defaultKey := "AKIAIOSFODNN7EXAMPLE"
|
|
defaultSecret := "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
|
defaultSessionToken := "AQoEXAMPLEH4aoAH0gNCAPy"
|
|
defaultRegion := "us-west-2"
|
|
|
|
fooKey := "AKIAI44QH8DHBEXAMPLE"
|
|
fooSecret := "je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY"
|
|
fooRegion := "us-east-1"
|
|
|
|
config := fmt.Sprintf(`
|
|
[default]
|
|
aws_access_key_id=%v
|
|
aws_secret_access_key=%v
|
|
aws_session_token=%v
|
|
|
|
[foo]
|
|
aws_access_key_id=%v
|
|
aws_secret_access_key=%v
|
|
`, defaultKey, defaultSecret, defaultSessionToken, fooKey, fooSecret)
|
|
|
|
files := map[string]string{
|
|
"example.ini": config,
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
cfgPath := filepath.Join(path, "example.ini")
|
|
cs := &awsProfileCredentialService{
|
|
Path: cfgPath,
|
|
Profile: "foo",
|
|
RegionName: fooRegion,
|
|
}
|
|
creds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := aws.Credentials{
|
|
AccessKey: fooKey,
|
|
SecretKey: fooSecret,
|
|
RegionName: fooRegion,
|
|
SessionToken: "",
|
|
}
|
|
|
|
if expected != creds {
|
|
t.Fatalf("Expected credentials %v but got %v", expected, creds)
|
|
}
|
|
|
|
// "default" profile
|
|
cs = &awsProfileCredentialService{
|
|
Path: cfgPath,
|
|
Profile: "",
|
|
RegionName: defaultRegion,
|
|
}
|
|
|
|
creds, err = cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected = aws.Credentials{
|
|
AccessKey: defaultKey,
|
|
SecretKey: defaultSecret,
|
|
RegionName: defaultRegion,
|
|
SessionToken: defaultSessionToken,
|
|
}
|
|
|
|
if expected != creds {
|
|
t.Fatalf("Expected credentials %v but got %v", expected, creds)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestProfileCredentialServiceWithEnvVars(t *testing.T) {
|
|
defaultKey := "AKIAIOSFODNN7EXAMPLE"
|
|
defaultSecret := "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
|
defaultSessionToken := "AQoEXAMPLEH4aoAH0gNCAPy"
|
|
defaultRegion := "us-east-1"
|
|
profile := "profileName"
|
|
config := fmt.Sprintf(`
|
|
[%s]
|
|
aws_access_key_id=%s
|
|
aws_secret_access_key=%s
|
|
aws_session_token=%s
|
|
`, profile, defaultKey, defaultSecret, defaultSessionToken)
|
|
|
|
files := map[string]string{
|
|
"example.ini": config,
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
cfgPath := filepath.Join(path, "example.ini")
|
|
|
|
t.Setenv(awsCredentialsFileEnvVar, cfgPath)
|
|
t.Setenv(awsProfileEnvVar, profile)
|
|
t.Setenv(awsRegionEnvVar, defaultRegion)
|
|
|
|
cs := &awsProfileCredentialService{}
|
|
creds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := aws.Credentials{
|
|
AccessKey: defaultKey,
|
|
SecretKey: defaultSecret,
|
|
RegionName: defaultRegion,
|
|
SessionToken: defaultSessionToken,
|
|
}
|
|
|
|
if expected != creds {
|
|
t.Fatalf("Expected credentials %v but got %v", expected, creds)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestProfileCredentialServiceWithDefaultPath(t *testing.T) {
|
|
defaultKey := "AKIAIOSFODNN7EXAMPLE"
|
|
defaultSecret := "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
|
defaultSessionToken := "AQoEXAMPLEH4aoAH0gNCAPy"
|
|
defaultRegion := "us-west-22"
|
|
|
|
config := fmt.Sprintf(`
|
|
[default]
|
|
aws_access_key_id=%s
|
|
aws_secret_access_key=%s
|
|
aws_session_token=%s
|
|
`, defaultKey, defaultSecret, defaultSessionToken)
|
|
|
|
files := map[string]string{}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
|
|
t.Setenv("USERPROFILE", path)
|
|
t.Setenv("HOME", path)
|
|
|
|
cfgDir := filepath.Join(path, ".aws")
|
|
err := os.MkdirAll(cfgDir, os.ModePerm)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := os.WriteFile(filepath.Join(cfgDir, "credentials"), []byte(config), 0600); err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
cs := &awsProfileCredentialService{RegionName: defaultRegion}
|
|
creds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := aws.Credentials{
|
|
AccessKey: defaultKey,
|
|
SecretKey: defaultSecret,
|
|
RegionName: defaultRegion,
|
|
SessionToken: defaultSessionToken,
|
|
}
|
|
|
|
if expected != creds {
|
|
t.Fatalf("Expected credentials %v but got %v", expected, creds)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestProfileCredentialServiceWithError(t *testing.T) {
|
|
configNoAccessKeyID := `
|
|
[default]
|
|
aws_secret_access_key = secret
|
|
`
|
|
|
|
configNoSecret := `
|
|
[default]
|
|
aws_access_key_id=accessKey
|
|
`
|
|
tests := []struct {
|
|
note string
|
|
config string
|
|
err string
|
|
}{
|
|
{
|
|
note: "no aws_access_key_id",
|
|
config: configNoAccessKeyID,
|
|
err: "does not contain \"aws_access_key_id\"",
|
|
},
|
|
{
|
|
note: "no aws_secret_access_key",
|
|
config: configNoSecret,
|
|
err: "does not contain \"aws_secret_access_key\"",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
|
|
files := map[string]string{
|
|
"example.ini": tc.config,
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
cfgPath := filepath.Join(path, "example.ini")
|
|
cs := &awsProfileCredentialService{
|
|
Path: cfgPath,
|
|
}
|
|
_, err := cs.credentials(context.Background())
|
|
if err == nil {
|
|
t.Fatal("Expected error but got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.err) {
|
|
t.Errorf("expected error to contain %v, got %v", tc.err, err.Error())
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMetadataCredentialService(t *testing.T) {
|
|
ts := ec2CredTestServer{}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
// wrong path: cred service path not well formed
|
|
cs := awsMetadataCredentialService{
|
|
RoleName: "my_iam_role",
|
|
RegionName: "us-east-1",
|
|
credServicePath: "this is not a URL", // malformed
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
_, err := cs.credentials(context.Background())
|
|
assertErr("unsupported protocol scheme \"\"", err, t)
|
|
|
|
// wrong path: no role set but no ECS URI in environment
|
|
os.Unsetenv(ecsRelativePathEnvVar)
|
|
cs = awsMetadataCredentialService{
|
|
RegionName: "us-east-1",
|
|
logger: logging.Get(),
|
|
}
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("metadata endpoint cannot be determined from settings and environment", err, t)
|
|
|
|
// wrong path: missing token
|
|
t.Setenv(ecsFullPathEnvVar, "fullPath")
|
|
os.Unsetenv(ecsAuthorizationTokenEnvVar)
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("unable to get ECS metadata authorization token", err, t)
|
|
os.Unsetenv(ecsFullPathEnvVar)
|
|
|
|
// wrong path: creds not found
|
|
cs = awsMetadataCredentialService{
|
|
RoleName: "not_my_iam_role", // not present
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("metadata HTTP request returned unexpected status: 404 Not Found", err, t)
|
|
|
|
// wrong path: malformed JSON body
|
|
cs = awsMetadataCredentialService{
|
|
RoleName: "my_bad_iam_role", // not good
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("failed to parse credential response from metadata service: invalid character 'T' looking for beginning of value", err, t)
|
|
|
|
// wrong path: token service error
|
|
cs = awsMetadataCredentialService{
|
|
RoleName: "my_iam_role",
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/missing_token",
|
|
logger: logging.Get(),
|
|
} // will 404
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("metadata token HTTP request returned unexpected status: 404 Not Found", err, t)
|
|
|
|
// wrong path: token service returns bad token
|
|
cs = awsMetadataCredentialService{
|
|
RoleName: "my_iam_role",
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/bad_token",
|
|
logger: logging.Get(),
|
|
} // not good
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("metadata HTTP request returned unexpected status: 401 Unauthorized", err, t)
|
|
|
|
// wrong path: bad result code from EC2 metadata service
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Failure", // this is bad
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 30)}
|
|
cs = awsMetadataCredentialService{
|
|
RoleName: "my_iam_role",
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("metadata service query did not succeed: Failure", err, t)
|
|
|
|
// happy path: base case
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 300)}
|
|
cs = awsMetadataCredentialService{
|
|
RoleName: "my_iam_role",
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
var creds aws.Credentials
|
|
creds, err = cs.credentials(context.Background())
|
|
if err != nil {
|
|
// Cannot proceed with test if unable to fetch credentials.
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assertEq(creds.AccessKey, ts.payload.AccessKeyID, t)
|
|
assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t)
|
|
assertEq(creds.RegionName, cs.RegionName, t)
|
|
assertEq(creds.SessionToken, ts.payload.Token, t)
|
|
|
|
// happy path: verify credentials are cached based on expiry
|
|
ts.payload.AccessKeyID = "ICHANGEDTHISBUTWEWONTSEEIT"
|
|
creds, err = cs.credentials(context.Background())
|
|
if err != nil {
|
|
// Cannot proceed with test if unable to fetch credentials.
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assertEq(creds.AccessKey, "MYAWSACCESSKEYGOESHERE", t) // the original value
|
|
assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t)
|
|
assertEq(creds.RegionName, cs.RegionName, t)
|
|
assertEq(creds.SessionToken, ts.payload.Token, t)
|
|
|
|
// happy path: with refresh
|
|
// first time through
|
|
cs = awsMetadataCredentialService{
|
|
RoleName: "my_iam_role",
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 2)} // short time
|
|
|
|
creds, err = cs.credentials(context.Background())
|
|
if err != nil {
|
|
// Cannot proceed with test if unable to fetch credentials.
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assertEq(creds.AccessKey, ts.payload.AccessKeyID, t)
|
|
assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t)
|
|
assertEq(creds.RegionName, cs.RegionName, t)
|
|
assertEq(creds.SessionToken, ts.payload.Token, t)
|
|
|
|
// second time through, with changes
|
|
ts.payload.AccessKeyID = "ICHANGEDTHISANDWEWILLSEEIT"
|
|
creds, err = cs.credentials(context.Background())
|
|
if err != nil {
|
|
// Cannot proceed with test if unable to fetch credentials.
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assertEq(creds.AccessKey, ts.payload.AccessKeyID, t) // the new value
|
|
assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t)
|
|
assertEq(creds.RegionName, cs.RegionName, t)
|
|
assertEq(creds.SessionToken, ts.payload.Token, t)
|
|
|
|
// happy path: credentials fetched from full path var
|
|
cs = awsMetadataCredentialService{
|
|
RegionName: "us-east-1",
|
|
credServicePath: "", // not set as we want to test env var resolution
|
|
logger: logging.Get(),
|
|
}
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 2)} // short time
|
|
t.Setenv(ecsFullPathEnvVar, ts.server.URL+"/fullPath")
|
|
t.Setenv(ecsAuthorizationTokenEnvVar, "THIS_IS_A_GOOD_TOKEN")
|
|
|
|
creds, err = cs.credentials(context.Background())
|
|
if err != nil {
|
|
// Cannot proceed with test if unable to fetch credentials.
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assertEq(creds.AccessKey, ts.payload.AccessKeyID, t)
|
|
assertEq(creds.SecretKey, ts.payload.SecretAccessKey, t)
|
|
assertEq(creds.RegionName, cs.RegionName, t)
|
|
assertEq(creds.SessionToken, ts.payload.Token, t)
|
|
os.Unsetenv(ecsFullPathEnvVar)
|
|
os.Unsetenv(ecsAuthorizationTokenEnvVar)
|
|
}
|
|
|
|
func TestMetadataServiceErrorHandled(t *testing.T) {
|
|
ts := ec2CredTestServer{}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
// wrong path: handle errors from credential service
|
|
cs := &awsMetadataCredentialService{
|
|
RoleName: "not_my_iam_role", // not present
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
|
|
_, err := cs.credentials(context.Background())
|
|
assertErr("metadata HTTP request returned unexpected status: 404 Not Found", err, t)
|
|
}
|
|
|
|
func TestV4Signing(t *testing.T) {
|
|
ts := ec2CredTestServer{}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
// happy path: sign correctly
|
|
cs := &awsMetadataCredentialService{
|
|
RoleName: "my_iam_role", // not present
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 2)}
|
|
req, _ := http.NewRequest("GET", "https://mybucket.s3.amazonaws.com/bundle.tar.gz", strings.NewReader(""))
|
|
|
|
// force a non-random source so that we can predict the v4a signing key and, thus, signature
|
|
myReader := strings.NewReader("000000000000000000000000000000000")
|
|
aws.SetRandomSource(myReader)
|
|
defer func() { aws.SetRandomSource(rand.Reader) }()
|
|
|
|
tests := []struct {
|
|
sigVersion string
|
|
expectedAuthorization []string
|
|
}{
|
|
{
|
|
sigVersion: "4",
|
|
expectedAuthorization: []string{
|
|
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/s3/aws4_request," +
|
|
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token," +
|
|
"Signature=d3f0561abae5e35d9ee2c15e678bb7acacc4b4743707a8f7fbcbfdb519078990",
|
|
},
|
|
},
|
|
{
|
|
sigVersion: "4a",
|
|
expectedAuthorization: []string{
|
|
// this signature is for go 1.20+, which changed crypto/ecdsa so signatures differ from go 1.18
|
|
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/s3/aws4_request, " +
|
|
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
|
|
"Signature=3045022031b9dd601cd02650193586a32721d0614bf2e34bbc76cff0d9812366d1dc8878022100d0cfbd91bd2dd98f1e2d7feb9091c48f8b66a20174922770ec9e3b74db8e1826",
|
|
// this signature is for go 1.18+. Remove this and only test for a single value when OPA drops go 1.19
|
|
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/s3/aws4_request, " +
|
|
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
|
|
"Signature=304402207d1bcb6fb68d85be3e9f6948a8dc8596a531b3f5a82ca2350acabe98941312bc02207d81ed07c7356226d93611820548a806c8e1f0cc72ff41ba672d23901e5a06bf",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
creds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal("unexpected error getting credentials")
|
|
}
|
|
|
|
if err := aws.SignRequest(req, "s3", creds, time.Unix(1556129697, 0), test.sigVersion); err != nil {
|
|
t.Fatal("unexpected error during signing", err)
|
|
}
|
|
|
|
// expect mandatory headers
|
|
assertEq("mybucket.s3.amazonaws.com", req.Header.Get("Host"), t)
|
|
assertIn(test.expectedAuthorization, req.Header.Get("Authorization"), t)
|
|
assertEq("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
req.Header.Get("X-Amz-Content-Sha256"), t)
|
|
assertEq("20190424T181457Z", req.Header.Get("X-Amz-Date"), t)
|
|
assertEq("MYAWSSECURITYTOKENGOESHERE", req.Header.Get("X-Amz-Security-Token"), t)
|
|
}
|
|
}
|
|
|
|
func TestV4SigningUnsignedPayload(t *testing.T) {
|
|
ts := ec2CredTestServer{}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
// happy path: sign correctly
|
|
cs := &awsMetadataCredentialService{
|
|
RoleName: "my_iam_role", // not present
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 2)}
|
|
|
|
// force a non-random source so that we can predict the v4a signing key and, thus, signature
|
|
myReader := strings.NewReader("000000000000000000000000000000000")
|
|
aws.SetRandomSource(myReader)
|
|
defer func() { aws.SetRandomSource(rand.Reader) }()
|
|
|
|
tests := []struct {
|
|
disablePayloadSigning bool
|
|
expectedAuthorization []string
|
|
expectedShaHeaderVal string
|
|
}{
|
|
{
|
|
disablePayloadSigning: true,
|
|
expectedAuthorization: []string{
|
|
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/s3/aws4_request," +
|
|
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token," +
|
|
"Signature=3682e55f6d86d3372003b3d28c74aa960f076d91fce833b129ae76415a12e5e4",
|
|
},
|
|
expectedShaHeaderVal: "UNSIGNED-PAYLOAD",
|
|
},
|
|
{
|
|
disablePayloadSigning: false,
|
|
expectedAuthorization: []string{
|
|
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/s3/aws4_request," +
|
|
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token," +
|
|
"Signature=d3f0561abae5e35d9ee2c15e678bb7acacc4b4743707a8f7fbcbfdb519078990",
|
|
},
|
|
expectedShaHeaderVal: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
creds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal("unexpected error getting credentials")
|
|
}
|
|
req, _ := http.NewRequest("GET", "https://mybucket.s3.amazonaws.com/bundle.tar.gz", strings.NewReader(""))
|
|
var body []byte
|
|
if req.Body == nil {
|
|
body = []byte("")
|
|
} else {
|
|
body, _ = io.ReadAll(req.Body)
|
|
req.Body = io.NopCloser(bytes.NewReader(body))
|
|
}
|
|
|
|
authHeader, awsHeaders := aws.SignV4(req.Header, req.Method, req.URL, body, "s3", creds, time.Unix(1556129697, 0).UTC(), test.disablePayloadSigning)
|
|
req.Header.Set("Authorization", authHeader)
|
|
for k, v := range awsHeaders {
|
|
req.Header.Add(k, v)
|
|
}
|
|
|
|
// expect mandatory headers
|
|
assertEq("mybucket.s3.amazonaws.com", req.Header.Get("Host"), t)
|
|
assertIn(test.expectedAuthorization, req.Header.Get("Authorization"), t)
|
|
assertEq(test.expectedShaHeaderVal, req.Header.Get("X-Amz-Content-Sha256"), t)
|
|
assertEq("20190424T181457Z", req.Header.Get("X-Amz-Date"), t)
|
|
assertEq("MYAWSSECURITYTOKENGOESHERE", req.Header.Get("X-Amz-Security-Token"), t)
|
|
}
|
|
}
|
|
|
|
func TestV4SigningForApiGateway(t *testing.T) {
|
|
ts := ec2CredTestServer{}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
cs := &awsMetadataCredentialService{
|
|
RoleName: "my_iam_role", // not present
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 2)}
|
|
req, _ := http.NewRequest("POST", "https://myrestapi.execute-api.us-east-1.amazonaws.com/prod/logs",
|
|
strings.NewReader("{ \"payload\": 42 }"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
creds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal("unexpected error getting credentials")
|
|
}
|
|
|
|
if err := aws.SignRequest(req, "execute-api", creds, time.Unix(1556129697, 0), "4"); err != nil {
|
|
t.Fatal("unexpected error during signing")
|
|
}
|
|
|
|
// expect mandatory headers
|
|
assertEq(req.Header.Get("Host"), "myrestapi.execute-api.us-east-1.amazonaws.com", t)
|
|
assertEq(req.Header.Get("Authorization"),
|
|
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/execute-api/aws4_request,"+
|
|
"SignedHeaders=content-type;host;x-amz-date;x-amz-security-token,"+
|
|
"Signature=c8ee72cc45050b255bcbf19defc693f7cd788959b5380fa0985de6e865635339", t)
|
|
// no content sha should be set, since this is specific to s3 and glacier
|
|
assertEq(req.Header.Get("X-Amz-Content-Sha256"), "", t)
|
|
assertEq(req.Header.Get("X-Amz-Date"), "20190424T181457Z", t)
|
|
assertEq(req.Header.Get("X-Amz-Security-Token"), "MYAWSSECURITYTOKENGOESHERE", t)
|
|
}
|
|
|
|
func TestV4SigningOmitsIgnoredHeaders(t *testing.T) {
|
|
ts := ec2CredTestServer{}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
cs := &awsMetadataCredentialService{
|
|
RoleName: "my_iam_role", // not present
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 2)}
|
|
req, _ := http.NewRequest("POST", "https://myrestapi.execute-api.us-east-1.amazonaws.com/prod/logs",
|
|
strings.NewReader("{ \"payload\": 42 }"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// These are headers that should never be included in the signed headers
|
|
req.Header.Set("User-Agent", "Unit Tests!")
|
|
req.Header.Set("Authorization", "Auth header will be overwritten, and shouldn't be signed")
|
|
req.Header.Set("X-Amzn-Trace-Id", "Some trace id")
|
|
|
|
// force a non-random source so that we can predict the v4a signing key and, thus, signature
|
|
myReader := strings.NewReader("000000000000000000000000000000000")
|
|
aws.SetRandomSource(myReader)
|
|
defer func() { aws.SetRandomSource(rand.Reader) }()
|
|
|
|
tests := []struct {
|
|
sigVersion string
|
|
expectedAuthorization []string
|
|
}{
|
|
{
|
|
sigVersion: "4",
|
|
expectedAuthorization: []string{"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/execute-api/aws4_request," +
|
|
"SignedHeaders=content-type;host;x-amz-date;x-amz-security-token," +
|
|
"Signature=c8ee72cc45050b255bcbf19defc693f7cd788959b5380fa0985de6e865635339",
|
|
},
|
|
},
|
|
{
|
|
sigVersion: "4a",
|
|
expectedAuthorization: []string{
|
|
// this signature is for go 1.20+, which changed crypto/ecdsa so signatures differ from go 1.18
|
|
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/execute-api/aws4_request, " +
|
|
"SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
|
|
"Signature=3045022100e62b33949d5d5666c1cc737db6673600d7893b977df48e4eb64a6e8747582a2f022011f56ad285472956a3e00c6971d03ebd8ecb579804d8fd91a6fb483a1f502118",
|
|
// this signature is for go 1.18+. Remove this and only test for a single value when OPA drops go 1.19
|
|
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/execute-api/aws4_request, " +
|
|
"SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
|
|
"Signature=30450221009f3b0cda178456dfd1bec61b78bdbd115c0cf497eaa52c58bbb2850ad9c49c3002207009cb88a1219a4a6626056c31823a6b5bc2728bc88bc98a06e12e1148482c94",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
creds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal("unexpected error getting credentials")
|
|
}
|
|
|
|
if err := aws.SignRequest(req, "execute-api", creds, time.Unix(1556129697, 0), test.sigVersion); err != nil {
|
|
t.Fatal("unexpected error during signing")
|
|
}
|
|
|
|
// Check the signed headers doesn't include user-agent, authorization or x-amz-trace-id
|
|
assertIn(test.expectedAuthorization, req.Header.Get("Authorization"), t)
|
|
// The headers omitted from signing should still be present in the request
|
|
assertEq(req.Header.Get("User-Agent"), "Unit Tests!", t)
|
|
assertEq(req.Header.Get("X-Amzn-Trace-Id"), "Some trace id", t)
|
|
}
|
|
|
|
}
|
|
|
|
func TestV4SigningCustomPort(t *testing.T) {
|
|
ts := ec2CredTestServer{}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
cs := &awsMetadataCredentialService{
|
|
RoleName: "my_iam_role", // not present
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 2)}
|
|
req, _ := http.NewRequest("GET", "https://custom.s3.server:9000/bundle.tar.gz", strings.NewReader(""))
|
|
|
|
creds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal("unexpected error getting credentials")
|
|
}
|
|
|
|
if err := aws.SignRequest(req, "s3", creds, time.Unix(1556129697, 0), "4"); err != nil {
|
|
t.Fatal("unexpected error during signing")
|
|
}
|
|
|
|
// expect mandatory headers
|
|
assertEq(req.Header.Get("Host"), "custom.s3.server:9000", t)
|
|
assertEq(req.Header.Get("Authorization"),
|
|
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/s3/aws4_request,"+
|
|
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token,"+
|
|
"Signature=765b67c6b136f99d9b769171c9939fc444021f7d17e4fbe6e1ab8b1926713c2b", t)
|
|
assertEq(req.Header.Get("X-Amz-Content-Sha256"),
|
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", t)
|
|
assertEq(req.Header.Get("X-Amz-Date"), "20190424T181457Z", t)
|
|
assertEq(req.Header.Get("X-Amz-Security-Token"), "MYAWSSECURITYTOKENGOESHERE", t)
|
|
}
|
|
|
|
func TestV4SigningDoesNotMutateBody(t *testing.T) {
|
|
ts := ec2CredTestServer{}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
cs := &awsMetadataCredentialService{
|
|
RoleName: "my_iam_role", // not present
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 2)}
|
|
|
|
// force a non-random source so that we can predict the v4a signing key and, thus, signature
|
|
myReader := strings.NewReader("000000000000000000000000000000000")
|
|
aws.SetRandomSource(myReader)
|
|
defer func() { aws.SetRandomSource(rand.Reader) }()
|
|
|
|
tests := []struct {
|
|
sigVersion string
|
|
}{
|
|
{sigVersion: "4"},
|
|
{sigVersion: "4a"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
req, _ := http.NewRequest("POST", "https://myrestapi.execute-api.us-east-1.amazonaws.com/prod/logs",
|
|
strings.NewReader("{ \"payload\": 42 }"))
|
|
|
|
creds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal("unexpected error getting credentials")
|
|
}
|
|
|
|
if err := aws.SignRequest(req, "execute-api", creds, time.Unix(1556129697, 0), test.sigVersion); err != nil {
|
|
t.Fatal("unexpected error during signing")
|
|
}
|
|
|
|
// Read the body and check that it was not mutated
|
|
body, _ := io.ReadAll(req.Body)
|
|
assertEq(string(body), "{ \"payload\": 42 }", t)
|
|
}
|
|
}
|
|
|
|
func TestV4SigningWithMultiValueHeaders(t *testing.T) {
|
|
ts := ec2CredTestServer{}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
cs := &awsMetadataCredentialService{
|
|
RoleName: "my_iam_role", // not present
|
|
RegionName: "us-east-1",
|
|
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: ts.server.URL + "/latest/api/token",
|
|
logger: logging.Get(),
|
|
}
|
|
ts.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 2)}
|
|
req, _ := http.NewRequest("POST", "https://myrestapi.execute-api.us-east-1.amazonaws.com/prod/logs",
|
|
strings.NewReader("{ \"payload\": 42 }"))
|
|
req.Header.Add("Accept", "text/plain")
|
|
req.Header.Add("Accept", "text/html")
|
|
|
|
// force a non-random source so that we can predict the v4a signing key and, thus, signature
|
|
myReader := strings.NewReader("000000000000000000000000000000000")
|
|
aws.SetRandomSource(myReader)
|
|
defer func() { aws.SetRandomSource(rand.Reader) }()
|
|
|
|
tests := []struct {
|
|
sigVersion string
|
|
expectedAuthorization []string
|
|
}{
|
|
{
|
|
sigVersion: "4",
|
|
expectedAuthorization: []string{
|
|
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/execute-api/aws4_request," +
|
|
"SignedHeaders=accept;host;x-amz-date;x-amz-security-token," +
|
|
"Signature=0237b0c789cad36212f0efba70c02549e1f659ab9caaca16423930cc7236c046",
|
|
},
|
|
},
|
|
{
|
|
sigVersion: "4a",
|
|
expectedAuthorization: []string{
|
|
// this signature is for go 1.20+, which changed crypto/ecdsa so signatures differ from go 1.18
|
|
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/execute-api/aws4_request, " +
|
|
"SignedHeaders=accept;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
|
|
"Signature=3046022100f7fd07e2a00b1be3074be0c2e3871bd42ddc4c01549b1ffc4809ef3fafde80780221008c6bf906cdb9040ebeb94d1134598e7920fa8cb7bda91b00ce0ab9838b79631b",
|
|
// this signature is for go 1.18+. Remove this and only test for a single value when OPA drops go 1.19
|
|
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/execute-api/aws4_request, " +
|
|
"SignedHeaders=accept;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
|
|
"Signature=304402202d5f2d4d42fe59b2e61fa455cb35a335139d109c2d37aaa8946d45fd0fb4989c022068238cbfbc80326f5cc391f2b6837910191ceabb58ec0bf986c0141f76046594",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
creds, err := cs.credentials(context.Background())
|
|
if err != nil {
|
|
t.Fatal("unexpected error getting credentials")
|
|
}
|
|
|
|
if err := aws.SignRequest(req, "execute-api", creds, time.Unix(1556129697, 0), test.sigVersion); err != nil {
|
|
t.Fatal("unexpected error during signing")
|
|
}
|
|
|
|
if len(req.Header.Values("Authorization")) != 1 {
|
|
t.Fatal("Authorization header is multi-valued. This will break AWS v4 signing.")
|
|
}
|
|
// Check the signed headers includes our multi-value 'accept' header
|
|
assertIn(test.expectedAuthorization, req.Header.Get("Authorization"), t)
|
|
// The multi-value headers are preserved
|
|
assertEq("text/plain", req.Header.Values("Accept")[0], t)
|
|
assertEq("text/html", req.Header.Values("Accept")[1], t)
|
|
}
|
|
}
|
|
|
|
// simulate EC2 metadata service
|
|
type ec2CredTestServer struct {
|
|
server *httptest.Server
|
|
payload metadataPayload // must set before use
|
|
}
|
|
|
|
func (t *ec2CredTestServer) handle(w http.ResponseWriter, r *http.Request) {
|
|
goodPath := "/latest/meta-data/iam/security-credentials/my_iam_role"
|
|
badPath := "/latest/meta-data/iam/security-credentials/my_bad_iam_role"
|
|
goodPathFull := "/fullPath"
|
|
|
|
goodTokenPath := "/latest/api/token"
|
|
badTokenPath := "/latest/api/bad_token"
|
|
|
|
tokenValue := "THIS_IS_A_GOOD_TOKEN"
|
|
jsonBytes, _ := json.Marshal(t.payload)
|
|
|
|
switch r.URL.Path {
|
|
case goodTokenPath:
|
|
// a valid token
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write([]byte(tokenValue))
|
|
case badTokenPath:
|
|
// an invalid token
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write([]byte("THIS_IS_A_BAD_TOKEN"))
|
|
case goodPath:
|
|
// validate token...
|
|
if r.Header.Get("X-aws-ec2-metadata-token") == tokenValue {
|
|
// a metadata response that's well-formed
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write(jsonBytes)
|
|
} else {
|
|
// an unauthorized response
|
|
w.WriteHeader(401)
|
|
}
|
|
case badPath:
|
|
// a metadata response that's not well-formed
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write([]byte("This isn't a JSON payload"))
|
|
case goodPathFull:
|
|
// validate token...
|
|
if r.Header.Get("Authorization") == tokenValue {
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write(jsonBytes)
|
|
} else {
|
|
// AWS returns a 404 if the token is wrong
|
|
w.WriteHeader(404)
|
|
}
|
|
default:
|
|
// something else that we won't be able to find
|
|
w.WriteHeader(404)
|
|
}
|
|
}
|
|
|
|
func (t *ec2CredTestServer) start() {
|
|
t.server = httptest.NewServer(http.HandlerFunc(t.handle))
|
|
}
|
|
|
|
func (t *ec2CredTestServer) stop() {
|
|
t.server.Close()
|
|
}
|
|
|
|
func TestWebIdentityCredentialService(t *testing.T) {
|
|
t.Setenv("AWS_REGION", "us-west-1")
|
|
|
|
testAccessKey := "ASgeIAIOSFODNN7EXAMPLE"
|
|
ts := stsTestServer{
|
|
t: t,
|
|
accessKey: testAccessKey,
|
|
assumeRoleWithWebIdentity: true,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
cs := awsWebIdentityCredentialService{
|
|
stsURL: ts.server.URL,
|
|
logger: logging.Get(),
|
|
}
|
|
|
|
files := map[string]string{
|
|
"good_token_file": "good-token",
|
|
"bad_token_file": "bad-token",
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
goodTokenFile := filepath.Join(path, "good_token_file")
|
|
badTokenFile := filepath.Join(path, "bad_token_file")
|
|
|
|
// wrong path: no AWS_ROLE_ARN set
|
|
err := cs.populateFromEnv()
|
|
assertErr("no AWS_ROLE_ARN set in environment", err, t)
|
|
t.Setenv("AWS_ROLE_ARN", "role:arn")
|
|
|
|
// wrong path: no AWS_WEB_IDENTITY_TOKEN_FILE set
|
|
err = cs.populateFromEnv()
|
|
assertErr("no AWS_WEB_IDENTITY_TOKEN_FILE set in environment", err, t)
|
|
t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", "/nonsense")
|
|
|
|
// happy path: both env vars set
|
|
err = cs.populateFromEnv()
|
|
if err != nil {
|
|
t.Fatalf("Error while getting env vars: %s", err)
|
|
}
|
|
|
|
// wrong path: refresh with invalid web token file
|
|
err = cs.refreshFromService(context.Background())
|
|
assertErr("unable to read web token for sts HTTP request: open /nonsense: no such file or directory", err, t)
|
|
|
|
// wrong path: refresh with "bad token"
|
|
t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", badTokenFile)
|
|
_ = cs.populateFromEnv()
|
|
err = cs.refreshFromService(context.Background())
|
|
assertErr("STS HTTP request returned unexpected status: 401 Unauthorized", err, t)
|
|
|
|
// happy path: refresh with "good token"
|
|
t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", goodTokenFile)
|
|
_ = cs.populateFromEnv()
|
|
err = cs.refreshFromService(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected err: %s", err)
|
|
}
|
|
|
|
// happy path: refresh and get credentials
|
|
creds, _ := cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: refresh with session and get credentials
|
|
cs.expiration = time.Now()
|
|
cs.SessionName = "TEST_SESSION"
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: don't refresh, but get credentials
|
|
ts.accessKey = "OTHERKEY"
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy/wrong path: refresh with "bad token" but return previous credentials
|
|
t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", badTokenFile)
|
|
_ = cs.populateFromEnv()
|
|
cs.expiration = time.Now()
|
|
creds, err = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
assertErr("STS HTTP request returned unexpected status: 401 Unauthorized", err, t)
|
|
|
|
// wrong path: refresh with "bad token" but return previous credentials
|
|
t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", goodTokenFile)
|
|
t.Setenv("AWS_ROLE_ARN", "BrokenRole")
|
|
_ = cs.populateFromEnv()
|
|
cs.expiration = time.Now()
|
|
creds, err = cs.credentials(context.Background())
|
|
assertErr("failed to parse credential response from STS service: EOF", err, t)
|
|
})
|
|
}
|
|
|
|
func TestAssumeRoleCredentialServiceUsingWrongSigningProvider(t *testing.T) {
|
|
t.Setenv("AWS_REGION", "us-west-1")
|
|
|
|
testAccessKey := "ASgeIAIOSFODNN7EXAMPLE"
|
|
ts := stsTestServer{
|
|
t: t,
|
|
accessKey: testAccessKey,
|
|
assumeRoleWithWebIdentity: false,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
cs := awsAssumeRoleCredentialService{
|
|
stsURL: ts.server.URL,
|
|
logger: logging.Get(),
|
|
}
|
|
|
|
// wrong path: no AWS signing plugin
|
|
err := cs.populateFromEnv()
|
|
assertErr("a AWS signing plugin must be specified when AssumeRole credential provider is enabled", err, t)
|
|
|
|
// wrong path: unsupported AWS signing plugin
|
|
cs.AWSSigningPlugin = &awsSigningAuthPlugin{AWSWebIdentityCredentials: &awsWebIdentityCredentialService{}}
|
|
err = cs.populateFromEnv()
|
|
assertErr("unsupported AWS signing plugin with AssumeRole credential provider", err, t)
|
|
}
|
|
|
|
func TestAssumeRoleCredentialServiceUsingEnvCredentialsProvider(t *testing.T) {
|
|
t.Setenv("AWS_REGION", "us-west-1")
|
|
|
|
testAccessKey := "ASgeIAIOSFODNN7EXAMPLE"
|
|
ts := stsTestServer{
|
|
t: t,
|
|
accessKey: testAccessKey,
|
|
assumeRoleWithWebIdentity: false,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
cs := awsAssumeRoleCredentialService{
|
|
stsURL: ts.server.URL,
|
|
logger: logging.Get(),
|
|
AWSSigningPlugin: &awsSigningAuthPlugin{AWSEnvironmentCredentials: &awsEnvironmentCredentialService{}},
|
|
}
|
|
|
|
// wrong path: no AWS IAM Role ARN set in environment or config
|
|
err := cs.populateFromEnv()
|
|
assertErr("no AWS_ROLE_ARN set in environment or configuration", err, t)
|
|
t.Setenv("AWS_ROLE_ARN", "role:arn")
|
|
|
|
// happy path: set AWS IAM Role ARN as env var
|
|
err = cs.populateFromEnv()
|
|
if err != nil {
|
|
t.Fatalf("Error while getting env vars: %s", err)
|
|
}
|
|
|
|
// happy path: set AWS IAM Role ARN in config
|
|
os.Unsetenv("AWS_ROLE_ARN")
|
|
cs.RoleArn = "role:arn"
|
|
|
|
err = cs.populateFromEnv()
|
|
if err != nil {
|
|
t.Fatalf("Error while getting env vars: %s", err)
|
|
}
|
|
|
|
// wrong path: refresh and get credentials but signing credentials not set via env variables
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("no AWS_ACCESS_KEY_ID set in environment", err, t)
|
|
|
|
t.Setenv("AWS_ACCESS_KEY_ID", "MYAWSACCESSKEYGOESHERE")
|
|
|
|
_, err = cs.credentials(context.Background())
|
|
assertErr("no AWS_SECRET_ACCESS_KEY set in environment", err, t)
|
|
|
|
t.Setenv("AWS_SECRET_ACCESS_KEY", "MYAWSSECRETACCESSKEYGOESHERE")
|
|
|
|
// happy path: refresh and get credentials
|
|
creds, _ := cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: refresh with session and get credentials
|
|
cs.expiration = time.Now()
|
|
cs.SessionName = "TEST_SESSION"
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: don't refresh as credentials not expired so STS not called
|
|
// verify existing credentials haven't changed
|
|
ts.accessKey = "OTHERKEY"
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: refresh expired credentials
|
|
// verify new credentials are set
|
|
cs.expiration = time.Now()
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, ts.accessKey, t)
|
|
}
|
|
|
|
func TestAssumeRoleCredentialServiceUsingProfileCredentialsProvider(t *testing.T) {
|
|
t.Setenv("AWS_REGION", "us-west-1")
|
|
|
|
testAccessKey := "ASgeIAIOSFODNN7EXAMPLE"
|
|
ts := stsTestServer{
|
|
t: t,
|
|
accessKey: testAccessKey,
|
|
assumeRoleWithWebIdentity: false,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
defaultKey := "MYAWSACCESSKEYGOESHERE"
|
|
defaultSecret := "MYAWSSECRETACCESSKEYGOESHERE"
|
|
defaultSessionToken := "AQoEXAMPLEH4aoAH0gNCAPy"
|
|
|
|
config := fmt.Sprintf(`
|
|
[foo]
|
|
aws_access_key_id=%v
|
|
aws_secret_access_key=%v
|
|
aws_session_token=%v
|
|
`, defaultKey, defaultSecret, defaultSessionToken)
|
|
|
|
files := map[string]string{
|
|
"example.ini": config,
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
cfgPath := filepath.Join(path, "example.ini")
|
|
|
|
cs := awsAssumeRoleCredentialService{
|
|
stsURL: ts.server.URL,
|
|
logger: logging.Get(),
|
|
AWSSigningPlugin: &awsSigningAuthPlugin{AWSProfileCredentials: &awsProfileCredentialService{Path: cfgPath, Profile: "foo"}},
|
|
}
|
|
|
|
// wrong path: no AWS IAM Role ARN set in environment or config
|
|
err := cs.populateFromEnv()
|
|
assertErr("no AWS_ROLE_ARN set in environment or configuration", err, t)
|
|
t.Setenv("AWS_ROLE_ARN", "role:arn")
|
|
|
|
// happy path: set AWS IAM Role ARN as env var
|
|
err = cs.populateFromEnv()
|
|
if err != nil {
|
|
t.Fatalf("Error while getting env vars: %s", err)
|
|
}
|
|
|
|
// happy path: set AWS IAM Role ARN in config
|
|
os.Unsetenv("AWS_ROLE_ARN")
|
|
cs.RoleArn = "role:arn"
|
|
|
|
err = cs.populateFromEnv()
|
|
if err != nil {
|
|
t.Fatalf("Error while getting env vars: %s", err)
|
|
}
|
|
|
|
// happy path: refresh and get credentials
|
|
creds, _ := cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: refresh with session and get credentials
|
|
cs.expiration = time.Now()
|
|
cs.SessionName = "TEST_SESSION"
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: don't refresh as credentials not expired so STS not called
|
|
// verify existing credentials haven't changed
|
|
ts.accessKey = "OTHERKEY"
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: refresh expired credentials
|
|
// verify new credentials are set
|
|
cs.expiration = time.Now()
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, ts.accessKey, t)
|
|
})
|
|
}
|
|
|
|
func TestAssumeRoleCredentialServiceUsingMetadataCredentialsProvider(t *testing.T) {
|
|
t.Setenv("AWS_REGION", "us-west-1")
|
|
|
|
testAccessKey := "ASgeIAIOSFODNN7EXAMPLE"
|
|
ts := stsTestServer{
|
|
t: t,
|
|
accessKey: testAccessKey,
|
|
assumeRoleWithWebIdentity: false,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
tsMetadata := ec2CredTestServer{}
|
|
tsMetadata.payload = metadataPayload{
|
|
AccessKeyID: "MYAWSACCESSKEYGOESHERE",
|
|
SecretAccessKey: "MYAWSSECRETACCESSKEYGOESHERE",
|
|
Code: "Success",
|
|
Token: "MYAWSSECURITYTOKENGOESHERE",
|
|
Expiration: time.Now().UTC().Add(time.Minute * 300)}
|
|
tsMetadata.start()
|
|
defer tsMetadata.stop()
|
|
|
|
cs := awsAssumeRoleCredentialService{
|
|
stsURL: ts.server.URL,
|
|
logger: logging.Get(),
|
|
AWSSigningPlugin: &awsSigningAuthPlugin{AWSMetadataCredentials: &awsMetadataCredentialService{RoleName: "my_iam_role", credServicePath: tsMetadata.server.URL + "/latest/meta-data/iam/security-credentials/",
|
|
tokenPath: tsMetadata.server.URL + "/latest/api/token"}},
|
|
}
|
|
|
|
// wrong path: no AWS IAM Role ARN set in environment or config
|
|
err := cs.populateFromEnv()
|
|
assertErr("no AWS_ROLE_ARN set in environment or configuration", err, t)
|
|
t.Setenv("AWS_ROLE_ARN", "role:arn")
|
|
|
|
// happy path: set AWS IAM Role ARN as env var
|
|
err = cs.populateFromEnv()
|
|
if err != nil {
|
|
t.Fatalf("Error while getting env vars: %s", err)
|
|
}
|
|
|
|
// happy path: set AWS IAM Role ARN in config
|
|
os.Unsetenv("AWS_ROLE_ARN")
|
|
cs.RoleArn = "role:arn"
|
|
|
|
err = cs.populateFromEnv()
|
|
if err != nil {
|
|
t.Fatalf("Error while getting env vars: %s", err)
|
|
}
|
|
|
|
// happy path: refresh and get credentials
|
|
creds, _ := cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: refresh with session and get credentials
|
|
cs.expiration = time.Now()
|
|
cs.SessionName = "TEST_SESSION"
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: don't refresh as credentials not expired so STS not called
|
|
// verify existing credentials haven't changed
|
|
ts.accessKey = "OTHERKEY"
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, testAccessKey, t)
|
|
|
|
// happy path: refresh expired credentials
|
|
// verify new credentials are set
|
|
cs.expiration = time.Now()
|
|
creds, _ = cs.credentials(context.Background())
|
|
assertEq(creds.AccessKey, ts.accessKey, t)
|
|
}
|
|
|
|
func TestStsPath(t *testing.T) {
|
|
cs := awsWebIdentityCredentialService{}
|
|
|
|
defaultPath := fmt.Sprintf(stsDefaultPath, stsDefaultDomain)
|
|
assertEq(defaultPath, cs.stsPath(), t)
|
|
|
|
cs.RegionName = "us-east-2"
|
|
assertEq("https://sts.us-east-2.amazonaws.com", cs.stsPath(), t)
|
|
|
|
cs.Domain = "example.com"
|
|
assertEq("https://sts.us-east-2.example.com", cs.stsPath(), t)
|
|
|
|
cs.stsURL = "http://test.com"
|
|
assertEq("http://test.com", cs.stsPath(), t)
|
|
}
|
|
|
|
func TestStsPathFromEnv(t *testing.T) {
|
|
t.Setenv(awsRoleArnEnvVar, "role:arn")
|
|
t.Setenv(awsWebIdentityTokenFileEnvVar, "/nonsense")
|
|
|
|
tests := []struct {
|
|
note string
|
|
env map[string]string
|
|
cs awsWebIdentityCredentialService
|
|
want string
|
|
}{
|
|
{
|
|
note: "region set in config",
|
|
cs: awsWebIdentityCredentialService{
|
|
RegionName: "us-east-2",
|
|
},
|
|
want: "https://sts.us-east-2.amazonaws.com",
|
|
},
|
|
{
|
|
note: "region set in env",
|
|
env: map[string]string{
|
|
awsRegionEnvVar: "us-east-1",
|
|
},
|
|
want: "https://sts.us-east-1.amazonaws.com",
|
|
},
|
|
{
|
|
note: "region set in env and config (config wins)",
|
|
env: map[string]string{
|
|
awsRegionEnvVar: "us-east-1",
|
|
},
|
|
cs: awsWebIdentityCredentialService{
|
|
RegionName: "us-east-2",
|
|
},
|
|
want: "https://sts.us-east-2.amazonaws.com",
|
|
},
|
|
{
|
|
note: "domain set in config",
|
|
cs: awsWebIdentityCredentialService{
|
|
RegionName: "us-east-2",
|
|
Domain: "foo.example.com",
|
|
},
|
|
want: "https://sts.us-east-2.foo.example.com",
|
|
},
|
|
{
|
|
note: "domain set in env",
|
|
env: map[string]string{
|
|
awsDomainEnvVar: "bar.example.com",
|
|
},
|
|
cs: awsWebIdentityCredentialService{
|
|
RegionName: "us-east-2", // Region must always be set
|
|
},
|
|
want: "https://sts.us-east-2.bar.example.com",
|
|
},
|
|
{
|
|
note: "domain set in env and config (config wins)",
|
|
env: map[string]string{
|
|
awsDomainEnvVar: "bar.example.com",
|
|
},
|
|
cs: awsWebIdentityCredentialService{
|
|
RegionName: "us-east-2", // Region must always be set
|
|
Domain: "foo.example.com",
|
|
},
|
|
want: "https://sts.us-east-2.foo.example.com",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
for k, v := range tc.env {
|
|
t.Setenv(k, v)
|
|
}
|
|
if err := tc.cs.populateFromEnv(); err != nil {
|
|
t.Fatalf("Unexpected err: %s", err)
|
|
}
|
|
assertEq(tc.want, tc.cs.stsPath(), t)
|
|
})
|
|
}
|
|
}
|
|
|
|
// simulate AWS Security Token Service (AWS STS)
|
|
type stsTestServer struct {
|
|
t *testing.T
|
|
server *httptest.Server
|
|
accessKey string
|
|
assumeRoleWithWebIdentity bool
|
|
}
|
|
|
|
func (t *stsTestServer) handle(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/" || r.Method != http.MethodPost {
|
|
w.WriteHeader(404)
|
|
return
|
|
}
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
|
|
if r.PostForm.Get("Action") != "AssumeRoleWithWebIdentity" && r.PostForm.Get("Action") != "AssumeRole" {
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
|
|
if r.PostForm.Get("RoleArn") == "BrokenRole" {
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write([]byte("{}"))
|
|
return
|
|
}
|
|
|
|
if t.assumeRoleWithWebIdentity {
|
|
token := r.PostForm.Get("WebIdentityToken")
|
|
if token != "good-token" {
|
|
w.WriteHeader(401)
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
}
|
|
|
|
sessionName := r.PostForm.Get("RoleSessionName")
|
|
|
|
var xmlResponse string
|
|
|
|
if t.assumeRoleWithWebIdentity {
|
|
// Taken from STS docs: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
|
|
xmlResponse = `<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
|
|
<AssumeRoleWithWebIdentityResult>
|
|
<SubjectFromWebIdentityToken>amzn1.account.AF6RHO7KZU5XRVQJGXK6HB56KR2A</SubjectFromWebIdentityToken>
|
|
<Audience>client.5498841531868486423.1548@apps.example.com</Audience>
|
|
<AssumedRoleUser>
|
|
<Arn>arn:aws:sts::123456789012:assumed-role/FederatedWebIdentityRole/%[1]s</Arn>
|
|
<AssumedRoleId>AROACLKWSDQRAOEXAMPLE:%[1]s</AssumedRoleId>
|
|
</AssumedRoleUser>
|
|
<Credentials>
|
|
<SessionToken>AQoDYXdzEE0a8ANXXXXXXXXNO1ewxE5TijQyp+IEXAMPLE</SessionToken>
|
|
<SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
|
|
<Expiration>%s</Expiration>
|
|
<AccessKeyId>%s</AccessKeyId>
|
|
</Credentials>
|
|
<Provider>www.amazon.com</Provider>
|
|
</AssumeRoleWithWebIdentityResult>
|
|
<ResponseMetadata>
|
|
<RequestId>ad4156e9-bce1-11e2-82e6-6b6efEXAMPLE</RequestId>
|
|
</ResponseMetadata>
|
|
</AssumeRoleWithWebIdentityResponse>`
|
|
} else {
|
|
// Taken from STS docs: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
|
xmlResponse = `<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
|
|
<AssumeRoleResult>
|
|
<SourceIdentity>DevUser123</SourceIdentity>
|
|
<AssumedRoleUser>
|
|
<Arn>arn:aws:sts::123456789012:assumed-role/demo/John</Arn>
|
|
<AssumedRoleId>ARO123EXAMPLE123:%[1]s</AssumedRoleId>
|
|
</AssumedRoleUser>
|
|
<Credentials>
|
|
<SessionToken>
|
|
AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
|
|
LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
|
|
QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
|
|
9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
|
|
+scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
|
|
</SessionToken>
|
|
<SecretAccessKey>
|
|
wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY
|
|
</SecretAccessKey>
|
|
<Expiration>%s</Expiration>
|
|
<AccessKeyId>%s</AccessKeyId>
|
|
</Credentials>
|
|
<PackedPolicySize>8</PackedPolicySize>
|
|
</AssumeRoleResult>
|
|
<ResponseMetadata>
|
|
<RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
|
|
</ResponseMetadata>
|
|
</AssumeRoleResponse>`
|
|
}
|
|
|
|
_, _ = w.Write([]byte(fmt.Sprintf(xmlResponse, sessionName, time.Now().Add(time.Hour).Format(time.RFC3339), t.accessKey)))
|
|
}
|
|
|
|
func (t *stsTestServer) start() {
|
|
t.server = httptest.NewServer(http.HandlerFunc(t.handle))
|
|
}
|
|
|
|
func (t *stsTestServer) stop() {
|
|
t.server.Close()
|
|
}
|
|
|
|
func TestECRAuthPluginFailsWithoutAWSAuthPlugins(t *testing.T) {
|
|
ap := newECRAuthPlugin(&awsSigningAuthPlugin{
|
|
logger: logging.NewNoOpLogger(),
|
|
})
|
|
|
|
req := httptest.NewRequest("", "http://somewhere.com", nil)
|
|
|
|
err := ap.Prepare(req)
|
|
if err == nil {
|
|
t.Error("ecrAuthPlugin.Prepare(): expected and error")
|
|
}
|
|
}
|
|
|
|
func TestECRAuthPluginRequestsAuthorizationToken(t *testing.T) {
|
|
// Environment credentials to sign the ecr get authorization token request
|
|
t.Setenv(accessKeyEnvVar, "blablabla")
|
|
t.Setenv(secretKeyEnvVar, "tatata")
|
|
t.Setenv(awsRegionEnvVar, "us-east-1")
|
|
t.Setenv(sessionTokenEnvVar, "lalala")
|
|
|
|
awsAuthPlugin := awsSigningAuthPlugin{
|
|
logger: logging.NewNoOpLogger(),
|
|
AWSEnvironmentCredentials: &awsEnvironmentCredentialService{},
|
|
}
|
|
|
|
ap := newECRAuthPlugin(&awsAuthPlugin)
|
|
ap.ecr = &ecrStub{token: aws.ECRAuthorizationToken{
|
|
AuthorizationToken: "secret",
|
|
}}
|
|
|
|
req := httptest.NewRequest("", "http://somewhere.com", nil)
|
|
|
|
if err := ap.Prepare(req); err != nil {
|
|
t.Errorf("ecrAuthPlugin.Prepare() = %q", err)
|
|
}
|
|
|
|
got := req.Header.Get("Authorization")
|
|
want := "Basic secret"
|
|
if got != want {
|
|
t.Errorf("req.Header.Get(\"Authorization\") = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestECRAuthPluginRequestsRedirection(t *testing.T) {
|
|
// Environment credentials to sign the ecr get authorization token request
|
|
t.Setenv(accessKeyEnvVar, "blablabla")
|
|
t.Setenv(secretKeyEnvVar, "tatata")
|
|
t.Setenv(awsRegionEnvVar, "us-east-1")
|
|
t.Setenv(sessionTokenEnvVar, "lalala")
|
|
|
|
ap := awsSigningAuthPlugin{
|
|
logger: logging.NewNoOpLogger(),
|
|
AWSEnvironmentCredentials: &awsEnvironmentCredentialService{},
|
|
host: "somewhere.com",
|
|
AWSService: "ecr",
|
|
}
|
|
|
|
apECR := newECRAuthPlugin(&ap)
|
|
apECR.ecr = &ecrStub{token: aws.ECRAuthorizationToken{
|
|
AuthorizationToken: "secret",
|
|
}}
|
|
|
|
ap.ecrAuthPlugin = apECR
|
|
|
|
// Request to the host specified in the plugin configuration
|
|
req := httptest.NewRequest("", "http://somewhere.com", nil)
|
|
|
|
if err := ap.Prepare(req); err != nil {
|
|
t.Errorf("ecrAuthPlugin.Prepare() = %q", err)
|
|
}
|
|
|
|
got := req.Header.Get("Authorization")
|
|
want := "Basic secret"
|
|
if got != want {
|
|
t.Errorf("req.Header.Get(\"Authorization\") = %q, want %q", got, want)
|
|
}
|
|
|
|
// Redirection to another host
|
|
req = httptest.NewRequest("", "http://somewhere-else.com", nil)
|
|
|
|
if err := ap.Prepare(req); err != nil {
|
|
t.Errorf("ecrAuthPlugin.Prepare() = %q", err)
|
|
}
|
|
|
|
got = req.Header.Get("Authorization")
|
|
want = ""
|
|
if got != want {
|
|
t.Errorf("req.Header.Get(\"Authorization\") = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
type ecrStub struct {
|
|
token aws.ECRAuthorizationToken
|
|
}
|
|
|
|
func (es *ecrStub) GetAuthorizationToken(context.Context, aws.Credentials, string) (aws.ECRAuthorizationToken, error) {
|
|
return es.token, nil
|
|
}
|
|
|
|
func TestECRAuthPluginReusesCachedToken(t *testing.T) {
|
|
logger := logging.NewNoOpLogger()
|
|
ap := ecrAuthPlugin{
|
|
token: aws.ECRAuthorizationToken{
|
|
AuthorizationToken: "secret",
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
},
|
|
awsAuthPlugin: &awsSigningAuthPlugin{
|
|
logger: logger,
|
|
},
|
|
logger: logger,
|
|
}
|
|
|
|
req := httptest.NewRequest("", "http://somewhere.com", nil)
|
|
|
|
if err := ap.Prepare(req); err != nil {
|
|
t.Errorf("ecrAuthPlugin.Prepare() = %q", err)
|
|
}
|
|
|
|
got := req.Header.Get("Authorization")
|
|
want := "Basic secret"
|
|
if got != want {
|
|
t.Errorf("req.Header.Get(\"Authorization\") = %q, want %q", got, want)
|
|
}
|
|
}
|