From f9d03227258c8b7990e2cc3ff47df71c964764be Mon Sep 17 00:00:00 2001 From: Kumbirai Tanekha Date: Mon, 4 Apr 2022 13:57:49 +0000 Subject: [PATCH] Robust testing of AWS using feature-rich mock server --- .gitleaks.toml | 8 +- go.mod | 8 +- go.sum | 9 + internal/plugin/connectors/http/aws/aws.go | 111 ++++++----- .../plugin/connectors/http/aws/connector.go | 10 +- internal/providers/awssecrets/provider.go | 12 +- .../awssecrets/aws_secrets_provider_test.go | 184 ++++++++++++++++++ 7 files changed, 286 insertions(+), 56 deletions(-) diff --git a/.gitleaks.toml b/.gitleaks.toml index a03399d9c..b951efe3a 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -18,6 +18,9 @@ tags = ["key", "AWS"] description = "sample AWS key in AWS HTTP connector comments" regex = '''AKIAJC5FABNOFVBKRWHA''' [[rules.whitelist]] +description = "sample AWS key in AWS HTTP connector comments" +regex = '''AKIAIOSFODNN7EXAMPLE''' +[[rules.whitelist]] description = "since-removed sample AWS key" regex = '''AKIAJADDJE4Q4JVX3HAA''' @@ -214,6 +217,7 @@ files = [ "test/ssh/id_(.*)", # since-removed ssh test certs "test/util/ssl/(.*)", # test ssl certs "internal/plugin/connectors/tcp/mssql/connection_details_test.go", # fake cert string + "internal/plugin/connectors/http/aws/aws.go", # example AWS authorization header string ] # As of v4, gitleaks can whitelist paths to accommodate no longer using # paths in the `files` whitelist. @@ -232,10 +236,12 @@ paths = [ "test/connector/tcp/mssql/certs", "test/ssh", "test/util/ssl", - "internal/plugin/connectors/tcp/mssql" + "internal/plugin/connectors/tcp/mssql", + "internal/plugin/connectors/http/aws" ] regexes = [ "AKIAJC5FABNOFVBKRWHA", # sample AWS key in AWS HTTP connector comments + "AKIAIOSFODNN7EXAMPLE", # sample AWS key in AWS HTTP connector comments "AKIAJADDJE4Q4JVX3HAA", # since-removed sample AWS key "SuperSecure", # dummy password used in conjur integration test docker-compose ] diff --git a/go.mod b/go.mod index 64275f45a..508409c6c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/cyberark/secretless-broker require ( + github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect + github.com/Microsoft/go-winio v0.4.16 // indirect github.com/aws/aws-sdk-go v1.15.79 github.com/cenkalti/backoff v2.2.1+incompatible github.com/codegangsta/cli v1.20.0 @@ -17,6 +19,7 @@ require ( github.com/go-ozzo/ozzo-validation v3.6.0+incompatible github.com/google/btree v1.0.0 // indirect github.com/googleapis/gnostic v0.3.1 // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/vault/api v1.0.2 github.com/heptiolabs/healthcheck v0.0.0-20180807145615-6ff867650f40 @@ -24,8 +27,10 @@ require ( github.com/joho/godotenv v1.2.0 github.com/json-iterator/go v1.1.8 // indirect github.com/lib/pq v0.0.0-20180123210206-19c8e9ad0095 + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect - github.com/pkg/errors v0.8.1 + github.com/pkg/errors v0.9.1 github.com/pkg/profile v1.2.1 github.com/prometheus/client_golang v1.2.1 // indirect github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 @@ -33,6 +38,7 @@ require ( github.com/stretchr/testify v1.3.0 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 gopkg.in/yaml.v2 v2.2.2 + gotest.tools v2.2.0+incompatible // indirect k8s.io/api v0.0.0-20180712090710-2d6f90ab1293 k8s.io/apiextensions-apiserver v0.0.0-20180808065829-408db4a50408 k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d diff --git a/go.sum b/go.sum index b6964646a..285e87b2a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -96,6 +98,7 @@ github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1a github.com/gopherjs/gopherjs v0.0.0-20180202210947-296de816d4fe/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -179,12 +182,14 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v0.0.0-20180718012357-94122c33edd3/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -195,6 +200,7 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -225,6 +231,7 @@ github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIH github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20170925172151-0b37b35ec743/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -281,6 +288,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -322,6 +330,7 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 h1:0efs3hwEZhFKsCoP8l6dDB1AZWMgnEl3yWXWRZTOaEA= gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= k8s.io/api v0.0.0-20180712090710-2d6f90ab1293 h1:hROmpFC7JMobXFXMmD7ZKZLhDKvr1IKfFJoYS/45G/8= k8s.io/api v0.0.0-20180712090710-2d6f90ab1293/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= k8s.io/apiextensions-apiserver v0.0.0-20180808065829-408db4a50408 h1:GcrrWo5PlDjJ6cSFoxKlIy3xH+IvXa/uYs90NxdbEV4= diff --git a/internal/plugin/connectors/http/aws/aws.go b/internal/plugin/connectors/http/aws/aws.go index 43fc8a59d..560f5103f 100644 --- a/internal/plugin/connectors/http/aws/aws.go +++ b/internal/plugin/connectors/http/aws/aws.go @@ -6,7 +6,6 @@ import ( "io/ioutil" gohttp "net/http" "net/url" - "regexp" "strings" "time" @@ -20,16 +19,7 @@ import ( // From https://github.com/aws/aws-sdk-go/blob/master/aws/signer/v4/v4.go#L77 const timeFormat = "20060102T150405Z" -// reForCredentialComponent matches headers strings like: -// -// Credential=AKIAJC5FABNOFVBKRWHA/20171103/us-east-1/ec2/aws4_request -// -// See https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html -var reForCredentialComponent = regexp.MustCompile( - `^Credential=\w+\/\d+\/([\w-_]+)\/(\w+)\/aws4_request$`, -) - -// newAmzDate parses a date string using the AWS signer time format +// newAmzDate parses a Date string using the AWS signer time format func newAmzDate(amzDateStr string) (time.Time, error) { if amzDateStr == "" { return time.Time{}, fmt.Errorf("missing required header: %s", "X-Amz-Date") @@ -39,33 +29,64 @@ func newAmzDate(amzDateStr string) (time.Time, error) { } // requestMetadataFromAuthz parses an authorization header string and create a -// requestMetadata instance populated with the associated region and service +// RequestMetadata instance populated with the associated Region and service // name -func requestMetadataFromAuthz(authorizationStr string) (*requestMetadata, error) { - // extract credentials section of authorization header - credentialsComponent, err := extractCredentialsComponent(authorizationStr) - if err != nil { - return nil, err - } - // validate credential component of authorization header, then extract region - // and service name - matches := reForCredentialComponent.FindStringSubmatch(credentialsComponent) - if len(matches) != 3 { - return nil, fmt.Errorf("malformed credential component of Authorization header") +func requestMetadataFromAuthz(authorizationStr string) (*RequestMetadata, error) { + var signedHeaders []string + //var signature string + //var secretKeyId string + //var date string + var region string + var service string + + // Authorization Header format: + // + // AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;range;x-amz-date, Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024 + // + // See https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html + for _, p := range strings.Split(authorizationStr, ", ") { + if strings.HasPrefix(p, "SignedHeaders=") { + signedHeaders = strings.Split(p[len("SignedHeaders="):], ";") + continue + } + + //if strings.HasPrefix(p, "Signature=") { + // signature = p[len("Signature="):] + // continue + //} + + if strings.HasPrefix(p, "AWS4-HMAC-SHA256 Credential=") { + credentialParts := strings.SplitN( + p[len("AWS4-HMAC-SHA256 Credential="):], + "/", + 5, + ) + if len(credentialParts) != 5 { + return nil, fmt.Errorf("malformed credential component of Authorization header") + } + // secretKeyId = credentialParts[0] + // date = credentialParts[1] + region = credentialParts[2] + service = credentialParts[3] + continue + } + } - return &requestMetadata{ - region: matches[1], - serviceName: matches[2], + return &RequestMetadata{ + Region: region, + ServiceName: service, + SignedHeaders: signedHeaders, }, nil } -// requestMetadata captures the metadata of a signed AWS request: date, region -// and service name -type requestMetadata struct { - date time.Time - region string - serviceName string +// RequestMetadata captures the metadata of a signed AWS request: Date, Region, +// Service name and Signed headers +type RequestMetadata struct { + Date time.Time + Region string + ServiceName string + SignedHeaders []string } // extractCredentialsComponent extracts the credentials component from an @@ -87,9 +108,9 @@ func extractCredentialsComponent(authorizationStr string) (string, error) { return strings.TrimPrefix(tokens[0], "AWS4-HMAC-SHA256 "), nil } -// newRequestMetadata parses the request headers to extract the metadata +// NewRequestMetadata parses the request headers to extract the metadata // necessary to sign the request -func newRequestMetadata(r *gohttp.Request) (*requestMetadata, error) { +func NewRequestMetadata(r *gohttp.Request) (*RequestMetadata, error) { authorizationStr := r.Header.Get("Authorization") amzDateStr := r.Header.Get("X-Amz-Date") @@ -99,22 +120,22 @@ func newRequestMetadata(r *gohttp.Request) (*requestMetadata, error) { return nil, nil } - // parse date string + // parse Date string // date, err := newAmzDate(amzDateStr) if err != nil { return nil, err } - // create request metadata by extracting service name and region from + // create request metadata by extracting service name and Region from // Authorization header reqMeta, err := requestMetadataFromAuthz(authorizationStr) if err != nil { return nil, err } - // populate request metadata with date - reqMeta.date = date + // populate request metadata with Date + reqMeta.Date = date return reqMeta, nil } @@ -148,7 +169,7 @@ func newAmzStaticCredentials( // signRequest uses metadata and credentials to sign a request func signRequest( r *gohttp.Request, - reqMeta *requestMetadata, + reqMeta *RequestMetadata, credentialsByID connector.CredentialValuesByID, ) error { // Create AWS static credentials using provided credentials @@ -165,9 +186,9 @@ func signRequest( _, err = signer.NewSigner(amzCreds).Sign( r, bytes.NewReader(bodyBytes), - reqMeta.serviceName, - reqMeta.region, - reqMeta.date, + reqMeta.ServiceName, + reqMeta.Region, + reqMeta.Date, ) if err != nil { return err @@ -193,7 +214,7 @@ func signRequest( // // Note: There is a plan to add a configuration option to instruct Secretless to // upgrade the connect between Secretless and the target endpoint to TLS. -func setAmzEndpoint(req *gohttp.Request, reqMeta *requestMetadata) error { +func setAmzEndpoint(req *gohttp.Request, reqMeta *RequestMetadata) error { shouldSetEndpoint := req.URL.Scheme == "http" && req.URL.Host == "secretless.empty" @@ -202,8 +223,8 @@ func setAmzEndpoint(req *gohttp.Request, reqMeta *requestMetadata) error { } endpoint, err := endpoints.DefaultResolver().EndpointFor( - reqMeta.serviceName, - reqMeta.region, + reqMeta.ServiceName, + reqMeta.Region, ) if err != nil { return err diff --git a/internal/plugin/connectors/http/aws/connector.go b/internal/plugin/connectors/http/aws/connector.go index 40c756b5c..597f9c200 100644 --- a/internal/plugin/connectors/http/aws/connector.go +++ b/internal/plugin/connectors/http/aws/connector.go @@ -25,8 +25,8 @@ func (c *Connector) Connect( ) error { var err error - // Extract metadata of a signed AWS request: date, region and service name - reqMeta, err := newRequestMetadata(req) + // Extract metadata of a signed AWS request: Date, Region and service name + reqMeta, err := NewRequestMetadata(req) if err != nil { return err } @@ -47,9 +47,9 @@ func (c *Connector) Connect( // Use metadata and credentials to sign request c.logger.Debugf( - "Signing for service=%s region=%s", - reqMeta.serviceName, - reqMeta.region, + "Signing for service=%s Region=%s", + reqMeta.ServiceName, + reqMeta.Region, ) return signRequest(req, reqMeta, credentialsByID) } diff --git a/internal/providers/awssecrets/provider.go b/internal/providers/awssecrets/provider.go index 0dbe9a288..61057a8c5 100644 --- a/internal/providers/awssecrets/provider.go +++ b/internal/providers/awssecrets/provider.go @@ -19,26 +19,30 @@ type Provider struct { // ProviderFactory constructs a Provider. The API client is configured from // in-cluster environment variables and files. func ProviderFactory(options plugin_v1.ProviderOptions) (plugin_v1.Provider, error) { + return NewProvider(options, aws.Config{}) +} +// NewProvider creates a provider with an optional custom AWS config. +func NewProvider(options plugin_v1.ProviderOptions, config aws.Config) (*Provider, error) { // All clients require a Session. The Session provides the client with // shared configuration such as region, endpoint, and credentials. A // Session should be shared where possible to take advantage of // configuration and credential caching. sess, err := session.NewSessionWithOptions(session.Options{ + Config: config, SharedConfigState: session.SharedConfigEnable, }) if err != nil { return nil, fmt.Errorf("ERROR: Could not create AWS Secrets provider: %s", err) } + // Create a new instance of the service's client with a Session. client := secretsmanager.New(sess) - provider := &Provider{ + return &Provider{ Name: options.Name, Client: client, - } - - return provider, nil + }, nil } // GetName returns the name of the provider diff --git a/test/providers/awssecrets/aws_secrets_provider_test.go b/test/providers/awssecrets/aws_secrets_provider_test.go index 5308ff6d1..979ecbc8d 100644 --- a/test/providers/awssecrets/aws_secrets_provider_test.go +++ b/test/providers/awssecrets/aws_secrets_provider_test.go @@ -1,15 +1,158 @@ package main import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" "testing" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + v4 "github.com/aws/aws-sdk-go/aws/signer/v4" + "github.com/aws/aws-sdk-go/service/secretsmanager" _ "github.com/joho/godotenv/autoload" . "github.com/smartystreets/goconvey/convey" + awsinternal "github.com/cyberark/secretless-broker/internal/plugin/connectors/http/aws" plugin_v1 "github.com/cyberark/secretless-broker/internal/plugin/v1" "github.com/cyberark/secretless-broker/internal/providers" + "github.com/cyberark/secretless-broker/internal/providers/awssecrets" ) +// Works uses a mock server to capture the request from the provider and compare +// the authorization header with a manually generated one using the same credentials. +// Assertions are also made against the request payload and response payload. +func Works( + t *testing.T, + resignCreds *credentials.Credentials, + setupProviderAuth func() func(), +) { + var originalAuthHeaders []string + var reSignedAuthHeaders []string + var secretInputs []secretsmanager.GetSecretValueInput + + // secretStringNotBinary determines the type of secret output payload returned by the + // mock server, String or Binary. Default is String. + secretStringNotBinary := true + + // TODO: This should really be standalone test fixture, that maintains a map of + // request-secret-id to the latest request details and response details, or some + // other thing that is unique to a request. This will allow for arbitrarily complex + // and robust testing using this mock server. + // NOTE: This server can and has been run standalone and consumed by the AWS CLI to + // confirm that it behaves as expected. Automating such a test wouldn't hurt. + + // Mock server for fetching secrets. Secret value in the response is always the value + // "[request-secret-id]-value" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqBytes, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + // Parse and capture the request secretInput + var secretInput secretsmanager.GetSecretValueInput + err = json.Unmarshal(reqBytes, &secretInput) + if err != nil { + t.Fatal(err) + } + secretInputs = append(secretInputs, secretInput) + + // Parse request for AWS signed request metadata + reqMeta, err := awsinternal.NewRequestMetadata(r) + if err != nil { + t.Fatal(err) + } + + // Capture original signed request header + originalAuthHeaders = append(originalAuthHeaders, r.Header.Get("Authorization")) + + // Remove all non-signed headers + signedHeaders := map[string]bool{} + for _, name := range reqMeta.SignedHeaders { + signedHeaders[strings.ToLower(name)] = true + } + for name := range r.Header { + if ok := signedHeaders[strings.ToLower(name)]; ok { + continue + } + + r.Header.Del(name) + } + + // Re-sign request + _, err = v4.NewSigner(resignCreds).Sign( + r, + bytes.NewReader(reqBytes), + reqMeta.ServiceName, + reqMeta.Region, + reqMeta.Date, + ) + if err != nil { + t.Fatal(err) + } + + // Capture resigned request header + reSignedAuthHeaders = append(reSignedAuthHeaders, r.Header.Get("Authorization")) + + // Craft response. The first response is secret string, then all subsequent + // responses are secret binary + var secretOutput secretsmanager.GetSecretValueOutput + secretValue := aws.StringValue(secretInput.SecretId) + "-value" + if secretStringNotBinary { + secretOutput.SetSecretString(secretValue) + } else { + secretOutput.SetSecretBinary([]byte(secretValue)) + } + + // Write response + responseBytes, err := json.Marshal(secretOutput) + if err != nil { + t.Fatal(err) + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write(responseBytes) + })) + defer server.Close() + + // Setup authentication for provider + cleanup := setupProviderAuth() + defer cleanup() + + // Create provider (with custom endpoint) + p, err := awssecrets.NewProvider(plugin_v1.ProviderOptions{Name: "aws"}, aws.Config{ + Endpoint: aws.String(server.URL), + }) + So(err, ShouldBeNil) + + // Make 2 attempts to get values from provider + secretStringNotBinary = true + secretValue0, err := p.GetValue("meow-id-0") + So(err, ShouldBeNil) + + secretStringNotBinary = false + secretValue1, err := p.GetValue("meow-id-1") + So(err, ShouldBeNil) + + // Ensure 2 attempts are recorded + So(reSignedAuthHeaders, ShouldHaveLength, 2) + + // Assert that the auth header sent by the Provider matches the manually + // signed one + So(reSignedAuthHeaders[0], ShouldEqual, originalAuthHeaders[0]) + So(reSignedAuthHeaders[1], ShouldEqual, originalAuthHeaders[1]) + // Assert on the requested secret id + So(aws.StringValue(secretInputs[0].SecretId), ShouldEqual, "meow-id-0") + So(aws.StringValue(secretInputs[1].SecretId), ShouldEqual, "meow-id-1") + // Assert on the response secret values + So(string(secretValue0), ShouldEqual, "meow-id-0-value") + So(string(secretValue1), ShouldEqual, "meow-id-1-value") +} + func TestAWSSecrets_Provider(t *testing.T) { var err error var provider plugin_v1.Provider @@ -28,4 +171,45 @@ func TestAWSSecrets_Provider(t *testing.T) { Convey("Has the expected provider name", t, func() { So(provider.GetName(), ShouldEqual, "aws") }) + + + Convey("Fetches credentials using AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY", t, func() { + Works( + t, + credentials.NewStaticCredentials( + "xyz", + "abc", + "", + ), + func() func() { + _ = os.Setenv("AWS_ACCESS_KEY_ID", "xyz") + _ = os.Setenv("AWS_SECRET_ACCESS_KEY", "abc") + return func() { + _ = os.Unsetenv("AWS_ACCESS_KEY_ID") + _ = os.Unsetenv("AWS_SECRET_ACCESS_KEY") + } + }, + ) + }) + + Convey("Fetches credentials using AWS_SESSION_TOKEN", t, func() { + Works( + t, + credentials.NewStaticCredentials( + "abc", + "meow", + "moo", + ), + func() func() { + _ = os.Setenv("AWS_ACCESS_KEY_ID", "abc") + _ = os.Setenv("AWS_SECRET_ACCESS_KEY", "meow") + _ = os.Setenv("AWS_SESSION_TOKEN", "moo") + return func() { + _ = os.Unsetenv("AWS_ACCESS_KEY_ID") + _ = os.Unsetenv("AWS_SECRET_ACCESS_KEY") + _ = os.Unsetenv("AWS_SESSION_TOKEN") + } + }, + ) + }) }