diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 4b3459bac7..f2e9d59483 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -453,6 +453,8 @@ functions: type: test params: binary: "bash" + env: + AWS_TEST: ecs include_expansions_in_env: [SKIP_ECS_AUTH_TEST] args: [*task-runner, evg-test-aws-ecs] run-aws-auth-test-with-aws-web-identity-credentials: diff --git a/etc/run-mongodb-aws-test.sh b/etc/run-mongodb-aws-test.sh index ceca8b9c01..281bca740c 100755 --- a/etc/run-mongodb-aws-test.sh +++ b/etc/run-mongodb-aws-test.sh @@ -32,3 +32,8 @@ set -x # For Go 1.16+, Go builds requires a go.mod file in the current working directory or a parent # directory. Spawn a new subshell, "cd" to the project directory, then run "go run". (cd ${PROJECT_DIRECTORY} && go test -timeout 30m -v ./internal/test/aws/... | tee -a test.suite) + +# Also run the ext/awsauth integration test, which uses the AWS SDK default credential +# chain to cover scenarios where credentials are not embedded in the URI (EC2, ECS, +# WebIdentity) in addition to the inline-credential scenarios. +(cd ${PROJECT_DIRECTORY}/ext/awsauth/test && go test -timeout 30m -v ./... | tee -a test.suite) diff --git a/ext/awsauth/awsauth.go b/ext/awsauth/awsauth.go new file mode 100644 index 0000000000..31a1033e8d --- /dev/null +++ b/ext/awsauth/awsauth.go @@ -0,0 +1,58 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package awsauth + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +// CredentialsProvider adapts an AWS SDK v2 CredentialsProvider to the +// options.AWSCredentialsProvider interface used by the MongoDB Go Driver. +// +// CredentialsProvider is expected to implement the options.AWSCredentialsProvider interface: +// +// import "go.mongodb.org/mongo-driver/v2/mongo/options" +// var _ options.AWSCredentialsProvider = (*CredentialsProvider)(nil) +type CredentialsProvider struct { + provider aws.CredentialsProvider +} + +// NewCredentialsProvider returns a CredentialsProvider that wraps AWS +// CredentialsProvider. CredentialsProvider is expected to not be nil. +func NewCredentialsProvider(credentialsProvider aws.CredentialsProvider) *CredentialsProvider { + return &CredentialsProvider{ + provider: credentialsProvider, + } +} + +// AWSCredentials is a struct that contains AWS credentials information. +type AWSCredentials = struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + Source string + CanExpire bool + Expires time.Time + AccountID string +} + +// Retrieve returns the credentials. +func (p *CredentialsProvider) Retrieve(ctx context.Context) (AWSCredentials, error) { + creds, err := p.provider.Retrieve(ctx) + return AWSCredentials{ + AccessKeyID: creds.AccessKeyID, + SecretAccessKey: creds.SecretAccessKey, + SessionToken: creds.SessionToken, + Source: creds.Source, + CanExpire: creds.CanExpire, + Expires: creds.Expires, + AccountID: creds.AccountID, + }, err +} diff --git a/ext/awsauth/doc.go b/ext/awsauth/doc.go new file mode 100644 index 0000000000..1aa583a5b3 --- /dev/null +++ b/ext/awsauth/doc.go @@ -0,0 +1,22 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +// This package allows credential providers and signers in AWS SDK v2 to be +// supplied to the MongoDB Go Driver for use with the MONGODB-AWS authentication +// mechanism. +// +// This package is a separate module to avoid adding the AWS SDK as a dependency +// of the main driver. Users who don't need AWS authentication won't need to +// import the AWS SDK. +// +// NewCredentialsProvider() adapts an AWS CredentialsProvider to be used in: +// +// ClientOptions +// ClientEncryptionOptions +// AutoEncryptionOptions +// +// ClientOptions +package awsauth diff --git a/ext/awsauth/examples/example_test.go b/ext/awsauth/examples/example_test.go new file mode 100644 index 0000000000..e83106c675 --- /dev/null +++ b/ext/awsauth/examples/example_test.go @@ -0,0 +1,35 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package awsauth_test + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/config" + "go.mongodb.org/mongo-driver/ext/awsauth" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +func Example_defaultConfig() { + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + panic(err) + } + awsCredentialProvider := awsauth.NewCredentialsProvider(cfg.Credentials) + credential := options.Credential{ + AuthMechanism: "MONGODB-AWS", + AWSCredentialsProvider: awsCredentialProvider, + } + client, err := mongo.Connect( + options.Client().SetAuth(credential), + ) + if err != nil { + panic(err) + } + _ = client +} diff --git a/ext/awsauth/examples/go.mod b/ext/awsauth/examples/go.mod new file mode 100644 index 0000000000..e97c3e0937 --- /dev/null +++ b/ext/awsauth/examples/go.mod @@ -0,0 +1,36 @@ +module go.mongodb.org/mongo-driver/ext/awsauth/examples + +go 1.24 + +require ( + github.com/aws/aws-sdk-go-v2/config v1.32.25 + go.mongodb.org/mongo-driver/ext/awsauth v0.0.0 + go.mongodb.org/mongo-driver/v2 v2.0.0-beta2 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.24 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect + github.com/aws/smithy-go v1.27.1 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/klauspost/compress v1.17.6 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/text v0.22.0 // indirect +) + +replace go.mongodb.org/mongo-driver/ext/awsauth => .. diff --git a/ext/awsauth/examples/go.sum b/ext/awsauth/examples/go.sum new file mode 100644 index 0000000000..c27b99fb71 --- /dev/null +++ b/ext/awsauth/examples/go.sum @@ -0,0 +1,76 @@ +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= +github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= +github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= +github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver/v2 v2.0.0-beta2 h1:PRtbRKwblE8ZfI8qOhofcjn9y8CmKZI7trS5vDMeJX0= +go.mongodb.org/mongo-driver/v2 v2.0.0-beta2/go.mod h1:UGLb3ZgEzaY0cCbJpH9UFt9B6gEXiTPzsnJS38nBeoU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/ext/awsauth/go.mod b/ext/awsauth/go.mod new file mode 100644 index 0000000000..9f8d556b8a --- /dev/null +++ b/ext/awsauth/go.mod @@ -0,0 +1,7 @@ +module go.mongodb.org/mongo-driver/ext/awsauth + +go 1.19 + +require github.com/aws/aws-sdk-go-v2 v1.0.0 + +require github.com/aws/smithy-go v1.0.0 // indirect diff --git a/ext/awsauth/go.sum b/ext/awsauth/go.sum new file mode 100644 index 0000000000..a4ccc4d32f --- /dev/null +++ b/ext/awsauth/go.sum @@ -0,0 +1,15 @@ +github.com/aws/aws-sdk-go-v2 v1.0.0 h1:ncEVPoHArsG+HjoDe/3ex/TG1CbLwMQ4eaWj0UGdyTo= +github.com/aws/aws-sdk-go-v2 v1.0.0/go.mod h1:smfAbmpW+tcRVuNUjo3MOArSZmW72t62rkCzc2i0TWM= +github.com/aws/smithy-go v1.0.0 h1:hkhcRKG9rJ4Fn+RbfXY7Tz7b3ITLDyolBnLLBhwbg/c= +github.com/aws/smithy-go v1.0.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +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= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/ext/awsauth/test/aws_test.go b/ext/awsauth/test/aws_test.go new file mode 100644 index 0000000000..781c1bc006 --- /dev/null +++ b/ext/awsauth/test/aws_test.go @@ -0,0 +1,97 @@ +// Copyright (C) MongoDB, Inc. 2025-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package awsauthtest + +import ( + "context" + "errors" + "os" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/config" + "go.mongodb.org/mongo-driver/ext/awsauth" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +// trackingCredentialsProvider wraps an options.AWSCredentialsProvider and counts calls. +type trackingCredentialsProvider struct { + inner options.AWSCredentialsProvider + called int +} + +func (p *trackingCredentialsProvider) Retrieve(ctx context.Context) (struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + Source string + CanExpire bool + Expires time.Time + AccountID string +}, error, +) { + p.called++ + return p.inner.Retrieve(ctx) +} + +// TestAWSDefaultCustomCredentialProviderAuthenticates is prose test 1: +// "Custom Credential Provider Authenticates" from the MongoDB AWS auth spec. +// https://github.com/mongodb/specifications/blob/master/source/auth/tests/mongodb-aws.md +// +// Uses the AWS SDK default credential chain so all 6 scenarios (Regular, EC2, ECS, +// AssumeRole, WebIdentity, Lambda) are covered in environments where the SDK can +// resolve credentials automatically without needing inline credentials in the URI. +func TestAWSDefaultCustomCredentialProviderAuthenticates(t *testing.T) { + if test := os.Getenv("AWS_TEST"); test == "assume-role" || test == "regular" { + t.Skipf("Skipping test for %s", test) + } + + rawURI := os.Getenv("MONGODB_URI") + if rawURI == "" { + t.Skip("MONGODB_URI not set") + } + + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + t.Fatalf("failed to load AWS config: %v", err) + } + + tracking := &trackingCredentialsProvider{ + inner: awsauth.NewCredentialsProvider(cfg.Credentials), + } + + // SetAuth overrides any inline credentials that ApplyURI may have extracted + // from the URI; the AWS SDK default chain provides credentials instead. + client, err := mongo.Connect( + options.Client(). + ApplyURI(rawURI). + SetAuth(options.Credential{ + AuthMechanism: "MONGODB-AWS", + AWSCredentialsProvider: tracking, + }), + ) + if err != nil { + t.Fatalf("failed to connect: %v", err) + } + defer func() { + if err := client.Disconnect(context.Background()); err != nil { + t.Errorf("Disconnect: %v", err) + } + }() + + err = client.Database("aws").Collection("test"). + FindOne(context.Background(), bson.D{{Key: "x", Value: 1}}).Err() + if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { + t.Fatalf("unexpected FindOne error: %v", err) + } + + if tracking.called == 0 { + t.Fatal("expected custom credential provider to be called at least once") + } +} diff --git a/ext/awsauth/test/go.mod b/ext/awsauth/test/go.mod new file mode 100644 index 0000000000..547ddc13cd --- /dev/null +++ b/ext/awsauth/test/go.mod @@ -0,0 +1,31 @@ +module go.mongodb.org/mongo-driver/ext/awsauth/test + +go 1.19 + +require ( + github.com/aws/aws-sdk-go-v2/config v1.0.0 + go.mongodb.org/mongo-driver/ext/awsauth v0.0.0 + go.mongodb.org/mongo-driver/v2 v2.0.0-beta2 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.0.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.0.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.0.0 // indirect + github.com/aws/smithy-go v1.0.0 // indirect + github.com/klauspost/compress v1.17.6 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/text v0.22.0 // indirect +) + +replace ( + go.mongodb.org/mongo-driver/ext/awsauth => ../ + go.mongodb.org/mongo-driver/v2 => ../../.. +) diff --git a/ext/awsauth/test/go.sum b/ext/awsauth/test/go.sum new file mode 100644 index 0000000000..f04f4ea2a2 --- /dev/null +++ b/ext/awsauth/test/go.sum @@ -0,0 +1,66 @@ +github.com/aws/aws-sdk-go-v2 v1.0.0 h1:ncEVPoHArsG+HjoDe/3ex/TG1CbLwMQ4eaWj0UGdyTo= +github.com/aws/aws-sdk-go-v2 v1.0.0/go.mod h1:smfAbmpW+tcRVuNUjo3MOArSZmW72t62rkCzc2i0TWM= +github.com/aws/aws-sdk-go-v2/config v1.0.0 h1:x6vSFAwqAvhYPeSu60f0ZUlGHo3PKKmwDOTL8aMXtv4= +github.com/aws/aws-sdk-go-v2/config v1.0.0/go.mod h1:WysE/OpUgE37tjtmtJd8GXgT8s1euilE5XtUkRNUQ1w= +github.com/aws/aws-sdk-go-v2/credentials v1.0.0 h1:0M7netgZ8gCV4v7z1km+Fbl7j6KQYyZL7SS0/l5Jn/4= +github.com/aws/aws-sdk-go-v2/credentials v1.0.0/go.mod h1:/SvsiqBf509hG4Bddigr3NB12MIpfHhZapyBurJe8aY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.0 h1:lO7fH5n7Q1dKcDBpuTmwJylD1bOQiRig8LI6TD9yVQk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.0/go.mod h1:wpMHDCXvOXZxGCRSidyepa8uJHY4vaBGfY2/+oKU/Bc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.0 h1:IAutMPSrynpvKOpHG6HyWHmh1xmxWAmYOK84NrQVqVQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.0/go.mod h1:3jExOmpbjgPnz2FJaMOfbSk1heTkZ66aD3yNtVhnjvI= +github.com/aws/aws-sdk-go-v2/service/sts v1.0.0 h1:6XCgxNfE4L/Fnq+InhVNd16DKc6Ue1f3dJl3IwwJRUQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.0.0/go.mod h1:5f+cELGATgill5Pu3/vK3Ebuigstc+qYEHW5MvGWZO4= +github.com/aws/smithy-go v1.0.0 h1:hkhcRKG9rJ4Fn+RbfXY7Tz7b3ITLDyolBnLLBhwbg/c= +github.com/aws/smithy-go v1.0.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +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/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/go.work b/go.work index 5aa3762826..e544efb123 100644 --- a/go.work +++ b/go.work @@ -8,4 +8,7 @@ use ( ./internal/cmd/faas/awslambda/mongodb ./internal/test/compilecheck ./internal/test/goleak + ./ext/awsauth + ./ext/awsauth/examples + ./ext/awsauth/test ) diff --git a/internal/aws/credentials/chain_provider.go b/internal/aws/credentials/chain_provider.go index 4f593c097b..4becf8aa03 100644 --- a/internal/aws/credentials/chain_provider.go +++ b/internal/aws/credentials/chain_provider.go @@ -11,25 +11,20 @@ package credentials import ( + "context" + "errors" + "go.mongodb.org/mongo-driver/v2/internal/aws/awserr" ) -// A ChainProvider will search for a provider which returns credentials -// and cache that provider until Retrieve is called again. -// // The ChainProvider provides a way of chaining multiple providers together // which will pick the first available using priority order of the Providers // in the list. // // If none of the Providers retrieve valid credentials Value, ChainProvider's -// Retrieve() will return the error ErrNoValidProvidersFoundInChain. -// -// If a Provider is found which returns valid credentials Value ChainProvider -// will cache that Provider for all calls to IsExpired(), until Retrieve is -// called again. +// Retrieve() will return an error. type ChainProvider struct { Providers []Provider - curr Provider } // NewChainCredentials returns a pointer to a new Credentials object @@ -42,31 +37,19 @@ func NewChainCredentials(providers []Provider) *Credentials { // Retrieve returns the credentials value or error if no provider returned // without error. -// -// If a provider is found it will be cached and any calls to IsExpired() -// will return the expired state of the cached provider. -func (c *ChainProvider) Retrieve() (Value, error) { +func (c *ChainProvider) Retrieve(ctx context.Context) (Value, error) { errs := make([]error, 0, len(c.Providers)) for _, p := range c.Providers { - creds, err := p.Retrieve() + creds, err := p.Retrieve(ctx) if err == nil { - c.curr = p - return creds, nil + if !creds.Expired() && creds.HasKeys() { + return creds, nil + } + err = errors.New("credentials are invalid") } errs = append(errs, err) } - c.curr = nil err := awserr.NewBatchError("NoCredentialProviders", "no valid providers in chain", errs) return Value{}, err } - -// IsExpired will returned the expired state of the currently cached provider -// if there is one. If there is no current provider, true will be returned. -func (c *ChainProvider) IsExpired() bool { - if c.curr != nil { - return c.curr.IsExpired() - } - - return true -} diff --git a/internal/aws/credentials/chain_provider_test.go b/internal/aws/credentials/chain_provider_test.go index 20644def01..9c6662ccce 100644 --- a/internal/aws/credentials/chain_provider_test.go +++ b/internal/aws/credentials/chain_provider_test.go @@ -11,34 +11,19 @@ package credentials import ( + "context" "reflect" "testing" "go.mongodb.org/mongo-driver/v2/internal/aws/awserr" ) -type secondStubProvider struct { - creds Value - expired bool - err error -} - -func (s *secondStubProvider) Retrieve() (Value, error) { - s.expired = false - s.creds.ProviderName = "secondStubProvider" - return s.creds, s.err -} - -func (s *secondStubProvider) IsExpired() bool { - return s.expired -} - -func TestChainProviderWithNames(t *testing.T) { +func TestChainProviderRetrieve(t *testing.T) { p := &ChainProvider{ Providers: []Provider{ &stubProvider{err: awserr.New("FirstError", "first provider error", nil)}, &stubProvider{err: awserr.New("SecondError", "second provider error", nil)}, - &secondStubProvider{ + &stubProvider{ creds: Value{ AccessKeyID: "AKIF", SecretAccessKey: "NOSECRET", @@ -55,15 +40,11 @@ func TestChainProviderWithNames(t *testing.T) { }, } - creds, err := p.Retrieve() + creds, err := p.Retrieve(context.Background()) if err != nil { t.Errorf("Expect no error, got %v", err) } - if e, a := "secondStubProvider", creds.ProviderName; e != a { - t.Errorf("Expect provider name to match, %v got, %v", e, a) - } - // Also check credentials if e, a := "AKIF", creds.AccessKeyID; e != a { t.Errorf("Expect access key ID to match, %v got %v", e, a) } @@ -75,78 +56,12 @@ func TestChainProviderWithNames(t *testing.T) { } } -func TestChainProviderGet(t *testing.T) { - p := &ChainProvider{ - Providers: []Provider{ - &stubProvider{err: awserr.New("FirstError", "first provider error", nil)}, - &stubProvider{err: awserr.New("SecondError", "second provider error", nil)}, - &stubProvider{ - creds: Value{ - AccessKeyID: "AKID", - SecretAccessKey: "SECRET", - SessionToken: "", - }, - }, - }, - } - - creds, err := p.Retrieve() - if err != nil { - t.Errorf("Expect no error, got %v", err) - } - if e, a := "AKID", creds.AccessKeyID; e != a { - t.Errorf("Expect access key ID to match, %v got %v", e, a) - } - if e, a := "SECRET", creds.SecretAccessKey; e != a { - t.Errorf("Expect secret access key to match, %v got %v", e, a) - } - if v := creds.SessionToken; len(v) != 0 { - t.Errorf("Expect session token to be empty, %v", v) - } -} - -func TestChainProviderIsExpired(t *testing.T) { - stubProvider := &stubProvider{expired: true} - p := &ChainProvider{ - Providers: []Provider{ - stubProvider, - }, - } - - if !p.IsExpired() { - t.Errorf("Expect expired to be true before any Retrieve") - } - _, err := p.Retrieve() - if err != nil { - t.Errorf("Expect no error, got %v", err) - } - if p.IsExpired() { - t.Errorf("Expect not expired after retrieve") - } - - stubProvider.expired = true - if !p.IsExpired() { - t.Errorf("Expect return of expired provider") - } - - _, err = p.Retrieve() - if err != nil { - t.Errorf("Expect no error, got %v", err) - } - if p.IsExpired() { - t.Errorf("Expect not expired after retrieve") - } -} - func TestChainProviderWithNoProvider(t *testing.T) { p := &ChainProvider{ Providers: []Provider{}, } - if !p.IsExpired() { - t.Errorf("Expect expired with no providers") - } - _, err := p.Retrieve() + _, err := p.Retrieve(context.Background()) if err.Error() != "NoCredentialProviders: no valid providers in chain" { t.Errorf("Expect no providers error returned, got %v", err) } @@ -164,10 +79,7 @@ func TestChainProviderWithNoValidProvider(t *testing.T) { }, } - if !p.IsExpired() { - t.Errorf("Expect expired with no providers") - } - _, err := p.Retrieve() + _, err := p.Retrieve(context.Background()) expectErr := awserr.NewBatchError("NoCredentialProviders", "no valid providers in chain", errs) if e, a := expectErr, err; !reflect.DeepEqual(e, a) { diff --git a/internal/aws/credentials/credentials.go b/internal/aws/credentials/credentials.go index 919d0819b1..f64fc223f8 100644 --- a/internal/aws/credentials/credentials.go +++ b/internal/aws/credentials/credentials.go @@ -12,7 +12,7 @@ package credentials import ( "context" - "sync" + "sync/atomic" "time" "go.mongodb.org/mongo-driver/v2/internal/aws/awserr" @@ -33,8 +33,30 @@ type Value struct { // AWS Session Token SessionToken string - // Provider used to get credentials - ProviderName string + // Source of the credentials + Source string + + // States if the credentials can expire or not. + CanExpire bool + + // The time the credentials will expire at. Should be ignored if CanExpire + // is false. + Expires time.Time + + // The ID of the account for the credentials. + AccountID string +} + +// Expired returns if the credentials have expired. +func (v Value) Expired() bool { + if v.CanExpire { + // Calling Round(0) on the current time will truncate the monotonic + // reading only. Ensures credential expiry time is always based on + // reported wall-clock time. + return !v.Expires.After(time.Now().Round(0)) + } + + return false } // HasKeys returns if the credentials Value has both AccessKeyID and @@ -52,18 +74,7 @@ func (v Value) HasKeys() bool { type Provider interface { // Retrieve returns nil if it successfully retrieved the value. // Error is returned if the value were not obtainable, or empty. - Retrieve() (Value, error) - - // IsExpired returns if the credentials are no longer valid, and need - // to be retrieved. - IsExpired() bool -} - -// ProviderWithContext is a Provider that can retrieve credentials with a Context -type ProviderWithContext interface { - Provider - - RetrieveWithContext(context.Context) (Value, error) + Retrieve(context.Context) (Value, error) } // A Credentials provides concurrency safe retrieval of AWS credentials Value. @@ -79,13 +90,12 @@ type ProviderWithContext interface { // // The first Credentials.Get() will always call Provider.Retrieve() to get the // first instance of the credentials Value. All calls to Get() after that -// will return the cached credentials Value until IsExpired() returns true. +// will return the cached credentials Value until Expired() returns true. type Credentials struct { - sf singleflight.Group - - m sync.RWMutex - creds Value provider Provider + + creds atomic.Value + sf singleflight.Group } // NewCredentials returns a pointer to a new Credentials with the provider set. @@ -96,34 +106,18 @@ func NewCredentials(provider Provider) *Credentials { return c } -// GetWithContext returns the credentials value, or error if the credentials +// Get returns the credentials value, or error if the credentials // Value failed to be retrieved. Will return early if the passed in context is // canceled. // // Will return the cached credentials Value if it has not expired. If the // credentials Value has expired the Provider's Retrieve() will be called // to refresh the credentials. -// -// If Credentials.Expire() was called the credentials Value will be force -// expired, and the next call to Get() will cause them to be refreshed. -func (c *Credentials) GetWithContext(ctx context.Context) (Value, error) { - // Check if credentials are cached, and not expired. - select { - case curCreds, ok := <-c.asyncIsExpired(): - // ok will only be true, of the credentials were not expired. ok will - // be false and have no value if the credentials are expired. - if ok { - return curCreds, nil - } - case <-ctx.Done(): - return Value{}, awserr.New("RequestCanceled", - "request context canceled", ctx.Err()) - } - +func (c *Credentials) Get(ctx context.Context) (Value, error) { // Cannot pass context down to the actual retrieve, because the first // context would cancel the whole group when there is not direct // association of items in the group. - resCh := c.sf.DoChan("", func() (interface{}, error) { + resCh := c.sf.DoChan("", func() (any, error) { return c.singleRetrieve(&suppressedContext{ctx}) }) select { @@ -135,49 +129,33 @@ func (c *Credentials) GetWithContext(ctx context.Context) (Value, error) { } } -func (c *Credentials) singleRetrieve(ctx context.Context) (interface{}, error) { - c.m.Lock() - defer c.m.Unlock() - - if curCreds := c.creds; !c.isExpiredLocked(curCreds) { - return curCreds, nil +func (c *Credentials) singleRetrieve(ctx context.Context) (any, error) { + if currCreds, ok := c.getCreds(); ok && !currCreds.Expired() { + return currCreds, nil } - var creds Value - var err error - if p, ok := c.provider.(ProviderWithContext); ok { - creds, err = p.RetrieveWithContext(ctx) - } else { - creds, err = c.provider.Retrieve() - } + newCreds, err := c.provider.Retrieve(ctx) if err == nil { - c.creds = creds + c.creds.Store(&newCreds) } - return creds, err + return newCreds, err } -// asyncIsExpired returns a channel of credentials Value. If the channel is -// closed the credentials are expired and credentials value are not empty. -func (c *Credentials) asyncIsExpired() <-chan Value { - ch := make(chan Value, 1) - go func() { - c.m.RLock() - defer c.m.RUnlock() - - if curCreds := c.creds; !c.isExpiredLocked(curCreds) { - ch <- curCreds - } - - close(ch) - }() +// getCreds returns the currently stored credentials and true. Returning false +// if no credentials were stored. +func (c *Credentials) getCreds() (Value, bool) { + v := c.creds.Load() + if v == nil { + return Value{}, false + } - return ch -} + val := v.(*Value) + if val == nil || !val.HasKeys() { + return Value{}, false + } -// isExpiredLocked helper method wrapping the definition of expired credentials. -func (c *Credentials) isExpiredLocked(creds interface{}) bool { - return creds == nil || creds.(Value) == Value{} || c.provider.IsExpired() + return *val, true } type suppressedContext struct { diff --git a/internal/aws/credentials/credentials_test.go b/internal/aws/credentials/credentials_test.go index cbe19ce15f..42a2290ed0 100644 --- a/internal/aws/credentials/credentials_test.go +++ b/internal/aws/credentials/credentials_test.go @@ -12,38 +12,21 @@ package credentials import ( "context" - "sync" "testing" "time" "go.mongodb.org/mongo-driver/v2/internal/aws/awserr" ) -func isExpired(c *Credentials) bool { - c.m.RLock() - defer c.m.RUnlock() - - return c.isExpiredLocked(c.creds) -} - type stubProvider struct { - creds Value - retrievedCount int - expired bool - err error + creds Value + err error } -func (s *stubProvider) Retrieve() (Value, error) { - s.retrievedCount++ - s.expired = false - s.creds.ProviderName = "stubProvider" +func (s *stubProvider) Retrieve(_ context.Context) (Value, error) { return s.creds, s.err } -func (s *stubProvider) IsExpired() bool { - return s.expired -} - func TestCredentialsGet(t *testing.T) { c := NewCredentials(&stubProvider{ creds: Value{ @@ -51,10 +34,9 @@ func TestCredentialsGet(t *testing.T) { SecretAccessKey: "SECRET", SessionToken: "", }, - expired: true, }) - creds, err := c.GetWithContext(context.Background()) + creds, err := c.Get(context.Background()) if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -70,102 +52,22 @@ func TestCredentialsGet(t *testing.T) { } func TestCredentialsGetWithError(t *testing.T) { - c := NewCredentials(&stubProvider{err: awserr.New("provider error", "", nil), expired: true}) + c := NewCredentials(&stubProvider{err: awserr.New("provider error", "", nil)}) - _, err := c.GetWithContext(context.Background()) + _, err := c.Get(context.Background()) if e, a := "provider error", err.(awserr.Error).Code(); e != a { t.Errorf("Expected provider error, %v got %v", e, a) } } -func TestCredentialsExpire(t *testing.T) { - stub := &stubProvider{} - c := NewCredentials(stub) - - stub.expired = false - if !isExpired(c) { - t.Errorf("Expected to start out expired") - } - - _, err := c.GetWithContext(context.Background()) - if err != nil { - t.Errorf("Expected no err, got %v", err) - } - if isExpired(c) { - t.Errorf("Expected not to be expired") - } - - stub.expired = true - if !isExpired(c) { - t.Errorf("Expected to be expired") - } -} - -func TestCredentialsGetWithProviderName(t *testing.T) { - stub := &stubProvider{} - - c := NewCredentials(stub) - - creds, err := c.GetWithContext(context.Background()) - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if e, a := creds.ProviderName, "stubProvider"; e != a { - t.Errorf("Expected provider name to match, %v got %v", e, a) - } -} - -type MockProvider struct { - // The date/time when to expire on - expiration time.Time - - // If set will be used by IsExpired to determine the current time. - // Defaults to time.Now if CurrentTime is not set. Available for testing - // to be able to mock out the current time. - CurrentTime func() time.Time -} - -// IsExpired returns if the credentials are expired. -func (e *MockProvider) IsExpired() bool { - curTime := e.CurrentTime - if curTime == nil { - curTime = time.Now - } - return e.expiration.Before(curTime()) -} - -func (*MockProvider) Retrieve() (Value, error) { - return Value{}, nil -} - -func TestCredentialsIsExpired_Race(_ *testing.T) { - creds := NewChainCredentials([]Provider{&MockProvider{}}) - - starter := make(chan struct{}) - var wg sync.WaitGroup - wg.Add(10) - for i := 0; i < 10; i++ { - go func() { - defer wg.Done() - <-starter - for i := 0; i < 100; i++ { - isExpired(creds) - } - }() - } - close(starter) - - wg.Wait() -} - type stubProviderConcurrent struct { stubProvider done chan struct{} } -func (s *stubProviderConcurrent) Retrieve() (Value, error) { +func (s *stubProviderConcurrent) Retrieve(ctx context.Context) (Value, error) { <-s.done - return s.stubProvider.Retrieve() + return s.stubProvider.Retrieve(ctx) } func TestCredentialsGetConcurrent(t *testing.T) { @@ -178,7 +80,7 @@ func TestCredentialsGetConcurrent(t *testing.T) { for i := 0; i < 2; i++ { go func() { - _, err := c.GetWithContext(context.Background()) + _, err := c.Get(context.Background()) if err != nil { t.Errorf("Expected no err, got %v", err) } @@ -187,6 +89,7 @@ func TestCredentialsGetConcurrent(t *testing.T) { } // Validates that a single call to Retrieve is shared between two calls to Get + time.Sleep(10 * time.Millisecond) stub.done <- struct{}{} <-done <-done diff --git a/internal/aws/signer/v4/v4.go b/internal/aws/signer/v4/v4.go index eaf5cca3ee..4bc9fa130e 100644 --- a/internal/aws/signer/v4/v4.go +++ b/internal/aws/signer/v4/v4.go @@ -97,11 +97,10 @@ type signingCtx struct { // is not needed as the full request context will be captured by the http.Request // value. It is included for reference though. // -// Sign will set the request's Body to be the `body` parameter passed in. If -// the body is not already an io.ReadCloser, it will be wrapped within one. If -// a `nil` body parameter passed to Sign, the request's Body field will be -// also set to nil. Its important to note that this functionality will not -// change the request's ContentLength of the request. +// Sign will set the request's Body to be the `body` parameter passed in. If an +// empty body parameter passed to Sign, the request's Body field will be set to +// nil. Its important to note that this functionality will not change the +// request's ContentLength of the request. // // Sign differs from Presign in that it will sign the request using HTTP // header values. This type of signing is intended for http.Request values that @@ -135,7 +134,7 @@ func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, regi } var err error - ctx.credValues, err = v4.Credentials.GetWithContext(r.Context()) + ctx.credValues, err = v4.Credentials.Get(r.Context()) if err != nil { return http.Header{}, err } diff --git a/internal/aws/signer/v4/v4_test.go b/internal/aws/signer/v4/v4_test.go index 8497574261..efd7a6f29e 100644 --- a/internal/aws/signer/v4/v4_test.go +++ b/internal/aws/signer/v4/v4_test.go @@ -382,7 +382,7 @@ func BenchmarkSignRequest(b *testing.B) { signer := buildSigner() req, body := buildRequestReaderSeeker("dynamodb", "us-east-1", "{}") for i := 0; i < b.N; i++ { - _, err := signer.Sign(req, body, "dynamodb", "us-east-1", time.Now()) + _, err := signer.signWithBody(req, body, "dynamodb", "us-east-1", time.Now()) if err != nil { b.Errorf("Expected no err, got %v", err) } diff --git a/internal/credproviders/assume_role_provider.go b/internal/credproviders/assume_role_provider.go index eec2247c70..1965799613 100644 --- a/internal/credproviders/assume_role_provider.go +++ b/internal/credproviders/assume_role_provider.go @@ -20,9 +20,6 @@ import ( ) const ( - // assumeRoleProviderName provides a name of assume role provider - assumeRoleProviderName = "AssumeRoleProvider" - stsURI = `https://sts.amazonaws.com/?Action=AssumeRoleWithWebIdentity&RoleSessionName=%s&RoleArn=%s&WebIdentityToken=%s&Version=2011-06-15` ) @@ -33,12 +30,11 @@ type AssumeRoleProvider struct { AwsRoleSessionNameEnv EnvVar httpClient *http.Client - expiration time.Time // expiryWindow will allow the credentials to trigger refreshing prior to the credentials actually expiring. // This is beneficial so expiring credentials do not cause request to fail unexpectedly due to exceptions. // - // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true + // E.g., an ExpiryWindow of 10s would cause calls to Expired() to return true // 10 seconds before the credentials are actually expired. expiryWindow time.Duration } @@ -57,11 +53,11 @@ func NewAssumeRoleProvider(httpClient *http.Client, expiryWindow time.Duration) } } -// RetrieveWithContext retrieves the keys from the AWS service. -func (a *AssumeRoleProvider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) { +// Retrieve retrieves the keys from the AWS service. +func (a *AssumeRoleProvider) Retrieve(ctx context.Context) (credentials.Value, error) { const defaultHTTPTimeout = 10 * time.Second - v := credentials.Value{ProviderName: assumeRoleProviderName} + var v credentials.Value roleArn := a.AwsRoleArnEnv.Get() tokenFile := a.AwsWebIdentityTokenFileEnv.Get() @@ -131,18 +127,9 @@ func (a *AssumeRoleProvider) RetrieveWithContext(ctx context.Context) (credentia if !v.HasKeys() { return v, errors.New("failed to retrieve web identity keys") } + v.CanExpire = true sec := int64(stsResp.Response.Result.Credentials.Expiration) - a.expiration = time.Unix(sec, 0).Add(-a.expiryWindow) + v.Expires = time.Unix(sec, 0).Add(-a.expiryWindow) return v, nil } - -// Retrieve retrieves the keys from the AWS service. -func (a *AssumeRoleProvider) Retrieve() (credentials.Value, error) { - return a.RetrieveWithContext(context.Background()) -} - -// IsExpired returns true if the credentials are expired. -func (a *AssumeRoleProvider) IsExpired() bool { - return a.expiration.Before(time.Now()) -} diff --git a/internal/credproviders/ec2_provider.go b/internal/credproviders/ec2_provider.go index df15a7f2c3..2de0a2857d 100644 --- a/internal/credproviders/ec2_provider.go +++ b/internal/credproviders/ec2_provider.go @@ -19,9 +19,6 @@ import ( ) const ( - // ec2ProviderName provides a name of EC2 provider - ec2ProviderName = "EC2Provider" - awsEC2URI = "http://169.254.169.254/" awsEC2RolePath = "latest/meta-data/iam/security-credentials/" awsEC2TokenPath = "latest/api/token" @@ -32,12 +29,11 @@ const ( // An EC2Provider retrieves credentials from EC2 metadata. type EC2Provider struct { httpClient *http.Client - expiration time.Time // expiryWindow will allow the credentials to trigger refreshing prior to the credentials actually expiring. // This is beneficial so expiring credentials do not cause request to fail unexpectedly due to exceptions. // - // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true + // E.g., an ExpiryWindow of 10s would cause calls to Expired() to return true // 10 seconds before the credentials are actually expired. expiryWindow time.Duration } @@ -108,7 +104,7 @@ func (e *EC2Provider) getRoleName(ctx context.Context, token string) (string, er } func (e *EC2Provider) getCredentials(ctx context.Context, token string, role string) (credentials.Value, time.Time, error) { - v := credentials.Value{ProviderName: ec2ProviderName} + var v credentials.Value pathWithRole := awsEC2URI + awsEC2RolePath + role req, err := http.NewRequest(http.MethodGet, pathWithRole, nil) @@ -146,9 +142,9 @@ func (e *EC2Provider) getCredentials(ctx context.Context, token string, role str return v, ec2Resp.Expiration, nil } -// RetrieveWithContext retrieves the keys from the AWS service. -func (e *EC2Provider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) { - v := credentials.Value{ProviderName: ec2ProviderName} +// Retrieve retrieves the keys from the AWS service. +func (e *EC2Provider) Retrieve(ctx context.Context) (credentials.Value, error) { + var v credentials.Value token, err := e.getToken(ctx) if err != nil { @@ -167,17 +163,8 @@ func (e *EC2Provider) RetrieveWithContext(ctx context.Context) (credentials.Valu if !v.HasKeys() { return v, errors.New("failed to retrieve EC2 keys") } - e.expiration = exp.Add(-e.expiryWindow) + v.CanExpire = true + v.Expires = exp.Add(-e.expiryWindow) return v, nil } - -// Retrieve retrieves the keys from the AWS service. -func (e *EC2Provider) Retrieve() (credentials.Value, error) { - return e.RetrieveWithContext(context.Background()) -} - -// IsExpired returns true if the credentials are expired. -func (e *EC2Provider) IsExpired() bool { - return e.expiration.Before(time.Now()) -} diff --git a/internal/credproviders/ecs_provider.go b/internal/credproviders/ecs_provider.go index 6816a3182d..e7b7ab643b 100644 --- a/internal/credproviders/ecs_provider.go +++ b/internal/credproviders/ecs_provider.go @@ -18,9 +18,6 @@ import ( ) const ( - // ecsProviderName provides a name of ECS provider - ecsProviderName = "ECSProvider" - awsRelativeURI = "http://169.254.170.2/" ) @@ -29,12 +26,11 @@ type ECSProvider struct { AwsContainerCredentialsRelativeURIEnv EnvVar httpClient *http.Client - expiration time.Time // expiryWindow will allow the credentials to trigger refreshing prior to the credentials actually expiring. // This is beneficial so expiring credentials do not cause request to fail unexpectedly due to exceptions. // - // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true + // E.g., an ExpiryWindow of 10s would cause calls to Expired() to return true // 10 seconds before the credentials are actually expired. expiryWindow time.Duration } @@ -49,11 +45,11 @@ func NewECSProvider(httpClient *http.Client, expiryWindow time.Duration) *ECSPro } } -// RetrieveWithContext retrieves the keys from the AWS service. -func (e *ECSProvider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) { +// Retrieve retrieves the keys from the AWS service. +func (e *ECSProvider) Retrieve(ctx context.Context) (credentials.Value, error) { const defaultHTTPTimeout = 10 * time.Second - v := credentials.Value{ProviderName: ecsProviderName} + var v credentials.Value relativeEcsURI := e.AwsContainerCredentialsRelativeURIEnv.Get() if len(relativeEcsURI) == 0 { @@ -96,17 +92,8 @@ func (e *ECSProvider) RetrieveWithContext(ctx context.Context) (credentials.Valu if !v.HasKeys() { return v, errors.New("failed to retrieve ECS keys") } - e.expiration = ecsResp.Expiration.Add(-e.expiryWindow) + v.CanExpire = true + v.Expires = ecsResp.Expiration.Add(-e.expiryWindow) return v, nil } - -// Retrieve retrieves the keys from the AWS service. -func (e *ECSProvider) Retrieve() (credentials.Value, error) { - return e.RetrieveWithContext(context.Background()) -} - -// IsExpired returns true if the credentials are expired. -func (e *ECSProvider) IsExpired() bool { - return e.expiration.Before(time.Now()) -} diff --git a/internal/credproviders/env_provider.go b/internal/credproviders/env_provider.go index cf6bb60b31..2483c2dcd0 100644 --- a/internal/credproviders/env_provider.go +++ b/internal/credproviders/env_provider.go @@ -7,14 +7,12 @@ package credproviders import ( + "context" "os" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" ) -// envProviderName provides a name of Env provider -const envProviderName = "EnvProvider" - // EnvVar is an environment variable type EnvVar string @@ -29,8 +27,6 @@ type EnvProvider struct { AwsAccessKeyIDEnv EnvVar AwsSecretAccessKeyEnv EnvVar AwsSessionTokenEnv EnvVar - - retrieved bool } // NewEnvProvider returns a pointer to an ECS credential provider. @@ -46,24 +42,18 @@ func NewEnvProvider() *EnvProvider { } // Retrieve retrieves the keys from the environment. -func (e *EnvProvider) Retrieve() (credentials.Value, error) { - e.retrieved = false - +func (e *EnvProvider) Retrieve(_ context.Context) (credentials.Value, error) { v := credentials.Value{ AccessKeyID: e.AwsAccessKeyIDEnv.Get(), SecretAccessKey: e.AwsSecretAccessKeyEnv.Get(), SessionToken: e.AwsSessionTokenEnv.Get(), - ProviderName: envProviderName, + CanExpire: false, } err := verify(v) - if err == nil { - e.retrieved = true + if err != nil { + // Expire the credentials if invalid. + v.CanExpire = true } return v, err } - -// IsExpired returns true if the credentials have not been retrieved. -func (e *EnvProvider) IsExpired() bool { - return !e.retrieved -} diff --git a/internal/credproviders/imds_provider.go b/internal/credproviders/imds_provider.go index f3674c727b..005b61da10 100644 --- a/internal/credproviders/imds_provider.go +++ b/internal/credproviders/imds_provider.go @@ -19,16 +19,12 @@ import ( ) const ( - // AzureProviderName provides a name of Azure provider - AzureProviderName = "AzureProvider" - azureURI = "http://169.254.169.254/metadata/identity/oauth2/token" ) // An AzureProvider retrieves credentials from Azure IMDS. type AzureProvider struct { httpClient *http.Client - expiration time.Time expiryWindow time.Duration } @@ -36,14 +32,13 @@ type AzureProvider struct { func NewAzureProvider(httpClient *http.Client, expiryWindow time.Duration) *AzureProvider { return &AzureProvider{ httpClient: httpClient, - expiration: time.Time{}, expiryWindow: expiryWindow, } } -// RetrieveWithContext retrieves the keys from the Azure service. -func (a *AzureProvider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) { - v := credentials.Value{ProviderName: AzureProviderName} +// Retrieve retrieves the keys from the Azure service. +func (a *AzureProvider) Retrieve(ctx context.Context) (credentials.Value, error) { + var v credentials.Value req, err := http.NewRequest(http.MethodGet, azureURI, nil) if err != nil { return v, fmt.Errorf("unable to retrieve Azure credentials: %w", err) @@ -85,19 +80,10 @@ func (a *AzureProvider) RetrieveWithContext(ctx context.Context) (credentials.Va if err != nil { return v, err } + v.CanExpire = true if expiration := expiresIn - a.expiryWindow; expiration > 0 { - a.expiration = time.Now().Add(expiration) + v.Expires = time.Now().Add(expiration) } return v, err } - -// Retrieve retrieves the keys from the Azure service. -func (a *AzureProvider) Retrieve() (credentials.Value, error) { - return a.RetrieveWithContext(context.Background()) -} - -// IsExpired returns if the credentials have been retrieved. -func (a *AzureProvider) IsExpired() bool { - return a.expiration.Before(time.Now()) -} diff --git a/internal/credproviders/static_provider.go b/internal/credproviders/static_provider.go index c7194cd0f5..2484e493cd 100644 --- a/internal/credproviders/static_provider.go +++ b/internal/credproviders/static_provider.go @@ -7,14 +7,12 @@ package credproviders import ( + "context" "errors" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" ) -// staticProviderName provides a name of Static provider -const staticProviderName = "StaticProvider" - // A StaticProvider is a set of credentials which are set programmatically, // and will never expire. type StaticProvider struct { @@ -41,18 +39,10 @@ func verify(v credentials.Value) error { } // Retrieve returns the credentials or error if the credentials are invalid. -func (s *StaticProvider) Retrieve() (credentials.Value, error) { +func (s *StaticProvider) Retrieve(_ context.Context) (credentials.Value, error) { if !s.verified { s.err = verify(s.Value) - s.ProviderName = staticProviderName s.verified = true } return s.Value, s.err } - -// IsExpired returns if the credentials are expired. -// -// For StaticProvider, the credentials never expired. -func (s *StaticProvider) IsExpired() bool { - return false -} diff --git a/internal/credutil/aws_options_provider.go b/internal/credutil/aws_options_provider.go new file mode 100644 index 0000000000..8aa0448fbb --- /dev/null +++ b/internal/credutil/aws_options_provider.go @@ -0,0 +1,37 @@ +// Copyright (C) MongoDB, Inc. 2017-present. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +package credutil + +import ( + "context" + + "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +// AWSOptionsProvider adapts options.AWSCredentialsProvider to the +// credentials.Provider interface used internally by the driver. +type AWSOptionsProvider struct { + Provider options.AWSCredentialsProvider +} + +// Retrieve returns the credentials from the wrapped options.AWSCredentialsProvider. +func (p AWSOptionsProvider) Retrieve(ctx context.Context) (credentials.Value, error) { + creds, err := p.Provider.Retrieve(ctx) + if err != nil { + return credentials.Value{}, err + } + return credentials.Value{ + AccessKeyID: creds.AccessKeyID, + SecretAccessKey: creds.SecretAccessKey, + SessionToken: creds.SessionToken, + Source: creds.Source, + CanExpire: creds.CanExpire, + Expires: creds.Expires, + AccountID: creds.AccountID, + }, nil +} diff --git a/internal/integration/client_side_encryption_prose_test.go b/internal/integration/client_side_encryption_prose_test.go index c59ac26b77..110a909697 100644 --- a/internal/integration/client_side_encryption_prose_test.go +++ b/internal/integration/client_side_encryption_prose_test.go @@ -3133,6 +3133,114 @@ func TestClientSideEncryptionProse_24_kmw_retry_tests(t *testing.T) { } } +type awsCredentialsProvider struct { + cnt int +} + +func (p *awsCredentialsProvider) Retrieve(ctx context.Context) (options.AWSCredentials, error) { + p.cnt++ + return options.AWSCredentials{ + AccessKeyID: awsAccessKeyID, + SecretAccessKey: awsSecretAccessKey, + }, nil +} + +func TestClientSideEncryptionProse_26_custom_aws_credentials(t *testing.T) { + mt := mtest.New(t, mtest.NewOptions().CreateClient(false)) + + mt.Run("Case 1: ClientEncryption with credentialProviders and incorrect kmsProviders", func(mt *mtest.T) { + opts := options.Client().ApplyURI(mtest.ClusterURI()) + integtest.AddTestServerAPIVersion(opts) + keyVaultClient, err := mongo.Connect(opts) + assert.NoErrorf(mt, err, "error on Connect: %v", err) + + var provider awsCredentialsProvider + ceo := options.ClientEncryption(). + SetKeyVaultNamespace("keyvault.datakeys"). + SetKmsProviders(map[string]map[string]any{ + "aws": { + "accessKeyId": awsAccessKeyID, + "secretAccessKey": awsSecretAccessKey, + }, + }). + SetAWSCredentialsProvider(&provider) + _, err = mongo.NewClientEncryption(keyVaultClient, ceo) + assert.ErrorContains(mt, err, "can only provide a custom AWS credential provider", + "unexpected error: %v", err) + }) + + mt.Run("Case 2: ClientEncryption with credentialProviders works", func(mt *mtest.T) { + opts := options.Client().ApplyURI(mtest.ClusterURI()) + integtest.AddTestServerAPIVersion(opts) + keyVaultClient, err := mongo.Connect(opts) + assert.NoErrorf(mt, err, "error on Connect: %v", err) + + var provider awsCredentialsProvider + ceo := options.ClientEncryption(). + SetKeyVaultNamespace("keyvault.datakeys"). + SetKmsProviders(map[string]map[string]any{ + "aws": {}, + }). + SetAWSCredentialsProvider(&provider) + clientEncryption, err := mongo.NewClientEncryption(keyVaultClient, ceo) + assert.NoErrorf(mt, err, "error on NewClientEncryption: %v", err) + + dkOpts := options.DataKey().SetMasterKey(bson.D{ + {"region", "us-east-1"}, + {"key", "arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0"}, + }) + _, err = clientEncryption.CreateDataKey(context.Background(), "aws", dkOpts) + assert.NoErrorf(mt, err, "unexpected error %v", err) + assert.Equal(mt, 1, provider.cnt, "expected credential provider to be called once") + }) + + mt.Run("Case 3: AutoEncryptionOpts with credentialProviders and incorrect kmsProviders", func(mt *mtest.T) { + var provider awsCredentialsProvider + aeo := options.AutoEncryption(). + SetKeyVaultNamespace("keyvault.datakeys"). + SetKmsProviders(map[string]map[string]any{ + "aws": { + "accessKeyId": awsAccessKeyID, + "secretAccessKey": awsSecretAccessKey, + }, + }). + SetAWSCredentialsProvider(&provider) + co := options.Client().SetAutoEncryptionOptions(aeo).ApplyURI(mtest.ClusterURI()) + integtest.AddTestServerAPIVersion(co) + _, err := mongo.Connect(co) + assert.ErrorContainsf(mt, err, "can only provide a custom AWS credential provider", + "unexpected error: %v", err) + }) + + mt.Run("Case 4: ClientEncryption with credentialProviders and valid environment variables", func(mt *mtest.T) { + mt.Setenv("AWS_ACCESS_KEY_ID", os.Getenv("FLE_AWS_ACCESS_KEY_ID")) + mt.Setenv("AWS_SECRET_ACCESS_KEY", os.Getenv("FLE_AWS_SECRET_ACCESS_KEY")) + + opts := options.Client().ApplyURI(mtest.ClusterURI()) + integtest.AddTestServerAPIVersion(opts) + keyVaultClient, err := mongo.Connect(opts) + assert.NoErrorf(mt, err, "error on Connect: %v", err) + + var provider awsCredentialsProvider + ceo := options.ClientEncryption(). + SetKeyVaultNamespace("keyvault.datakeys"). + SetKmsProviders(map[string]map[string]any{ + "aws": {}, + }). + SetAWSCredentialsProvider(&provider) + clientEncryption, err := mongo.NewClientEncryption(keyVaultClient, ceo) + assert.NoErrorf(mt, err, "error on NewClientEncryption: %v", err) + + dkOpts := options.DataKey().SetMasterKey(bson.D{ + {"region", "us-east-1"}, + {"key", "arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0"}, + }) + _, err = clientEncryption.CreateDataKey(context.Background(), "aws", dkOpts) + assert.NoErrorf(mt, err, "unexpected error %v", err) + assert.Equal(mt, 1, provider.cnt, "expected credential provider to be called once") + }) +} + func TestChangeStreams(t *testing.T) { mt := newCSE_T(t, mtest.NewOptions().CreateClient(false).Topologies(mtest.ReplicaSet)) mt.Setup() diff --git a/internal/test/aws/aws_test.go b/internal/test/aws/aws_test.go index 51b07ac696..dae683c303 100644 --- a/internal/test/aws/aws_test.go +++ b/internal/test/aws/aws_test.go @@ -11,14 +11,102 @@ import ( "errors" "os" "testing" + "time" "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/internal/assert" "go.mongodb.org/mongo-driver/v2/internal/require" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) +// staticAWSProvider returns fixed credentials on every call. +type staticAWSProvider struct { + creds struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + Source string + CanExpire bool + Expires time.Time + AccountID string + } +} + +func (p *staticAWSProvider) Retrieve(_ context.Context) (struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + Source string + CanExpire bool + Expires time.Time + AccountID string +}, error, +) { + return p.creds, nil +} + +// trackingAWSProvider wraps another provider and counts invocations. +type trackingAWSProvider struct { + inner options.AWSCredentialsProvider + called int +} + +func (p *trackingAWSProvider) Retrieve(ctx context.Context) (struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + Source string + CanExpire bool + Expires time.Time + AccountID string +}, error, +) { + p.called++ + return p.inner.Retrieve(ctx) +} + +// requireAWSAuthSupport skips t if the server at the given URI does not support +// MONGODB-AWS (introduced in MongoDB 4.4, maxWireVersion 9). The check is performed +// without auth so that it works even when the URI carries AWS credentials. +// The original URI's TLS and topology settings are preserved so that the probe +// connection is valid on TLS-required servers. +func requireAWSAuthSupport(t *testing.T, rawURI string) { + t.Helper() + // Reuse all options from the original URI (TLS, replica set, etc.) but clear + // auth so hello can be sent without triggering AWS credential negotiation. + checkOpts := options.Client().ApplyURI(rawURI) + checkOpts.Auth = nil + client, err := mongo.Connect(checkOpts) + if err != nil { + return // cannot probe; let the real connection surface any error + } + defer func() { _ = client.Disconnect(context.Background()) }() + + var result struct { + MaxWireVersion int32 `bson:"maxWireVersion"` + } + if err := client.Database("admin").RunCommand(context.Background(), bson.D{{Key: "hello", Value: 1}}).Decode(&result); err != nil { + return + } + if result.MaxWireVersion < 9 { + t.Skip("MONGODB-AWS requires MongoDB 4.4+ (maxWireVersion 9)") + } +} + +func awsFindOne(t *testing.T, client *mongo.Client) { + t.Helper() + err := client.Database("aws").Collection("test").FindOne(context.Background(), bson.D{{Key: "x", Value: 1}}).Err() + if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { + t.Fatalf("unexpected FindOne error: %v", err) + } +} + func TestAWS(t *testing.T) { + if os.Getenv("AWS_TEST") == "" { + t.Skip("Skipping test: AWS_TEST environment variable is not set") + } + uri := os.Getenv("MONGODB_URI") if uri == "" { t.Skip("Skipping test: MONGODB_URI environment variable is not set") @@ -31,10 +119,105 @@ func TestAWS(t *testing.T) { require.NoError(t, err) }() - coll := client.Database("aws").Collection("test") + awsFindOne(t, client) +} - err = coll.FindOne(context.Background(), bson.D{{Key: "x", Value: 1}}).Err() - if err != nil && !errors.Is(err, mongo.ErrNoDocuments) { - t.Logf("FindOne error: %v", err) +// Custom Credential Provider Authenticates +// +// Scenarios where MONGODB_URI contains no inline credentials (EC2, ECS, WebIdentity) +// are skipped because a valid custom provider for those environments requires the AWS +// SDK, which is not available in this module. +func TestAWSCustomCredentialProviderAuthenticates(t *testing.T) { + if os.Getenv("AWS_TEST") == "" { + t.Skip("Skipping test: AWS_TEST environment variable is not set") + } + + rawURI := os.Getenv("MONGODB_URI") + if rawURI == "" { + t.Skip("MONGODB_URI not set") + } + + // Extract inline credentials from the URI, if any. For scenarios where the CI + // tooling embeds credentials in the URI (Regular, AssumeRole, Lambda), we return + // them directly from the custom provider rather than using the AWS SDK default + // provider, per the spec allowance. + parsedOpts := options.Client().ApplyURI(rawURI) + if parsedOpts.Auth == nil || parsedOpts.Auth.Username == "" { + t.Skip("no inline credentials in MONGODB_URI; scenario requires AWS SDK default provider") + } + requireAWSAuthSupport(t, rawURI) + + creds := struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + Source string + CanExpire bool + Expires time.Time + AccountID string + }{ + AccessKeyID: parsedOpts.Auth.Username, + SecretAccessKey: parsedOpts.Auth.Password, + } + if parsedOpts.Auth.AuthMechanismProperties != nil { + creds.SessionToken = parsedOpts.Auth.AuthMechanismProperties["AWS_SESSION_TOKEN"] + } + + tracking := &trackingAWSProvider{inner: &staticAWSProvider{creds: creds}} + + client, err := mongo.Connect( + options.Client(). + ApplyURI(rawURI). + SetAuth(options.Credential{ + AuthMechanism: "MONGODB-AWS", + AWSCredentialsProvider: tracking, + }), + ) + require.NoError(t, err) + defer func() { require.NoError(t, client.Disconnect(context.Background())) }() + + awsFindOne(t, client) + assert.Greater(t, tracking.called, 0, "expected custom credential provider to be called at least once") +} + +// Custom Credential Provider Authentication Precedence — Custom Provider Takes +// Precedence Over Environment Variables +func TestAWSCustomCredentialProviderPrecedence(t *testing.T) { + if os.Getenv("AWS_TEST") == "" { + t.Skip("Skipping test: AWS_TEST environment variable is not set") } + + t.Setenv("AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID") + t.Setenv("AWS_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY") + + rawURI := os.Getenv("MONGODB_URI") + if rawURI == "" { + t.Skip("MONGODB_URI not set") + } + // Ensure the URI targets a server configured for MONGODB-AWS; without this + // guard the test runs (and fails) on any 4.4+ server where AWS env vars + // happen to be set in the developer's shell. + if uriOpts := options.Client().ApplyURI(rawURI); uriOpts.Auth == nil || + uriOpts.Auth.AuthMechanism != "MONGODB-AWS" { + t.Skip("MONGODB_URI does not specify MONGODB-AWS authentication mechanism") + } + requireAWSAuthSupport(t, rawURI) + + tracking := &trackingAWSProvider{inner: &staticAWSProvider{}} + + // SetAuth overrides any inline credentials that ApplyURI may have parsed from the + // URI, so the chain is: static(empty) -> custom provider -> env-var provider. + client, err := mongo.Connect( + options.Client(). + ApplyURI(rawURI). + SetAuth(options.Credential{ + AuthMechanism: "MONGODB-AWS", + AWSCredentialsProvider: tracking, + }), + ) + require.NoError(t, err) + defer func() { require.NoError(t, client.Disconnect(context.Background())) }() + + _ = client.Database("aws").Collection("test").FindOne(context.Background(), bson.D{{Key: "x", Value: 1}}) + assert.Greater(t, tracking.called, 0, "expected custom credential provider to be called at least once") } diff --git a/mongo/client.go b/mongo/client.go index 14b65b68bc..5642154e38 100644 --- a/mongo/client.go +++ b/mongo/client.go @@ -17,6 +17,7 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" + "go.mongodb.org/mongo-driver/v2/internal/credutil" "go.mongodb.org/mongo-driver/v2/internal/httputil" "go.mongodb.org/mongo-driver/v2/internal/logger" "go.mongodb.org/mongo-driver/v2/internal/mongoutil" @@ -663,7 +664,7 @@ func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt bypassAutoEncryption := opts.BypassAutoEncryption != nil && *opts.BypassAutoEncryption bypassQueryAnalysis := opts.BypassQueryAnalysis != nil && *opts.BypassQueryAnalysis - mc, err := mongocrypt.NewMongoCrypt(&mcopts.MongoCryptOptions{ + cryptOpts := &mcopts.MongoCryptOptions{ KmsProviders: kmsProviders, LocalSchemaMap: cryptSchemaMap, BypassQueryAnalysis: bypassQueryAnalysis, @@ -672,7 +673,11 @@ func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt CryptSharedLibOverridePath: cryptSharedLibPath, HTTPClient: opts.HTTPClient, KeyExpiration: opts.KeyExpiration, - }) + } + if opts.AWSCredentialsProvider != nil { + cryptOpts.AWSCredentialsProvider = credutil.AWSOptionsProvider{Provider: opts.AWSCredentialsProvider} + } + mc, err := mongocrypt.NewMongoCrypt(cryptOpts) if err != nil { return nil, err } diff --git a/mongo/client_encryption.go b/mongo/client_encryption.go index 67b015476c..e53c01e02f 100644 --- a/mongo/client_encryption.go +++ b/mongo/client_encryption.go @@ -14,6 +14,7 @@ import ( "strings" "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/internal/credutil" "go.mongodb.org/mongo-driver/v2/internal/mongoutil" "go.mongodb.org/mongo-driver/v2/mongo/options" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" @@ -53,7 +54,7 @@ func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options. return nil, fmt.Errorf("error creating KMS providers map: %w", err) } - mc, err := mongocrypt.NewMongoCrypt(&mcopts.MongoCryptOptions{ + cryptOpts := &mcopts.MongoCryptOptions{ KmsProviders: kmsProviders, // Explicitly disable loading the crypt_shared library for the Crypt used for // ClientEncryption because it's only needed for AutoEncryption and we don't expect users to @@ -61,7 +62,11 @@ func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options. CryptSharedLibDisabled: true, HTTPClient: cea.HTTPClient, KeyExpiration: cea.KeyExpiration, - }) + } + if cea.AWSCredentialsProvider != nil { + cryptOpts.AWSCredentialsProvider = credutil.AWSOptionsProvider{Provider: cea.AWSCredentialsProvider} + } + mc, err := mongocrypt.NewMongoCrypt(cryptOpts) if err != nil { return nil, err } diff --git a/mongo/client_examples_test.go b/mongo/client_examples_test.go index 173ea48605..1acd19d602 100644 --- a/mongo/client_examples_test.go +++ b/mongo/client_examples_test.go @@ -246,6 +246,22 @@ func ExampleConnect_kerberos() { _ = client } +type awsCredentialsProvider struct { + accessKeyID string + secretAccessKey string + sessionToken string +} + +func (a *awsCredentialsProvider) Retrieve(_ context.Context) ( + options.AWSCredentials, error, +) { + return options.AWSCredentials{ + AccessKeyID: a.accessKeyID, + SecretAccessKey: a.secretAccessKey, + SessionToken: a.sessionToken, + }, nil +} + func ExampleConnect_aWS() { // Configure a Client with authentication using the MONGODB-AWS // authentication mechanism. Credentials for this mechanism can come from @@ -267,10 +283,11 @@ func ExampleConnect_aWS() { // The order in which the driver searches for credentials is: // // 1. Credentials passed through the URI - // 2. Environment variables - // 3. ECS endpoint if and only if AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is + // 2. Custom AWS credential provider + // 3. Environment variables + // 4. ECS endpoint if and only if AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is // set - // 4. EC2 endpoint + // 5. EC2 endpoint // // The following examples set the appropriate credentials via the // ClientOptions.SetAuth method. All of these credentials can be specified @@ -352,6 +369,27 @@ func ExampleConnect_aWS() { panic(err) } _ = ecClient + + // Custom AWS credential provider + + // Applications can authenticate using a custom AWS credential provider as + // well. + awsCredentialProvider := &awsCredentialsProvider{ + accessKeyID: accessKeyID, + secretAccessKey: secretAccessKey, + sessionToken: sessionToken, + } + + credential := options.Credential{ + AuthMechanism: "MONGODB-AWS", + AWSCredentialsProvider: awsCredentialProvider, + } + awsClient, err := mongo.Connect( + options.Client().SetAuth(credential)) + if err != nil { + panic(err) + } + _ = awsClient } func ExampleConnect_stableAPI() { diff --git a/mongo/options/autoencryptionoptions.go b/mongo/options/autoencryptionoptions.go index db3508fb4f..69d6821fe1 100644 --- a/mongo/options/autoencryptionoptions.go +++ b/mongo/options/autoencryptionoptions.go @@ -31,17 +31,18 @@ import ( // // See corresponding setter methods for documentation. type AutoEncryptionOptions struct { - KeyVaultClientOptions *ClientOptions - KeyVaultNamespace string - KmsProviders map[string]map[string]any - SchemaMap map[string]any - BypassAutoEncryption *bool - ExtraOptions map[string]any - TLSConfig map[string]*tls.Config - HTTPClient *http.Client - EncryptedFieldsMap map[string]any - BypassQueryAnalysis *bool - KeyExpiration *time.Duration + KeyVaultClientOptions *ClientOptions + KeyVaultNamespace string + KmsProviders map[string]map[string]any + SchemaMap map[string]any + BypassAutoEncryption *bool + ExtraOptions map[string]any + TLSConfig map[string]*tls.Config + HTTPClient *http.Client + EncryptedFieldsMap map[string]any + BypassQueryAnalysis *bool + KeyExpiration *time.Duration + AWSCredentialsProvider AWSCredentialsProvider } // AutoEncryption creates a new AutoEncryptionOptions configured with default values. @@ -174,3 +175,9 @@ func (a *AutoEncryptionOptions) SetKeyExpiration(expiration time.Duration) *Auto return a } + +// SetAWSCredentialsProvider specifies options for custom AWS credential provider. +func (a *AutoEncryptionOptions) SetAWSCredentialsProvider(provider AWSCredentialsProvider) *AutoEncryptionOptions { + a.AWSCredentialsProvider = provider + return a +} diff --git a/mongo/options/clientencryptionoptions.go b/mongo/options/clientencryptionoptions.go index a6c477a7a9..76944be650 100644 --- a/mongo/options/clientencryptionoptions.go +++ b/mongo/options/clientencryptionoptions.go @@ -19,11 +19,12 @@ import ( // // See corresponding setter methods for documentation. type ClientEncryptionOptions struct { - KeyVaultNamespace string - KmsProviders map[string]map[string]any - TLSConfig map[string]*tls.Config - HTTPClient *http.Client - KeyExpiration *time.Duration + KeyVaultNamespace string + KmsProviders map[string]map[string]any + TLSConfig map[string]*tls.Config + HTTPClient *http.Client + KeyExpiration *time.Duration + AWSCredentialsProvider AWSCredentialsProvider } // ClientEncryptionOptionsBuilder contains options to configure client @@ -94,6 +95,15 @@ func (c *ClientEncryptionOptionsBuilder) SetKeyExpiration(expiration time.Durati return c } +// SetAWSCredentialsProvider specifies options for custom AWS credential provider. +func (c *ClientEncryptionOptionsBuilder) SetAWSCredentialsProvider(provider AWSCredentialsProvider) *ClientEncryptionOptionsBuilder { + c.Opts = append(c.Opts, func(opts *ClientEncryptionOptions) error { + opts.AWSCredentialsProvider = provider + return nil + }) + return c +} + // BuildTLSConfig specifies tls.Config options for each KMS provider to use to configure TLS on all connections created // to the KMS provider. The input map should contain a mapping from each KMS provider to a document containing the necessary // options, as follows: diff --git a/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index 882d1162b2..d2432e75e3 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -116,6 +116,7 @@ type Credential struct { PasswordSet bool OIDCMachineCallback OIDCCallback OIDCHumanCallback OIDCCallback + AWSCredentialsProvider AWSCredentialsProvider } // OIDCCallback is the type for both Human and Machine Callback flows. @@ -136,6 +137,22 @@ type OIDCCredential struct { RefreshToken *string } +// AWSCredentialsProvider is the interface used to retrieve AWS credentials. +type AWSCredentialsProvider interface { + Retrieve(ctx context.Context) (AWSCredentials, error) +} + +// AWSCredentials represents AWS credentials. +type AWSCredentials = struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + Source string + CanExpire bool + Expires time.Time + AccountID string +} + // IDPInfo contains the information needed to perform OIDC authentication with // an Identity Provider. type IDPInfo struct { diff --git a/x/mongo/driver/auth/aws_conv.go b/x/mongo/driver/auth/aws_sasl_adapter.go similarity index 64% rename from x/mongo/driver/auth/aws_conv.go rename to x/mongo/driver/auth/aws_sasl_adapter.go index 06dd04376c..3e97009ec7 100644 --- a/x/mongo/driver/auth/aws_conv.go +++ b/x/mongo/driver/auth/aws_sasl_adapter.go @@ -18,7 +18,6 @@ import ( "time" "go.mongodb.org/mongo-driver/v2/bson" - "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" v4signer "go.mongodb.org/mongo-driver/v2/internal/aws/signer/v4" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" ) @@ -32,59 +31,63 @@ const ( clientDone ) -type awsConversation struct { - state clientState - valid bool - nonce []byte - credentials *credentials.Credentials +type awsSaslAdapter struct { + signer *v4signer.Signer + + state clientState + nonce []byte +} + +var _ SaslClient = (*awsSaslAdapter)(nil) + +func (a *awsSaslAdapter) Start() (string, []byte, error) { + step, err := a.step(nil) + if err != nil { + return MongoDBAWS, nil, err + } + return MongoDBAWS, step, nil +} + +func (a *awsSaslAdapter) Next(_ context.Context, challenge []byte) ([]byte, error) { + step, err := a.step(challenge) + if err != nil { + return nil, err + } + return step, nil } -type serverMessage struct { - Nonce bson.Binary `bson:"s"` - Host string `bson:"h"` +func (a *awsSaslAdapter) Completed() bool { + return a.state == clientDone } const ( amzDateFormat = "20060102T150405Z" defaultRegion = "us-east-1" maxHostLength = 255 - responceNonceLength = 64 + responseNonceLength = 64 ) -// Step takes a string provided from a server (or just an empty string for the +// step takes a string provided from a server (or just an empty string for the // very first conversation step) and attempts to move the authentication // conversation forward. It returns a string to be sent to the server or an // error if the server message is invalid. Calling Step after a conversation // completes is also an error. -func (ac *awsConversation) Step(challenge []byte) (response []byte, err error) { - switch ac.state { +func (a *awsSaslAdapter) step(challenge []byte) (response []byte, err error) { + switch a.state { case clientStarting: - ac.state = clientFirst - response = ac.firstMsg() + a.state = clientFirst + response = a.firstMsg() case clientFirst: - ac.state = clientFinal - response, err = ac.finalMsg(challenge) + a.state = clientFinal + response, err = a.finalMsg(challenge) case clientFinal: - ac.state = clientDone - ac.valid = true + a.state = clientDone default: response, err = nil, errors.New("conversation already completed") } return } -// Done returns true if the conversation is completed or has errored. -func (ac *awsConversation) Done() bool { - return ac.state == clientDone -} - -// Valid returns true if the conversation successfully authenticated with the -// server, including counter-validation that the server actually has the -// user's stored credentials. -func (ac *awsConversation) Valid() bool { - return ac.valid -} - func getRegion(host string) (string, error) { region := defaultRegion @@ -111,20 +114,23 @@ func getRegion(host string) (string, error) { return region, nil } -func (ac *awsConversation) firstMsg() []byte { +func (a *awsSaslAdapter) firstMsg() []byte { // Values are cached for use in final message parameters - ac.nonce = make([]byte, 32) - _, _ = rand.Read(ac.nonce) + a.nonce = make([]byte, 32) + _, _ = rand.Read(a.nonce) idx, msg := bsoncore.AppendDocumentStart(nil) msg = bsoncore.AppendInt32Element(msg, "p", 110) - msg = bsoncore.AppendBinaryElement(msg, "r", 0x00, ac.nonce) + msg = bsoncore.AppendBinaryElement(msg, "r", 0x00, a.nonce) msg, _ = bsoncore.AppendDocumentEnd(msg, idx) return msg } -func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) { - var sm serverMessage +func (a *awsSaslAdapter) finalMsg(s1 []byte) ([]byte, error) { + var sm struct { + Nonce bson.Binary `bson:"s"` + Host string `bson:"h"` + } err := bson.Unmarshal(s1, &sm) if err != nil { return nil, err @@ -134,10 +140,10 @@ func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) { if sm.Nonce.Subtype != 0x00 { return nil, errors.New("server reply contained unexpected binary subtype") } - if len(sm.Nonce.Data) != responceNonceLength { - return nil, fmt.Errorf("server reply nonce was not %v bytes", responceNonceLength) + if len(sm.Nonce.Data) != responseNonceLength { + return nil, fmt.Errorf("server reply nonce was not %v bytes", responseNonceLength) } - if !bytes.HasPrefix(sm.Nonce.Data, ac.nonce) { + if !bytes.HasPrefix(sm.Nonce.Data, a.nonce) { return nil, errors.New("server nonce did not extend client nonce") } @@ -146,31 +152,23 @@ func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) { return nil, err } - creds, err := ac.credentials.GetWithContext(context.Background()) - if err != nil { - return nil, err - } - currentTime := time.Now().UTC() body := "Action=GetCallerIdentity&Version=2011-06-15" // Create http.Request - req, _ := http.NewRequest("POST", "/", strings.NewReader(body)) + req, err := http.NewRequest("POST", "/", strings.NewReader(body)) + if err != nil { + return nil, err + } + req.Host = sm.Host req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Length", "43") - req.Host = sm.Host req.Header.Set("X-Amz-Date", currentTime.Format(amzDateFormat)) - if len(creds.SessionToken) > 0 { - req.Header.Set("X-Amz-Security-Token", creds.SessionToken) - } req.Header.Set("X-MongoDB-Server-Nonce", base64.StdEncoding.EncodeToString(sm.Nonce.Data)) req.Header.Set("X-MongoDB-GS2-CB-Flag", "n") - // Create signer with credentials - signer := v4signer.NewSigner(ac.credentials) - // Get signed header - _, err = signer.Sign(req, strings.NewReader(body), "sts", region, currentTime) + _, err = a.signer.Sign(req, strings.NewReader(body), "sts", region, currentTime) if err != nil { return nil, err } @@ -179,8 +177,8 @@ func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) { idx, msg := bsoncore.AppendDocumentStart(nil) msg = bsoncore.AppendStringElement(msg, "a", req.Header.Get("Authorization")) msg = bsoncore.AppendStringElement(msg, "d", req.Header.Get("X-Amz-Date")) - if len(creds.SessionToken) > 0 { - msg = bsoncore.AppendStringElement(msg, "t", creds.SessionToken) + if sessionToken := req.Header.Get("X-Amz-Security-Token"); len(sessionToken) > 0 { + msg = bsoncore.AppendStringElement(msg, "t", sessionToken) } msg, _ = bsoncore.AppendDocumentEnd(msg, idx) diff --git a/x/mongo/driver/auth/creds/awscreds.go b/x/mongo/driver/auth/creds/awscreds.go index 36595c2090..c156031a00 100644 --- a/x/mongo/driver/auth/creds/awscreds.go +++ b/x/mongo/driver/auth/creds/awscreds.go @@ -44,7 +44,7 @@ func NewAWSCredentialProvider(httpClient *http.Client, providers ...credentials. // GetCredentialsDoc generates AWS credentials. func (p AWSCredentialProvider) GetCredentialsDoc(ctx context.Context) (bsoncore.Document, error) { - creds, err := p.Cred.GetWithContext(ctx) + creds, err := p.Cred.Get(ctx) if err != nil { return nil, err } diff --git a/x/mongo/driver/auth/creds/azurecreds.go b/x/mongo/driver/auth/creds/azurecreds.go index c6d4b473af..d2f16ef982 100644 --- a/x/mongo/driver/auth/creds/azurecreds.go +++ b/x/mongo/driver/auth/creds/azurecreds.go @@ -30,7 +30,7 @@ func NewAzureCredentialProvider(httpClient *http.Client) AzureCredentialProvider // GetCredentialsDoc generates Azure credentials. func (p AzureCredentialProvider) GetCredentialsDoc(ctx context.Context) (bsoncore.Document, error) { - creds, err := p.cred.GetWithContext(ctx) + creds, err := p.cred.Get(ctx) if err != nil { return nil, err } diff --git a/x/mongo/driver/auth/creds/credscaching_test.go b/x/mongo/driver/auth/creds/credscaching_test.go index ad464fe7ed..b7a4f0d9b3 100644 --- a/x/mongo/driver/auth/creds/credscaching_test.go +++ b/x/mongo/driver/auth/creds/credscaching_test.go @@ -53,20 +53,30 @@ func TestAWSCredentialProviderCaching(t *testing.T) { t.Setenv(urienv, testEndpoint) testCases := []struct { - expiration time.Duration - reqCount uint32 + expiration time.Duration + reqCount uint32 + assertError func(t assert.TestingT, err error) bool }{ { expiration: 20 * time.Minute, reqCount: 1, + assertError: func(t assert.TestingT, err error) bool { + return assert.NoError(t, err, "error in GetCredentialsDoc") + }, }, { expiration: 5 * time.Minute, reqCount: 2, + assertError: func(t assert.TestingT, err error) bool { + return assert.ErrorContains(t, err, "credentials are invalid") + }, }, { expiration: -1 * time.Minute, reqCount: 2, + assertError: func(t assert.TestingT, err error) bool { + return assert.ErrorContains(t, err, "credentials are invalid") + }, }, } for _, tc := range testCases { @@ -108,9 +118,9 @@ func TestAWSCredentialProviderCaching(t *testing.T) { p := AWSCredentialProvider{credentials.NewChainCredentials([]credentials.Provider{env, ecs})} var err error _, err = p.GetCredentialsDoc(context.Background()) - assert.NoError(t, err, "error in GetCredentialsDoc") + tc.assertError(t, err) _, err = p.GetCredentialsDoc(context.Background()) - assert.NoError(t, err, "error in GetCredentialsDoc") + tc.assertError(t, err) assert.Equal(t, tc.reqCount, atomic.LoadUint32(&cnt), "expected and actual credentials retrieval count don't match") }) } diff --git a/x/mongo/driver/auth/mongodbaws.go b/x/mongo/driver/auth/mongodbaws.go index dd9661e1a9..2f890d84c3 100644 --- a/x/mongo/driver/auth/mongodbaws.go +++ b/x/mongo/driver/auth/mongodbaws.go @@ -12,6 +12,7 @@ import ( "net/http" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" + v4signer "go.mongodb.org/mongo-driver/v2/internal/aws/signer/v4" "go.mongodb.org/mongo-driver/v2/internal/credproviders" "go.mongodb.org/mongo-driver/v2/x/mongo/driver" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/auth/creds" @@ -24,36 +25,40 @@ func newMongoDBAWSAuthenticator(cred *Cred, httpClient *http.Client) (Authentica if cred.Source != "" && cred.Source != sourceExternal { return nil, newAuthError("MONGODB-AWS source must be empty or $external", nil) } + if httpClient == nil { return nil, errors.New("httpClient must not be nil") } - return &MongoDBAWSAuthenticator{ - credentials: &credproviders.StaticProvider{ + + providers := []credentials.Provider{ + &credproviders.StaticProvider{ Value: credentials.Value{ AccessKeyID: cred.Username, SecretAccessKey: cred.Password, SessionToken: cred.Props["AWS_SESSION_TOKEN"], }, }, - httpClient: httpClient, + } + if cred.AWSCredentialsProvider != nil { + providers = append(providers, cred.AWSCredentialsProvider) + } + + return &MongoDBAWSAuthenticator{ + credentials: creds.NewAWSCredentialProvider(httpClient, providers...).Cred, }, nil } // MongoDBAWSAuthenticator uses AWS-IAM credentials over SASL to authenticate a connection. type MongoDBAWSAuthenticator struct { - credentials *credproviders.StaticProvider - httpClient *http.Client + credentials *credentials.Credentials } // Auth authenticates the connection. func (a *MongoDBAWSAuthenticator) Auth(ctx context.Context, cfg *driver.AuthConfig) error { - providers := creds.NewAWSCredentialProvider(a.httpClient, a.credentials) - adapter := &awsSaslAdapter{ - conversation: &awsConversation{ - credentials: providers.Cred, - }, + awsSasl := &awsSaslAdapter{ + signer: v4signer.NewSigner(a.credentials), } - err := ConductSaslConversation(ctx, cfg, sourceExternal, adapter) + err := ConductSaslConversation(ctx, cfg, sourceExternal, awsSasl) if err != nil { return newAuthError("sasl conversation error", err) } @@ -64,29 +69,3 @@ func (a *MongoDBAWSAuthenticator) Auth(ctx context.Context, cfg *driver.AuthConf func (a *MongoDBAWSAuthenticator) Reauth(_ context.Context, _ *driver.AuthConfig) error { return newAuthError("AWS authentication does not support reauthentication", nil) } - -type awsSaslAdapter struct { - conversation *awsConversation -} - -var _ SaslClient = (*awsSaslAdapter)(nil) - -func (a *awsSaslAdapter) Start() (string, []byte, error) { - step, err := a.conversation.Step(nil) - if err != nil { - return MongoDBAWS, nil, err - } - return MongoDBAWS, step, nil -} - -func (a *awsSaslAdapter) Next(_ context.Context, challenge []byte) ([]byte, error) { - step, err := a.conversation.Step(challenge) - if err != nil { - return nil, err - } - return step, nil -} - -func (a *awsSaslAdapter) Completed() bool { - return a.conversation.Done() -} diff --git a/x/mongo/driver/auth/mongodbaws_test.go b/x/mongo/driver/auth/mongodbaws_test.go index ea33124b55..74d6b05e33 100644 --- a/x/mongo/driver/auth/mongodbaws_test.go +++ b/x/mongo/driver/auth/mongodbaws_test.go @@ -7,10 +7,21 @@ package auth import ( + "context" "errors" + "net/http" "testing" + "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/internal/assert" + "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" + "go.mongodb.org/mongo-driver/v2/internal/require" + "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" + "go.mongodb.org/mongo-driver/v2/x/mongo/driver" + "go.mongodb.org/mongo-driver/v2/x/mongo/driver/description" + "go.mongodb.org/mongo-driver/v2/x/mongo/driver/drivertest" + "go.mongodb.org/mongo-driver/v2/x/mongo/driver/mnet" + "go.mongodb.org/mongo-driver/v2/x/mongo/driver/wiremessage" ) func TestGetRegion(t *testing.T) { @@ -45,3 +56,156 @@ func TestGetRegion(t *testing.T) { }) } } + +type testAWSCredentialsProvider struct { + cnt int +} + +func (a *testAWSCredentialsProvider) Retrieve(_ context.Context) (credentials.Value, error) { + a.cnt++ + return credentials.Value{}, nil +} + +func TestAWSCustomCredentialsProvider(t *testing.T) { + t.Setenv("AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID") + t.Setenv("AWS_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY") + + provider := &testAWSCredentialsProvider{} + for _, tc := range []struct { + name string + cred *Cred + cnt int + }{ + { + name: "provider with cred", + cred: &Cred{ + Username: "user", + Password: "pass", + Props: map[string]string{"AWS_SESSION_TOKEN": "token"}, + AWSCredentialsProvider: provider, + }, + cnt: 0, + }, + { + name: "provider with empty cred", + cred: &Cred{ + AWSCredentialsProvider: provider, + }, + cnt: 1, + }, + } { + provider.cnt = 0 + t.Run(tc.name, func(t *testing.T) { + authenticator, err := newMongoDBAWSAuthenticator( + tc.cred, + &http.Client{}, + ) + require.NoErrorf(t, err, "unexpected error %v", err) + + resps := make(chan []byte, 1) + written := make(chan []byte, 1) + var readErr chan error + go func() { + for { + processWm(resps, written, readErr) + } + }() + + desc := description.Server{ + WireVersion: &description.VersionRange{ + Max: 6, + }, + } + c := &drivertest.ChannelConn{ + Written: written, + ReadResp: resps, + ReadErr: readErr, + Desc: desc, + } + + mnetconn := mnet.NewConnection(c) + + err = authenticator.Auth(context.Background(), &driver.AuthConfig{Connection: mnetconn}) + assert.NoErrorf(t, err, "expected no error but got %v", err) + assert.Equalf(t, tc.cnt, provider.cnt, "expected provider to be called %v times but got %v", tc.cnt, provider.cnt) + }) + } +} + +func processWm(resps, written chan []byte, errChan chan error) { + buf := <-written + buf, ok := extractPayload(buf) + if !ok { + errChan <- errors.New("could not extract payload from message") + } + var p struct { + Payload bson.Binary `bson:"payload"` + } + err := bson.Unmarshal(buf, &p) + if err != nil { + errChan <- err + } + if p.Payload.Subtype != 0x00 { + errChan <- errors.New("unexpected payload subtype") + } + var n struct { + Nonce bson.Binary `bson:"r"` + } + err = bson.Unmarshal(p.Payload.Data, &n) + if err != nil { + errChan <- err + } + if n.Nonce.Subtype != 0x00 { + errChan <- errors.New("unexpected nonce subtype") + } + nonce := make([]byte, 64) + copy(nonce, n.Nonce.Data) + + writeReplies(resps, + bsoncore.BuildDocumentFromElements(nil, + bsoncore.AppendInt32Element(nil, "ok", 1), + bsoncore.AppendInt32Element(nil, "conversationId", 1), + bsoncore.AppendBinaryElement(nil, "payload", 0x00, bsoncore.BuildDocumentFromElements(nil, + bsoncore.AppendBinaryElement(nil, "s", n.Nonce.Subtype, nonce), + bsoncore.AppendStringElement(nil, "h", "region"), + )), + bsoncore.AppendBooleanElement(nil, "done", true), + ), + ) +} + +func extractPayload(wm []byte) (bsoncore.Document, bool) { + _, _, _, opcode, wm, ok := wiremessage.ReadHeader(wm) + if !ok { + return nil, ok + } + if opcode != wiremessage.OpMsg { + return nil, false + } + var actualPayload bsoncore.Document + _, wm, ok = wiremessage.ReadMsgFlags(wm) + if !ok { + return nil, ok + } + for loop := true; loop; { + var stype wiremessage.SectionType + stype, wm, ok = wiremessage.ReadMsgSectionType(wm) + if !ok { + return nil, ok + } + switch stype { + case wiremessage.DocumentSequence: + _, _, wm, ok = wiremessage.ReadMsgSectionDocumentSequence(wm) + if !ok { + return nil, ok + } + case wiremessage.SingleDocument: + actualPayload, _, ok = wiremessage.ReadMsgSectionSingleDocument(wm) + if !ok { + return nil, ok + } + loop = false + } + } + return actualPayload, true +} diff --git a/x/mongo/driver/auth/sasl.go b/x/mongo/driver/auth/sasl.go index 659fe45e2d..8aef5c0b08 100644 --- a/x/mongo/driver/auth/sasl.go +++ b/x/mongo/driver/auth/sasl.go @@ -134,7 +134,7 @@ func (sc *saslConversation) Finish(ctx context.Context, cfg *driver.AuthConfig, ) saslContinueCmd := operation.NewCommand(doc). Database(sc.source). - Deployment(driver.SingleConnectionDeployment{cfg.Connection}). + Deployment(driver.SingleConnectionDeployment{C: cfg.Connection}). ClusterClock(cfg.ClusterClock). ServerAPI(cfg.ServerAPI) @@ -162,7 +162,7 @@ func ConductSaslConversation(ctx context.Context, cfg *driver.AuthConfig, authSo } saslStartCmd := operation.NewCommand(saslStartDoc). Database(authSource). - Deployment(driver.SingleConnectionDeployment{cfg.Connection}). + Deployment(driver.SingleConnectionDeployment{C: cfg.Connection}). ClusterClock(cfg.ClusterClock). ServerAPI(cfg.ServerAPI) if err := saslStartCmd.Execute(ctx); err != nil { diff --git a/x/mongo/driver/driver.go b/x/mongo/driver/driver.go index 49ce715caa..fa55dd6a72 100644 --- a/x/mongo/driver/driver.go +++ b/x/mongo/driver/driver.go @@ -17,6 +17,7 @@ import ( "context" "time" + "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" "go.mongodb.org/mongo-driver/v2/internal/csot" "go.mongodb.org/mongo-driver/v2/mongo/address" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" @@ -73,13 +74,19 @@ type Authenticator interface { // Cred is a user's credential. type Cred struct { - Source string - Username string - Password string - PasswordSet bool - Props map[string]string - OIDCMachineCallback OIDCCallback - OIDCHumanCallback OIDCCallback + Source string + Username string + Password string + PasswordSet bool + Props map[string]string + OIDCMachineCallback OIDCCallback + OIDCHumanCallback OIDCCallback + AWSCredentialsProvider AWSCredentialsProvider +} + +// AWSCredentialsProvider is the interface used to retrieve AWS credentials. +type AWSCredentialsProvider interface { + Retrieve(ctx context.Context) (credentials.Value, error) } // Deployment is implemented by types that can select a server from a deployment. diff --git a/x/mongo/driver/mongocrypt/mongocrypt.go b/x/mongo/driver/mongocrypt/mongocrypt.go index 506d92beda..37a3a70648 100644 --- a/x/mongo/driver/mongocrypt/mongocrypt.go +++ b/x/mongo/driver/mongocrypt/mongocrypt.go @@ -24,6 +24,7 @@ import ( "unsafe" "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" "go.mongodb.org/mongo-driver/v2/internal/httputil" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/auth/creds" @@ -63,8 +64,16 @@ func NewMongoCrypt(opts *options.MongoCryptOptions) (*MongoCrypt, error) { if needsKmsProvider(opts.KmsProviders, "gcp") { kmsProviders["gcp"] = creds.NewGCPCredentialProvider(httpClient) } + awsCredentialsProvider := opts.AWSCredentialsProvider if needsKmsProvider(opts.KmsProviders, "aws") { - kmsProviders["aws"] = creds.NewAWSCredentialProvider(httpClient) + var providers []credentials.Provider + if awsCredentialsProvider != nil { + providers = append(providers, awsCredentialsProvider) + } + kmsProviders["aws"] = creds.NewAWSCredentialProvider(httpClient, providers...) + } else if awsCredentialsProvider != nil { + return nil, fmt.Errorf("can only provide a custom AWS credential provider " + + "when the state machine is configured for automatic AWS credential fetching") } if needsKmsProvider(opts.KmsProviders, "azure") { kmsProviders["azure"] = creds.NewAzureCredentialProvider(httpClient) diff --git a/x/mongo/driver/mongocrypt/options/mongocrypt_options.go b/x/mongo/driver/mongocrypt/options/mongocrypt_options.go index d97c535ed3..8daacad2af 100644 --- a/x/mongo/driver/mongocrypt/options/mongocrypt_options.go +++ b/x/mongo/driver/mongocrypt/options/mongocrypt_options.go @@ -10,6 +10,7 @@ import ( "net/http" "time" + "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" ) @@ -23,4 +24,5 @@ type MongoCryptOptions struct { CryptSharedLibOverridePath string HTTPClient *http.Client KeyExpiration *time.Duration + AWSCredentialsProvider credentials.Provider } diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index d65fdf56aa..0eb276c998 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -15,6 +15,7 @@ import ( "time" "go.mongodb.org/mongo-driver/v2/event" + "go.mongodb.org/mongo-driver/v2/internal/credutil" "go.mongodb.org/mongo-driver/v2/internal/logger" "go.mongodb.org/mongo-driver/v2/internal/optionsutil" "go.mongodb.org/mongo-driver/v2/mongo/options" @@ -94,36 +95,44 @@ func convertOIDCArgs(args *driver.OIDCArgs) *options.OIDCArgs { // ConvertCreds takes an [options.Credential] and returns the equivalent // [driver.Cred]. -func ConvertCreds(cred *options.Credential) *driver.Cred { - if cred == nil { +func ConvertCreds(credOpts *options.Credential) *driver.Cred { + if credOpts == nil { return nil } var oidcMachineCallback auth.OIDCCallback - if cred.OIDCMachineCallback != nil { + if credOpts.OIDCMachineCallback != nil { oidcMachineCallback = func(ctx context.Context, args *driver.OIDCArgs) (*driver.OIDCCredential, error) { - cred, err := cred.OIDCMachineCallback(ctx, convertOIDCArgs(args)) - return (*driver.OIDCCredential)(cred), err + credOpts, err := credOpts.OIDCMachineCallback(ctx, convertOIDCArgs(args)) + return (*driver.OIDCCredential)(credOpts), err } } var oidcHumanCallback auth.OIDCCallback - if cred.OIDCHumanCallback != nil { + if credOpts.OIDCHumanCallback != nil { oidcHumanCallback = func(ctx context.Context, args *driver.OIDCArgs) (*driver.OIDCCredential, error) { - cred, err := cred.OIDCHumanCallback(ctx, convertOIDCArgs(args)) - return (*driver.OIDCCredential)(cred), err + credOpts, err := credOpts.OIDCHumanCallback(ctx, convertOIDCArgs(args)) + return (*driver.OIDCCredential)(credOpts), err } } - return &auth.Cred{ - Source: cred.AuthSource, - Username: cred.Username, - Password: cred.Password, - PasswordSet: cred.PasswordSet, - Props: cred.AuthMechanismProperties, + cred := &auth.Cred{ + Source: credOpts.AuthSource, + Username: credOpts.Username, + Password: credOpts.Password, + PasswordSet: credOpts.PasswordSet, + Props: credOpts.AuthMechanismProperties, OIDCMachineCallback: oidcMachineCallback, OIDCHumanCallback: oidcHumanCallback, } + + if credOpts.AWSCredentialsProvider != nil { + cred.AWSCredentialsProvider = credutil.AWSOptionsProvider{ + Provider: credOpts.AWSCredentialsProvider, + } + } + + return cred } // NewConfig will translate data from client options into a topology config for