diff --git a/go.mod b/go.mod index df2ad6f04..0b81c7be8 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/opensearch-project/opensearch-go/v2 -go 1.11 +go 1.15 require ( - github.com/aws/aws-sdk-go v1.42.27 - github.com/stretchr/testify v1.7.0 - golang.org/x/text v0.3.7 // indirect + github.com/aws/aws-sdk-go v1.44.36 + github.com/aws/aws-sdk-go-v2 v1.16.5 + github.com/aws/aws-sdk-go-v2/config v1.15.11 + github.com/stretchr/testify v1.7.2 ) diff --git a/go.sum b/go.sum index 186d99539..157998766 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,31 @@ -github.com/aws/aws-sdk-go v1.42.27 h1:kxsBXQg3ee6LLbqjp5/oUeDgG7TENFrWYDmEVnd7spU= -github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= +github.com/aws/aws-sdk-go v1.44.36 h1:zeHQau9fGEIE0CsBnQFCqqYWc98m2bk7BQKtkdEqzF4= +github.com/aws/aws-sdk-go v1.44.36/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go-v2 v1.16.5 h1:Ah9h1TZD9E2S1LzHpViBO3Jz9FPL5+rmflmb8hXirtI= +github.com/aws/aws-sdk-go-v2 v1.16.5/go.mod h1:Wh7MEsmEApyL5hrWzpDkba4gwAPc5/piwLVLFnCxp48= +github.com/aws/aws-sdk-go-v2/config v1.15.11 h1:qfec8AtiCqVbwMcx51G1yO2PYVfWfhp2lWkDH65V9HA= +github.com/aws/aws-sdk-go-v2/config v1.15.11/go.mod h1:mD5tNFciV7YHNjPpFYqJ6KGpoSfY107oZULvTHIxtbI= +github.com/aws/aws-sdk-go-v2/credentials v1.12.6 h1:No1wZFW4bcM/uF6Tzzj6IbaeQJM+xxqXOYmoObm33ws= +github.com/aws/aws-sdk-go-v2/credentials v1.12.6/go.mod h1:mQgnRmBPF2S/M01W4T4Obp3ZaZB6o1s/R8cOUda9vtI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.6 h1:+NZzDh/RpcQTpo9xMFUgkseIam6PC+YJbdhbQp1NOXI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.6/go.mod h1:ClLMcuQA/wcHPmOIfNzNI4Y1Q0oDbmEkbYhMFOzHDh8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12 h1:Zt7DDk5V7SyQULUUwIKzsROtVzp/kVvcz15uQx/Tkow= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12/go.mod h1:Afj/U8svX6sJ77Q+FPWMzabJ9QjbwP32YlopgKALUpg= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6 h1:eeXdGVtXEe+2Jc49+/vAzna3FAQnUD4AagAw8tzbmfc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6/go.mod h1:FwpAKI+FBPIELJIdmQzlLtRe8LQSOreMcM2wBsPMvvc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.13 h1:L/l0WbIpIadRO7i44jZh1/XeXpNDX0sokFppb4ZnXUI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.13/go.mod h1:hiM/y1XPp3DoEPhoVEYc/CZcS58dP6RKJRDFp99wdX0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.6 h1:0ZxYAZ1cn7Swi/US55VKciCE6RhRHIwCKIWaMLdT6pg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.6/go.mod h1:DxAPjquoEHf3rUHh1b9+47RAaXB8/7cB6jkzCt/GOEI= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.9 h1:Gju1UO3E8ceuoYc/AHcdXLuTZ0WGE1PT2BYDwcYhJg8= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.9/go.mod h1:UqRD9bBt15P0ofRyDZX6CfsIqPpzeHOhZKWzgSuAzpo= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.7 h1:HLzjwQM9975FQWSF3uENDGHT1gFQm/q3QXu2BYIcI08= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.7/go.mod h1:lVxTdiiSHY3jb1aeg+BBFtDzZGSUCv6qaNOyEGCJ1AY= +github.com/aws/smithy-go v1.11.3 h1:DQixirEFM9IaKxX1olZ3ke3nvxRS2xMDteKIDWxozW8= +github.com/aws/smithy-go v1.11.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -10,14 +34,13 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -25,5 +48,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/signer/awsv2/sdkv2signer.go b/signer/awsv2/sdkv2signer.go new file mode 100644 index 000000000..3d44a17a9 --- /dev/null +++ b/signer/awsv2/sdkv2signer.go @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// The OpenSearch Contributors require contributions made to +// this file be licensed under the Apache-2.0 license or a +// compatible open source license. +// +// Modifications Copyright OpenSearch Contributors. See +// GitHub history for details. + +package awsv2 + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "github.com/aws/aws-sdk-go-v2/aws" + awsSignerV4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/opensearch-project/opensearch-go/v2/signer" + "io/ioutil" + "net/http" + "strings" + "time" +) + +const ( + openSearchService = "es" + emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` +) + +type awsSdkV2Signer struct { + service string + signer *awsSignerV4.Signer + awsCfg aws.Config +} + +// NewSigner returns an instance of Signer for AWS OpenSearchService +func NewSigner(cfg aws.Config) (signer.Signer, error) { + return NewSignerWithService(cfg, openSearchService) +} + +// NewSignerWithService returns an instance of Signer for given service +func NewSignerWithService(cfg aws.Config, service string) (signer.Signer, error) { + if len(strings.TrimSpace(service)) < 1 { + return nil, errors.New("service cannot be empty") + } + + return &awsSdkV2Signer{ + service: service, + signer: awsSignerV4.NewSigner(), + awsCfg: cfg, + }, nil +} + +func (s *awsSdkV2Signer) SignRequest(r *http.Request) error { + ctx := context.Background() + t := time.Now() + + creds, err := s.awsCfg.Credentials.Retrieve(ctx) + if err != nil { + return err + } + + if len(s.awsCfg.Region) == 0 { + return fmt.Errorf("aws region cannot be empty") + } + + hash, err := hexEncodedSha256OfRequest(r) + if err != nil { + return err + } + + return s.signer.SignHTTP(ctx, creds, r, hash, s.service, s.awsCfg.Region, t) +} + +func hexEncodedSha256OfRequest(r *http.Request) (string, error) { + if r.Body == nil { + return emptyStringSHA256, nil + } + + hasher := sha256.New() + reqBodyBytes, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", err + } + + if err := r.Body.Close(); err != nil { + return "", err + } + r.Body = ioutil.NopCloser(bytes.NewBuffer(reqBodyBytes)) + + hasher.Write(reqBodyBytes) + digest := hasher.Sum(nil) + + return hex.EncodeToString(digest), nil +} diff --git a/signer/awsv2/sdkv2signer_test.go b/signer/awsv2/sdkv2signer_test.go new file mode 100644 index 000000000..95d1f24d7 --- /dev/null +++ b/signer/awsv2/sdkv2signer_test.go @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// The OpenSearch Contributors require contributions made to +// this file be licensed under the Apache-2.0 license or a +// compatible open source license. +// +// Modifications Copyright OpenSearch Contributors. See +// GitHub history for details. + +package awsv2 + +import ( + "bytes" + "context" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/stretchr/testify/assert" + "net/http" + "os" + "testing" +) + +func getCredentialProvider(accessKey, secretAccessKey, token string) aws.CredentialsProviderFunc { + return func(ctx context.Context) (aws.Credentials, error) { + c := &aws.Credentials{ + AccessKeyID: accessKey, + SecretAccessKey: secretAccessKey, + SessionToken: token, + } + return *c, nil + } +} + +func TestV4SignerAwsSdkV2(t *testing.T) { + t.Run("sign request failed due to no region found", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "https://localhost:9200", nil) + assert.NoError(t, err) + region := os.Getenv("AWS_REGION") + os.Setenv("AWS_REGION", "") + defer func() { + os.Setenv("AWS_REGION", region) + }() + awsCfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithCredentialsProvider( + getCredentialProvider("AKID", "SECRET_KEY", "TOKEN"), + ), + config.WithRegion(""), + ) + assert.NoError(t, err) + + signer, err := NewSigner(awsCfg) + assert.NoError(t, err) + err = signer.SignRequest(req) + + assert.EqualErrorf( + t, err, "aws region cannot be empty", "unexpected error") + }) + t.Run("sign request success", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "https://localhost:9200", nil) + assert.NoError(t, err) + region := os.Getenv("AWS_REGION") + os.Setenv("AWS_REGION", "us-west-2") + defer func() { + os.Setenv("AWS_REGION", region) + }() + + awsCfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithRegion("us-west-2"), + config.WithCredentialsProvider( + getCredentialProvider("AKID", "SECRET_KEY", "TOKEN"), + ), + ) + assert.NoError(t, err) + + signer, err := NewSigner(awsCfg) + assert.NoError(t, err) + + err = signer.SignRequest(req) + assert.NoError(t, err) + + q := req.Header + assert.NotEmpty(t, q.Get("Authorization")) + assert.NotEmpty(t, q.Get("X-Amz-Date")) + }) + t.Run("sign request success with body", func(t *testing.T) { + req, err := http.NewRequest( + http.MethodPost, "https://localhost:9200", + bytes.NewBuffer([]byte(`some data`))) + assert.NoError(t, err) + region := os.Getenv("AWS_REGION") + os.Setenv("AWS_REGION", "us-west-2") + defer func() { + os.Setenv("AWS_REGION", region) + }() + + awsCfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithRegion("us-west-2"), + config.WithCredentialsProvider( + getCredentialProvider("AKID", "SECRET_KEY", "TOKEN"), + ), + ) + assert.NoError(t, err) + + signer, err := NewSigner(awsCfg) + assert.NoError(t, err) + + err = signer.SignRequest(req) + assert.NoError(t, err) + q := req.Header + assert.NotEmpty(t, q.Get("Authorization")) + assert.NotEmpty(t, q.Get("X-Amz-Date")) + }) + + t.Run("sign request success with body for other AWS Services", func(t *testing.T) { + req, err := http.NewRequest( + http.MethodPost, "https://localhost:9200", + bytes.NewBuffer([]byte(`some data`))) + assert.NoError(t, err) + region := os.Getenv("AWS_REGION") + os.Setenv("AWS_REGION", "us-west-2") + defer func() { + os.Setenv("AWS_REGION", region) + }() + + awsCfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithRegion("us-west-2"), + config.WithCredentialsProvider( + getCredentialProvider("AKID", "SECRET_KEY", "TOKEN"), + ), + ) + assert.NoError(t, err) + + signer, err := NewSignerWithService(awsCfg, "ec") + assert.NoError(t, err) + + assert.NoError(t, err) + err = signer.SignRequest(req) + assert.NoError(t, err) + q := req.Header + assert.NotEmpty(t, q.Get("Authorization")) + assert.NotEmpty(t, q.Get("X-Amz-Date")) + }) + + t.Run("sign request failed due to invalid service", func(t *testing.T) { + awsCfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithRegion("us-west-2"), + config.WithCredentialsProvider( + getCredentialProvider("AKID", "SECRET_KEY", "TOKEN"), + ), + ) + assert.NoError(t, err) + + _, err = NewSignerWithService(awsCfg, "") + assert.EqualError(t, err, "service cannot be empty") + }) +}