cyberark/secretless-broker

View on GitHub
internal/plugin/connectors/http/aws/connector_test.go

Summary

Maintainability
A
0 mins
Test Coverage
package aws

import (
    "bytes"
    "context"
    "io/ioutil"
    "net/http"
    "net/http/httputil"
    "testing"

    "github.com/stretchr/testify/assert"

    "github.com/cyberark/secretless-broker/internal/log"
)

const authzHeader = "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;range;x-amz-date, Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024"

type connectTestCase struct {
    description     string
    url             string
    headers         map[string][]string
    credentialsByID map[string][]byte
    assert          func(t *testing.T, beforeR *http.Request, afterR *http.Request, err error)
}

const connectTestCaseBodyContents = "xyz"
const connectTestCaseBodySHA256 = "3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282"

func (c connectTestCase) Run(t *testing.T) {
    t.Run(c.description, func(t *testing.T) {
        // Create original request before connect is called

        // Use a request body with standard contents for ease of calculating
        // x-amz-content-sha256
        var buf bytes.Buffer
        buf.Write([]byte(connectTestCaseBodyContents))

        beforeR, _ := http.NewRequest("PUT", c.url, &buf)
        beforeR.Header.Set(
            "x-amz-content-sha256",
            "this-will-be-generated-at-signing",
        )

        // Unsigned headers
        beforeR.Header.Set("Unsigned-Header-1", "Unsigned-Header-1-Value")
        beforeR.Header.Set("Unsigned-Header-2", "Unsigned-Header-2-Value")

        for key, values := range c.headers {
            for _, value := range values {
                beforeR.Header.Add(key, value)
            }
        }

        // Create a clone of the original request. We need the original for comparisons
        // during assertion.
        afterR := beforeR.Clone(context.Background())
        // This is needed because the cloning mechanism for the body isn't deep.
        // See https://github.com/golang/go/issues/36095
        afterR.Body, _ = beforeR.GetBody()

        // Call Connect method using the clone of the original request. Some of our assertions
        // will be based on the comparison of the original and the clone, since Connect will
        // potentially mutate the request passed to it.
        connector := Connector{logger: log.NewWithOptions(ioutil.Discard, "", false)}
        err := connector.Connect(afterR, c.credentialsByID)

        // Make assertions
        c.assert(t, beforeR, afterR, err)
    })
}

var testCases = []connectTestCase{
    {
        description:     "no signing",
        url:             "http://meow.moo",
        headers:         nil,
        credentialsByID: nil,
        assert: func(t *testing.T, beforeR *http.Request, afterR *http.Request, err error) {
            assert.NoError(t, err)

            beforeRDump, err := httputil.DumpRequest(beforeR, true)
            assert.NoError(t, err)
            afterRDump, err := httputil.DumpRequest(afterR, true)
            assert.NoError(t, err)

            // The request should remain the same before and after because there is no
            // initial-signing to override.
            assert.Equal(
                t,
                string(beforeRDump),
                string(afterRDump),
            )
        },
    },
    {
        description: "signing without endpoint discovery",
        url:         "http://meow.moo",
        headers: map[string][]string{
            "Authorization": {authzHeader},
            "X-Amz-Date":    {"20210102T150405Z"},
        },
        credentialsByID: map[string][]byte{
            "accessKeyId":     []byte("accessKeyIdValue"),
            "secretAccessKey": []byte("secretAccessKeyValue"),
        },
        assert: func(t *testing.T, beforeR *http.Request, afterR *http.Request, err error) {
            assert.NoError(t, err)

            // The request URL should remain the same because endpoint discovery is not
            // being used
            assert.Equal(
                t,
                beforeR.URL,
                afterR.URL,
            )
            // The Authorization should be modified and should use the injected credentials
            assert.NotEqual(
                t,
                beforeR.Header.Get("Authorization"),
                afterR.Header.Get("Authorization"),
            )
            assert.Equal(
                t,
                // The expected Authorization header value has been manually calculated
                "AWS4-HMAC-SHA256 Credential=accessKeyIdValue/20210102/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=44890edc510facaf7f55bec0cb8eb04a1444690d265858fdab15123b489d5d7b",
                afterR.Header.Get("Authorization"),
            )

            assertOnHeadersAfterSigning(t, afterR)
        },
    },
    {
        description: "signing with endpoint discovery",
        url:         "http://secretless.empty",
        headers: map[string][]string{
            "Authorization": {authzHeader},
            "X-Amz-Date":    {"20210102T150405Z"},
        },
        credentialsByID: map[string][]byte{
            "accessKeyId":     []byte("accessKeyIdValue"),
            "secretAccessKey": []byte("secretAccessKeyValue"),
        },
        assert: func(t *testing.T, beforeR *http.Request, afterR *http.Request, err error) {
            assert.NoError(t, err)

            // The request URL is changed to one determined by endpoint discovery
            assert.Equal(
                t,
                "https://s3.amazonaws.com",
                afterR.URL.String(),
            )
            // The Authorization should be modified and should use the injected credentials
            assert.Equal(
                t,
                // The expected Authorization header value has been manually calculated
                "AWS4-HMAC-SHA256 Credential=accessKeyIdValue/20210102/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=d75dd6809bc23c1045d7ea19f80bf18fc976b0943eed8aac5aa809810a6b2368",
                afterR.Header.Get("Authorization"),
            )

            assertOnHeadersAfterSigning(t, afterR)
        },
    },
    {
        description: "missing credentials for signing",
        url:         "http://meow.moo",
        headers: map[string][]string{
            "Authorization": {authzHeader},
            "X-Amz-Date":    {"20060102T150405Z"},
        },
        assert: func(t *testing.T, beforeR *http.Request, afterR *http.Request, err error) {
            assert.Error(t, err)
            assert.Contains(t, err.Error(), "AWS connection parameter")
            assert.Contains(t, err.Error(), "is not available")
        },
    },
}

func assertOnHeadersAfterSigning(t *testing.T, afterR *http.Request) {
    // X-Amz-Content-Sha256 is always recalculated
    assert.Equal(
        t,
        connectTestCaseBodySHA256,
        afterR.Header.Get("X-Amz-Content-Sha256"),
    )

    // Unsigned headers remain unchanged
    assert.Equal(
        t,
        "Unsigned-Header-1-Value",
        afterR.Header.Get("Unsigned-Header-1"),
    )
    assert.Equal(
        t,
        "Unsigned-Header-2-Value",
        afterR.Header.Get("Unsigned-Header-2"),
    )
}

func TestConnector_Connect(t *testing.T) {
    for _, testCase := range testCases {
        testCase.Run(t)
    }
}