From a95c31359462f8b6b0725aa048717756da215c54 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Mon, 27 Oct 2025 13:13:18 -0400 Subject: [PATCH 01/17] support custom AWS credential provider --- internal/aws/credentials/chain_provider.go | 6 +- .../aws/credentials/chain_provider_test.go | 17 +- internal/aws/credentials/credentials.go | 17 +- internal/aws/credentials/credentials_test.go | 8 +- internal/aws/types.go | 8 + .../credproviders/assume_role_provider.go | 9 +- internal/credproviders/aws_provider.go | 47 +++++ internal/credproviders/ec2_provider.go | 9 +- internal/credproviders/ecs_provider.go | 9 +- internal/credproviders/env_provider.go | 3 +- internal/credproviders/imds_provider.go | 9 +- internal/credproviders/static_provider.go | 3 +- .../client_side_encryption_prose_test.go | 120 +++++++++++++ mongo/client.go | 26 ++- mongo/client_encryption.go | 26 ++- mongo/client_examples_test.go | 30 +++- mongo/options/autoencryptionoptions.go | 7 + mongo/options/clientencryptionoptions.go | 20 ++- mongo/options/clientoptions.go | 8 + x/mongo/driver/auth/mongodbaws.go | 28 +-- x/mongo/driver/auth/mongodbaws_test.go | 161 ++++++++++++++++++ x/mongo/driver/driver.go | 16 +- x/mongo/driver/mongocrypt/mongocrypt.go | 18 +- .../mongocrypt/options/mongocrypt_options.go | 8 + x/mongo/driver/topology/topology_options.go | 29 +++- 25 files changed, 546 insertions(+), 96 deletions(-) create mode 100644 internal/credproviders/aws_provider.go diff --git a/internal/aws/credentials/chain_provider.go b/internal/aws/credentials/chain_provider.go index 942c7690fa..aca9ec77d9 100644 --- a/internal/aws/credentials/chain_provider.go +++ b/internal/aws/credentials/chain_provider.go @@ -11,6 +11,8 @@ package credentials import ( + "context" + "go.mongodb.org/mongo-driver/v2/internal/aws/awserr" ) @@ -45,10 +47,10 @@ func NewChainCredentials(providers []Provider) *Credentials { // // 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) { var 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 diff --git a/internal/aws/credentials/chain_provider_test.go b/internal/aws/credentials/chain_provider_test.go index 3680b78105..4514a8f122 100644 --- a/internal/aws/credentials/chain_provider_test.go +++ b/internal/aws/credentials/chain_provider_test.go @@ -11,6 +11,7 @@ package credentials import ( + "context" "reflect" "testing" @@ -23,7 +24,7 @@ type secondStubProvider struct { err error } -func (s *secondStubProvider) Retrieve() (Value, error) { +func (s *secondStubProvider) Retrieve(_ context.Context) (Value, error) { s.expired = false s.creds.ProviderName = "secondStubProvider" return s.creds, s.err @@ -54,7 +55,7 @@ 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) } @@ -90,7 +91,7 @@ func TestChainProviderGet(t *testing.T) { }, } - creds, err := p.Retrieve() + creds, err := p.Retrieve(context.Background()) if err != nil { t.Errorf("Expect no error, got %v", err) } @@ -113,10 +114,12 @@ func TestChainProviderIsExpired(t *testing.T) { }, } + ctx := context.Background() + if !p.IsExpired() { t.Errorf("Expect expired to be true before any Retrieve") } - _, err := p.Retrieve() + _, err := p.Retrieve(ctx) if err != nil { t.Errorf("Expect no error, got %v", err) } @@ -129,7 +132,7 @@ func TestChainProviderIsExpired(t *testing.T) { t.Errorf("Expect return of expired provider") } - _, err = p.Retrieve() + _, err = p.Retrieve(ctx) if err != nil { t.Errorf("Expect no error, got %v", err) } @@ -146,7 +149,7 @@ func TestChainProviderWithNoProvider(t *testing.T) { 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) } @@ -167,7 +170,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..6d46bce6ec 100644 --- a/internal/aws/credentials/credentials.go +++ b/internal/aws/credentials/credentials.go @@ -52,20 +52,13 @@ 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) + Retrieve(context.Context) (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) -} - // A Credentials provides concurrency safe retrieval of AWS credentials Value. // // A Credentials is also used to fetch Azure credentials Value. @@ -143,13 +136,7 @@ func (c *Credentials) singleRetrieve(ctx context.Context) (interface{}, error) { return curCreds, 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() - } + creds, err := c.provider.Retrieve(ctx) if err == nil { c.creds = creds } diff --git a/internal/aws/credentials/credentials_test.go b/internal/aws/credentials/credentials_test.go index e10848c7ac..4847441264 100644 --- a/internal/aws/credentials/credentials_test.go +++ b/internal/aws/credentials/credentials_test.go @@ -33,7 +33,7 @@ type stubProvider struct { err error } -func (s *stubProvider) Retrieve() (Value, error) { +func (s *stubProvider) Retrieve(_ context.Context) (Value, error) { s.retrievedCount++ s.expired = false s.creds.ProviderName = "stubProvider" @@ -133,7 +133,7 @@ func (e *MockProvider) IsExpired() bool { return e.expiration.Before(curTime()) } -func (*MockProvider) Retrieve() (Value, error) { +func (*MockProvider) Retrieve(_ context.Context) (Value, error) { return Value{}, nil } @@ -162,9 +162,9 @@ type stubProviderConcurrent struct { 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) { diff --git a/internal/aws/types.go b/internal/aws/types.go index 52aecda76b..8ebc242733 100644 --- a/internal/aws/types.go +++ b/internal/aws/types.go @@ -14,6 +14,14 @@ import ( "io" ) +// Credentials represents AWS credentials. +type Credentials struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + ExpirationCallback func() bool +} + // ReadSeekCloser wraps a io.Reader returning a ReaderSeekerCloser. Allows the // SDK to accept an io.Reader that is not also an io.Seeker for unsigned // streaming payload API operations. diff --git a/internal/credproviders/assume_role_provider.go b/internal/credproviders/assume_role_provider.go index eec2247c70..36aa939336 100644 --- a/internal/credproviders/assume_role_provider.go +++ b/internal/credproviders/assume_role_provider.go @@ -57,8 +57,8 @@ 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} @@ -137,11 +137,6 @@ func (a *AssumeRoleProvider) RetrieveWithContext(ctx context.Context) (credentia 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/aws_provider.go b/internal/credproviders/aws_provider.go new file mode 100644 index 0000000000..62af05535c --- /dev/null +++ b/internal/credproviders/aws_provider.go @@ -0,0 +1,47 @@ +// 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 credproviders + +import ( + "context" + + "go.mongodb.org/mongo-driver/v2/internal/aws" + "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" +) + +const awsProviderName = "AwsProvider" + +// AwsProvider retrieves credentials from the given AWS credentials provider. +type AwsProvider struct { + credentials *aws.Credentials + Provider func(context.Context) (aws.Credentials, error) +} + +// Retrieve retrieves the keys from the given AWS credentials provider. +func (a *AwsProvider) Retrieve(ctx context.Context) (credentials.Value, error) { + var value credentials.Value + if a.credentials == nil { + creds, err := a.Provider(ctx) + if err != nil { + return value, err + } + a.credentials = &creds + } + value.AccessKeyID = a.credentials.AccessKeyID + value.SecretAccessKey = a.credentials.SecretAccessKey + value.SessionToken = a.credentials.SessionToken + value.ProviderName = awsProviderName + return value, nil +} + +// IsExpired returns true if the credentials have not been retrieved. +func (a *AwsProvider) IsExpired() bool { + if a.credentials == nil || a.credentials.ExpirationCallback == nil { + return true + } + return a.credentials.ExpirationCallback() +} diff --git a/internal/credproviders/ec2_provider.go b/internal/credproviders/ec2_provider.go index df15a7f2c3..3b299f894c 100644 --- a/internal/credproviders/ec2_provider.go +++ b/internal/credproviders/ec2_provider.go @@ -146,8 +146,8 @@ 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) { +// Retrieve retrieves the keys from the AWS service. +func (e *EC2Provider) Retrieve(ctx context.Context) (credentials.Value, error) { v := credentials.Value{ProviderName: ec2ProviderName} token, err := e.getToken(ctx) @@ -172,11 +172,6 @@ func (e *EC2Provider) RetrieveWithContext(ctx context.Context) (credentials.Valu 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..a292fe2ee3 100644 --- a/internal/credproviders/ecs_provider.go +++ b/internal/credproviders/ecs_provider.go @@ -49,8 +49,8 @@ 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} @@ -101,11 +101,6 @@ func (e *ECSProvider) RetrieveWithContext(ctx context.Context) (credentials.Valu 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..7c77c18b98 100644 --- a/internal/credproviders/env_provider.go +++ b/internal/credproviders/env_provider.go @@ -7,6 +7,7 @@ package credproviders import ( + "context" "os" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" @@ -46,7 +47,7 @@ func NewEnvProvider() *EnvProvider { } // Retrieve retrieves the keys from the environment. -func (e *EnvProvider) Retrieve() (credentials.Value, error) { +func (e *EnvProvider) Retrieve(_ context.Context) (credentials.Value, error) { e.retrieved = false v := credentials.Value{ diff --git a/internal/credproviders/imds_provider.go b/internal/credproviders/imds_provider.go index f3674c727b..f2a801a9e2 100644 --- a/internal/credproviders/imds_provider.go +++ b/internal/credproviders/imds_provider.go @@ -41,8 +41,8 @@ func NewAzureProvider(httpClient *http.Client, expiryWindow time.Duration) *Azur } } -// RetrieveWithContext retrieves the keys from the Azure service. -func (a *AzureProvider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) { +// Retrieve retrieves the keys from the Azure service. +func (a *AzureProvider) Retrieve(ctx context.Context) (credentials.Value, error) { v := credentials.Value{ProviderName: AzureProviderName} req, err := http.NewRequest(http.MethodGet, azureURI, nil) if err != nil { @@ -92,11 +92,6 @@ func (a *AzureProvider) RetrieveWithContext(ctx context.Context) (credentials.Va 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 a99b223f02..4a12664f4a 100644 --- a/internal/credproviders/static_provider.go +++ b/internal/credproviders/static_provider.go @@ -7,6 +7,7 @@ package credproviders import ( + "context" "errors" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" @@ -42,7 +43,7 @@ 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 diff --git a/internal/integration/client_side_encryption_prose_test.go b/internal/integration/client_side_encryption_prose_test.go index d2f84a1c2b..e65063bd85 100644 --- a/internal/integration/client_side_encryption_prose_test.go +++ b/internal/integration/client_side_encryption_prose_test.go @@ -3146,6 +3146,126 @@ func TestClientSideEncryptionProse(t *testing.T) { }) } +func TestCustomAwsCredentialsProse(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) + + ceo := options.ClientEncryption(). + SetKeyVaultNamespace("keyvault.datakeys"). + SetKmsProviders(map[string]map[string]any{ + "aws": { + "accessKeyId": awsAccessKeyID, + "secretAccessKey": awsSecretAccessKey, + }, + }). + SetCredentialProviders(map[string]options.CredentialsProvider{ + "aws": func(ctx context.Context) (options.Credentials, error) { + return options.Credentials{}, nil + }, + }) + _, 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 calledCount int + ceo := options.ClientEncryption(). + SetKeyVaultNamespace("keyvault.datakeys"). + SetKmsProviders(map[string]map[string]any{ + "aws": map[string]any{}, + }). + SetCredentialProviders(map[string]options.CredentialsProvider{ + "aws": func(_ context.Context) (options.Credentials, error) { + calledCount++ + return options.Credentials{ + AccessKeyID: awsAccessKeyID, + SecretAccessKey: awsSecretAccessKey, + ExpirationCallback: func() bool { return false }, + }, nil + }, + }) + 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, calledCount, "expected credential provider to be called once") + }) + + mt.Run("Case 3: AutoEncryptionOpts with credentialProviders and incorrect kmsProviders", func(mt *mtest.T) { + aeo := options.AutoEncryption(). + SetKeyVaultNamespace("keyvault.datakeys"). + SetKmsProviders(map[string]map[string]any{ + "aws": { + "accessKeyId": awsAccessKeyID, + "secretAccessKey": awsSecretAccessKey, + }, + }). + SetCredentialProviders(map[string]options.CredentialsProvider{ + "aws": func(ctx context.Context) (options.Credentials, error) { + return options.Credentials{}, nil + }, + }) + 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_SECRET_ACCESS_KEY")) + mt.Setenv("AWS_SECRET_ACCESS_KEY", os.Getenv("FLE_AWS_ACCESS_KEY_ID")) + + opts := options.Client().ApplyURI(mtest.ClusterURI()) + integtest.AddTestServerAPIVersion(opts) + keyVaultClient, err := mongo.Connect(opts) + assert.NoErrorf(mt, err, "error on Connect: %v", err) + + var calledCount int + ceo := options.ClientEncryption(). + SetKeyVaultNamespace("keyvault.datakeys"). + SetKmsProviders(map[string]map[string]any{ + "aws": map[string]any{}, + }). + SetCredentialProviders(map[string]options.CredentialsProvider{ + "aws": func(ctx context.Context) (options.Credentials, error) { + calledCount++ + return options.Credentials{ + AccessKeyID: awsAccessKeyID, + SecretAccessKey: awsSecretAccessKey, + ExpirationCallback: func() bool { return false }, + }, nil + }, + }) + 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, calledCount, "expected credential provider to be called once") + }) +} + func getWatcher(mt *mtest.T, streamType mongo.StreamType, cpt *cseProseTest) watcher { mt.Helper() diff --git a/mongo/client.go b/mongo/client.go index 10182276b2..8ee47aa199 100644 --- a/mongo/client.go +++ b/mongo/client.go @@ -17,6 +17,9 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" + "go.mongodb.org/mongo-driver/v2/internal/aws" + "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" + "go.mongodb.org/mongo-driver/v2/internal/credproviders" "go.mongodb.org/mongo-driver/v2/internal/httputil" "go.mongodb.org/mongo-driver/v2/internal/logger" "go.mongodb.org/mongo-driver/v2/internal/mongoutil" @@ -651,6 +654,26 @@ func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt bypassAutoEncryption := opts.BypassAutoEncryption != nil && *opts.BypassAutoEncryption bypassQueryAnalysis := opts.BypassQueryAnalysis != nil && *opts.BypassQueryAnalysis + providers := make(map[string]credentials.Provider) + for k, fn := range opts.CredentialProviders { + if k == "aws" && fn != nil { + providers[k] = &credproviders.AwsProvider{ + Provider: func(ctx context.Context) (aws.Credentials, error) { + var creds aws.Credentials + c, err := fn(ctx) + if err != nil { + return creds, err + } + creds.AccessKeyID = c.AccessKeyID + creds.SecretAccessKey = c.SecretAccessKey + creds.SessionToken = c.SessionToken + creds.ExpirationCallback = c.ExpirationCallback + return creds, nil + }, + } + } + } + mc, err := mongocrypt.NewMongoCrypt(mcopts.MongoCrypt(). SetKmsProviders(kmsProviders). SetLocalSchemaMap(cryptSchemaMap). @@ -659,7 +682,8 @@ func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt SetCryptSharedLibDisabled(cryptSharedLibDisabled || bypassAutoEncryption). SetCryptSharedLibOverridePath(cryptSharedLibPath). SetHTTPClient(opts.HTTPClient). - SetKeyExpiration(opts.KeyExpiration)) + SetKeyExpiration(opts.KeyExpiration). + SetCredentialProviders(providers)) if err != nil { return nil, err } diff --git a/mongo/client_encryption.go b/mongo/client_encryption.go index 32851ffffb..44d1959644 100644 --- a/mongo/client_encryption.go +++ b/mongo/client_encryption.go @@ -14,6 +14,9 @@ import ( "strings" "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/internal/aws" + "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" + "go.mongodb.org/mongo-driver/v2/internal/credproviders" "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,6 +56,26 @@ func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options. return nil, fmt.Errorf("error creating KMS providers map: %w", err) } + providers := make(map[string]credentials.Provider) + for k, fn := range cea.CredentialProviders { + if k == "aws" && fn != nil { + providers[k] = &credproviders.AwsProvider{ + Provider: func(ctx context.Context) (aws.Credentials, error) { + var creds aws.Credentials + c, err := fn(ctx) + if err != nil { + return creds, err + } + creds.AccessKeyID = c.AccessKeyID + creds.SecretAccessKey = c.SecretAccessKey + creds.SessionToken = c.SessionToken + creds.ExpirationCallback = c.ExpirationCallback + return creds, nil + }, + } + } + } + mc, err := mongocrypt.NewMongoCrypt(mcopts.MongoCrypt(). SetKmsProviders(kmsProviders). // Explicitly disable loading the crypt_shared library for the Crypt used for @@ -60,7 +83,8 @@ func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options. // have the crypt_shared library installed if they're using ClientEncryption. SetCryptSharedLibDisabled(true). SetHTTPClient(cea.HTTPClient). - SetKeyExpiration(cea.KeyExpiration)) + SetKeyExpiration(cea.KeyExpiration). + SetCredentialProviders(providers)) if err != nil { return nil, err } diff --git a/mongo/client_examples_test.go b/mongo/client_examples_test.go index 971fc74f08..76f6bd366f 100644 --- a/mongo/client_examples_test.go +++ b/mongo/client_examples_test.go @@ -267,10 +267,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 +353,29 @@ func ExampleConnect_aWS() { panic(err) } _ = ecClient + + // Custom AWS credential provider + + // Applications can authenticate using a custom AWS credential provider as + // well. + credential := options.Credential{ + AuthMechanism: "MONGODB-AWS", + AwsCredentialsProvider: func(_ context.Context) ( + options.Credentials, error) { + return options.Credentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + SessionToken: sessionToken, + ExpirationCallback: func() bool { return false }, + }, nil + }, + } + 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..e98eeae162 100644 --- a/mongo/options/autoencryptionoptions.go +++ b/mongo/options/autoencryptionoptions.go @@ -42,6 +42,7 @@ type AutoEncryptionOptions struct { EncryptedFieldsMap map[string]any BypassQueryAnalysis *bool KeyExpiration *time.Duration + CredentialProviders map[string]CredentialsProvider } // AutoEncryption creates a new AutoEncryptionOptions configured with default values. @@ -174,3 +175,9 @@ func (a *AutoEncryptionOptions) SetKeyExpiration(expiration time.Duration) *Auto return a } + +// SetCredentialProviders specifies options for custom credential providers. +func (a *AutoEncryptionOptions) SetCredentialProviders(providers map[string]CredentialsProvider) *AutoEncryptionOptions { + a.CredentialProviders = providers + return a +} diff --git a/mongo/options/clientencryptionoptions.go b/mongo/options/clientencryptionoptions.go index a6c477a7a9..fc73d6daa2 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 + CredentialProviders map[string]CredentialsProvider } // ClientEncryptionOptionsBuilder contains options to configure client @@ -94,6 +95,15 @@ func (c *ClientEncryptionOptionsBuilder) SetKeyExpiration(expiration time.Durati return c } +// SetCredentialProviders specifies options for custom credential providers. +func (c *ClientEncryptionOptionsBuilder) SetCredentialProviders(providers map[string]CredentialsProvider) *ClientEncryptionOptionsBuilder { + c.Opts = append(c.Opts, func(opts *ClientEncryptionOptions) error { + opts.CredentialProviders = providers + 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 adc880a5e9..8698dd2f52 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -25,6 +25,7 @@ import ( "github.com/youmark/pkcs8" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" + "go.mongodb.org/mongo-driver/v2/internal/aws" "go.mongodb.org/mongo-driver/v2/internal/httputil" "go.mongodb.org/mongo-driver/v2/internal/optionsutil" "go.mongodb.org/mongo-driver/v2/mongo/readconcern" @@ -116,6 +117,7 @@ type Credential struct { PasswordSet bool OIDCMachineCallback OIDCCallback OIDCHumanCallback OIDCCallback + AwsCredentialsProvider func(context.Context) (Credentials, error) } // OIDCCallback is the type for both Human and Machine Callback flows. @@ -144,6 +146,12 @@ type IDPInfo struct { RequestScopes []string } +// CredentialsProvider is the function type that returns AWS credentials. +type CredentialsProvider func(context.Context) (Credentials, error) + +// Credentials represents AWS credentials. +type Credentials aws.Credentials + // BSONOptions are optional BSON marshaling and unmarshaling behaviors. type BSONOptions struct { // UseJSONStructTags causes the driver to fall back to using the "json" diff --git a/x/mongo/driver/auth/mongodbaws.go b/x/mongo/driver/auth/mongodbaws.go index dd9661e1a9..11d78c0cfa 100644 --- a/x/mongo/driver/auth/mongodbaws.go +++ b/x/mongo/driver/auth/mongodbaws.go @@ -27,27 +27,35 @@ func newMongoDBAWSAuthenticator(cred *Cred, httpClient *http.Client) (Authentica if httpClient == nil { return nil, errors.New("httpClient must not be nil") } - return &MongoDBAWSAuthenticator{ - credentials: &credproviders.StaticProvider{ - Value: credentials.Value{ - AccessKeyID: cred.Username, - SecretAccessKey: cred.Password, - SessionToken: cred.Props["AWS_SESSION_TOKEN"], + authenticator := MongoDBAWSAuthenticator{ + providers: []credentials.Provider{ + &credproviders.StaticProvider{ + Value: credentials.Value{ + AccessKeyID: cred.Username, + SecretAccessKey: cred.Password, + SessionToken: cred.Props["AWS_SESSION_TOKEN"], + }, }, }, httpClient: httpClient, - }, nil + } + if cred.AwsCredentialsProvider != nil { + authenticator.providers = append(authenticator.providers, &credproviders.AwsProvider{ + Provider: cred.AwsCredentialsProvider, + }) + } + return &authenticator, nil } // MongoDBAWSAuthenticator uses AWS-IAM credentials over SASL to authenticate a connection. type MongoDBAWSAuthenticator struct { - credentials *credproviders.StaticProvider - httpClient *http.Client + providers []credentials.Provider + httpClient *http.Client } // Auth authenticates the connection. func (a *MongoDBAWSAuthenticator) Auth(ctx context.Context, cfg *driver.AuthConfig) error { - providers := creds.NewAWSCredentialProvider(a.httpClient, a.credentials) + providers := creds.NewAWSCredentialProvider(a.httpClient, a.providers...) adapter := &awsSaslAdapter{ conversation: &awsConversation{ credentials: providers.Cred, diff --git a/x/mongo/driver/auth/mongodbaws_test.go b/x/mongo/driver/auth/mongodbaws_test.go index ef72d7f29f..0c251f8f0f 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" + "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) { @@ -46,3 +57,153 @@ func TestGetRegion(t *testing.T) { } } + +func TestAWSCustomCredentialProvider(t *testing.T) { + t.Setenv("AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID") + t.Setenv("AWS_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY") + + var cnt int + 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: func(_ context.Context) (aws.Credentials, error) { + cnt++ + return aws.Credentials{}, nil + }, + }, + cnt: 0, + }, + { + name: "provider with empty cred", + cred: &Cred{ + AwsCredentialsProvider: func(_ context.Context) (aws.Credentials, error) { + cnt++ + return aws.Credentials{}, nil + }, + }, + cnt: 1, + }, + } { + 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, cnt, "expected provider to be called %v times but got %v", tc.cnt, 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/driver.go b/x/mongo/driver/driver.go index 88995263e5..62850903af 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" "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,14 @@ 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 func(context.Context) (aws.Credentials, 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 c2c47a6334..7b57f2506d 100644 --- a/x/mongo/driver/mongocrypt/mongocrypt.go +++ b/x/mongo/driver/mongocrypt/mongocrypt.go @@ -23,6 +23,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" @@ -34,9 +35,10 @@ type kmsProvider interface { } type MongoCrypt struct { - wrapped *C.mongocrypt_t - kmsProviders map[string]kmsProvider - httpClient *http.Client + wrapped *C.mongocrypt_t + kmsProviders map[string]kmsProvider + httpClient *http.Client + credProviders map[string]credentials.Provider } // Version returns the version string for the loaded libmongocrypt, or an empty string @@ -62,8 +64,16 @@ func NewMongoCrypt(opts *options.MongoCryptOptions) (*MongoCrypt, error) { if needsKmsProvider(opts.KmsProviders, "gcp") { kmsProviders["gcp"] = creds.NewGCPCredentialProvider(httpClient) } + provider, ok := opts.CredentialProviders["aws"] if needsKmsProvider(opts.KmsProviders, "aws") { - kmsProviders["aws"] = creds.NewAWSCredentialProvider(httpClient) + var providers []credentials.Provider + if ok { + providers = append(providers, provider) + } + kmsProviders["aws"] = creds.NewAWSCredentialProvider(httpClient, providers...) + } else if ok { + 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 504065d4bf..e2003c9598 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,6 +24,7 @@ type MongoCryptOptions struct { CryptSharedLibOverridePath string HTTPClient *http.Client KeyExpiration *time.Duration + CredentialProviders map[string]credentials.Provider } // MongoCrypt creates a new MongoCryptOptions instance. @@ -79,3 +81,9 @@ func (mo *MongoCryptOptions) SetKeyExpiration(expiration *time.Duration) *MongoC mo.KeyExpiration = expiration return mo } + +// SetCredentialProviders sets the custom credential providers. +func (mo *MongoCryptOptions) SetCredentialProviders(providers map[string]credentials.Provider) *MongoCryptOptions { + mo.CredentialProviders = providers + return mo +} diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index 2d53805d3b..aae08066c3 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/aws" "go.mongodb.org/mongo-driver/v2/internal/logger" "go.mongodb.org/mongo-driver/v2/internal/optionsutil" "go.mongodb.org/mongo-driver/v2/mongo/options" @@ -113,14 +114,28 @@ func ConvertCreds(cred *options.Credential) *driver.Cred { } } + var awsCredentialsProvider func(context.Context) (aws.Credentials, error) + if cred.AwsCredentialsProvider != nil { + awsCredentialsProvider = func(ctx context.Context) (aws.Credentials, error) { + creds, err := cred.AwsCredentialsProvider(ctx) + return aws.Credentials{ + AccessKeyID: creds.AccessKeyID, + SecretAccessKey: creds.SecretAccessKey, + SessionToken: creds.SessionToken, + ExpirationCallback: creds.ExpirationCallback, + }, err + } + } + return &auth.Cred{ - Source: cred.AuthSource, - Username: cred.Username, - Password: cred.Password, - PasswordSet: cred.PasswordSet, - Props: cred.AuthMechanismProperties, - OIDCMachineCallback: oidcMachineCallback, - OIDCHumanCallback: oidcHumanCallback, + Source: cred.AuthSource, + Username: cred.Username, + Password: cred.Password, + PasswordSet: cred.PasswordSet, + Props: cred.AuthMechanismProperties, + OIDCMachineCallback: oidcMachineCallback, + OIDCHumanCallback: oidcHumanCallback, + AwsCredentialsProvider: awsCredentialsProvider, } } From 4f706399ab22d5d682b22110992a298e306db4e3 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 30 Oct 2025 16:02:50 -0400 Subject: [PATCH 02/17] update `Credentials` struct --- internal/aws/types.go | 23 +++++++++++++++---- internal/credproviders/aws_provider.go | 4 ++-- .../client_side_encryption_prose_test.go | 10 ++++---- mongo/client.go | 9 ++------ mongo/client_encryption.go | 9 ++------ mongo/client_examples_test.go | 7 +++--- mongo/options/clientoptions.go | 13 ++++++++--- x/mongo/driver/topology/topology_options.go | 7 +----- 8 files changed, 43 insertions(+), 39 deletions(-) diff --git a/internal/aws/types.go b/internal/aws/types.go index 8ebc242733..d4fc5cc2ec 100644 --- a/internal/aws/types.go +++ b/internal/aws/types.go @@ -12,14 +12,29 @@ package aws import ( "io" + "time" ) // Credentials represents AWS credentials. type Credentials struct { - AccessKeyID string - SecretAccessKey string - SessionToken string - ExpirationCallback func() bool + AccessKeyID string + SecretAccessKey string + SessionToken string + Source string + CanExpire bool + Expires time.Time + AccountID string +} + +func (v Credentials) 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 } // ReadSeekCloser wraps a io.Reader returning a ReaderSeekerCloser. Allows the diff --git a/internal/credproviders/aws_provider.go b/internal/credproviders/aws_provider.go index 62af05535c..9e16b18068 100644 --- a/internal/credproviders/aws_provider.go +++ b/internal/credproviders/aws_provider.go @@ -40,8 +40,8 @@ func (a *AwsProvider) Retrieve(ctx context.Context) (credentials.Value, error) { // IsExpired returns true if the credentials have not been retrieved. func (a *AwsProvider) IsExpired() bool { - if a.credentials == nil || a.credentials.ExpirationCallback == nil { + if a.credentials == nil { return true } - return a.credentials.ExpirationCallback() + return a.credentials.Expired() } diff --git a/internal/integration/client_side_encryption_prose_test.go b/internal/integration/client_side_encryption_prose_test.go index e65063bd85..fd8902419b 100644 --- a/internal/integration/client_side_encryption_prose_test.go +++ b/internal/integration/client_side_encryption_prose_test.go @@ -3189,9 +3189,8 @@ func TestCustomAwsCredentialsProse(t *testing.T) { "aws": func(_ context.Context) (options.Credentials, error) { calledCount++ return options.Credentials{ - AccessKeyID: awsAccessKeyID, - SecretAccessKey: awsSecretAccessKey, - ExpirationCallback: func() bool { return false }, + AccessKeyID: awsAccessKeyID, + SecretAccessKey: awsSecretAccessKey, }, nil }, }) @@ -3247,9 +3246,8 @@ func TestCustomAwsCredentialsProse(t *testing.T) { "aws": func(ctx context.Context) (options.Credentials, error) { calledCount++ return options.Credentials{ - AccessKeyID: awsAccessKeyID, - SecretAccessKey: awsSecretAccessKey, - ExpirationCallback: func() bool { return false }, + AccessKeyID: awsAccessKeyID, + SecretAccessKey: awsSecretAccessKey, }, nil }, }) diff --git a/mongo/client.go b/mongo/client.go index 8ee47aa199..0e068da364 100644 --- a/mongo/client.go +++ b/mongo/client.go @@ -659,16 +659,11 @@ func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt if k == "aws" && fn != nil { providers[k] = &credproviders.AwsProvider{ Provider: func(ctx context.Context) (aws.Credentials, error) { - var creds aws.Credentials c, err := fn(ctx) if err != nil { - return creds, err + return aws.Credentials{}, err } - creds.AccessKeyID = c.AccessKeyID - creds.SecretAccessKey = c.SecretAccessKey - creds.SessionToken = c.SessionToken - creds.ExpirationCallback = c.ExpirationCallback - return creds, nil + return aws.Credentials(c), nil }, } } diff --git a/mongo/client_encryption.go b/mongo/client_encryption.go index 44d1959644..974793a2d1 100644 --- a/mongo/client_encryption.go +++ b/mongo/client_encryption.go @@ -61,16 +61,11 @@ func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options. if k == "aws" && fn != nil { providers[k] = &credproviders.AwsProvider{ Provider: func(ctx context.Context) (aws.Credentials, error) { - var creds aws.Credentials c, err := fn(ctx) if err != nil { - return creds, err + return aws.Credentials{}, err } - creds.AccessKeyID = c.AccessKeyID - creds.SecretAccessKey = c.SecretAccessKey - creds.SessionToken = c.SessionToken - creds.ExpirationCallback = c.ExpirationCallback - return creds, nil + return aws.Credentials(c), nil }, } } diff --git a/mongo/client_examples_test.go b/mongo/client_examples_test.go index 76f6bd366f..d8ee6d723b 100644 --- a/mongo/client_examples_test.go +++ b/mongo/client_examples_test.go @@ -363,10 +363,9 @@ func ExampleConnect_aWS() { AwsCredentialsProvider: func(_ context.Context) ( options.Credentials, error) { return options.Credentials{ - AccessKeyID: accessKeyID, - SecretAccessKey: secretAccessKey, - SessionToken: sessionToken, - ExpirationCallback: func() bool { return false }, + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + SessionToken: sessionToken, }, nil }, } diff --git a/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index 8698dd2f52..336c508773 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -25,7 +25,6 @@ import ( "github.com/youmark/pkcs8" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" - "go.mongodb.org/mongo-driver/v2/internal/aws" "go.mongodb.org/mongo-driver/v2/internal/httputil" "go.mongodb.org/mongo-driver/v2/internal/optionsutil" "go.mongodb.org/mongo-driver/v2/mongo/readconcern" @@ -117,7 +116,7 @@ type Credential struct { PasswordSet bool OIDCMachineCallback OIDCCallback OIDCHumanCallback OIDCCallback - AwsCredentialsProvider func(context.Context) (Credentials, error) + AwsCredentialsProvider CredentialsProvider } // OIDCCallback is the type for both Human and Machine Callback flows. @@ -150,7 +149,15 @@ type IDPInfo struct { type CredentialsProvider func(context.Context) (Credentials, error) // Credentials represents AWS credentials. -type Credentials aws.Credentials +type Credentials struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + Source string + CanExpire bool + Expires time.Time + AccountID string +} // BSONOptions are optional BSON marshaling and unmarshaling behaviors. type BSONOptions struct { diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index aae08066c3..beccf2771c 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -118,12 +118,7 @@ func ConvertCreds(cred *options.Credential) *driver.Cred { if cred.AwsCredentialsProvider != nil { awsCredentialsProvider = func(ctx context.Context) (aws.Credentials, error) { creds, err := cred.AwsCredentialsProvider(ctx) - return aws.Credentials{ - AccessKeyID: creds.AccessKeyID, - SecretAccessKey: creds.SecretAccessKey, - SessionToken: creds.SessionToken, - ExpirationCallback: creds.ExpirationCallback, - }, err + return aws.Credentials(creds), err } } From 11b407d013802ce9f9e235d19cf479c62d07f821 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 8 Jan 2026 15:57:15 -0500 Subject: [PATCH 03/17] update interfaces --- .evergreen/config.yml | 24 ++++ Taskfile.yml | 2 + internal/aws/types.go | 23 ---- internal/credproviders/aws_provider.go | 47 -------- .../client_side_encryption_prose_test.go | 58 ++++----- mongo/client.go | 27 +---- mongo/client_encryption.go | 48 +++++--- mongo/client_examples_test.go | 36 ++++-- mongo/options/autoencryptionoptions.go | 30 ++--- mongo/options/clientencryptionoptions.go | 18 +-- mongo/options/clientoptions.go | 33 ++--- x/mongo/driver/auth/aws_conv.go | 114 +++++++++--------- x/mongo/driver/auth/mongodbaws.go | 75 +++++------- x/mongo/driver/auth/mongodbaws_test.go | 37 +++--- x/mongo/driver/driver.go | 10 +- x/mongo/driver/mongocrypt/mongocrypt.go | 15 ++- .../mongocrypt/options/mongocrypt_options.go | 8 +- x/mongo/driver/topology/topology_options.go | 68 +++++++---- 18 files changed, 329 insertions(+), 344 deletions(-) delete mode 100644 internal/credproviders/aws_provider.go diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 97fccf15ce..5bedfbf481 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -517,6 +517,20 @@ functions: binary: "bash" include_expansions_in_env: [KMS_TLS_TESTCASE] args: [*task-runner, evg-test-kms] + run-custom-aws-credentials-test: + - command: subprocess.exec + params: + binary: "bash" + env: + GO_BUILD_TAGS: cse + include_expansions_in_env: [AUTH, SSL, MONGODB_URI, TOPOLOGY, MONGO_GO_DRIVER_COMPRESSOR] + args: [*task-runner, setup-test] + - command: subprocess.exec + type: test + retry_on_failure: true + params: + binary: "bash" + args: [*task-runner, evg-test-custom-aws-credentials] run-kmip-tests: - command: subprocess.exec params: @@ -1438,6 +1452,16 @@ tasks: TOPOLOGY: "server" AUTH: "noauth" SSL: "nossl" + - name: "test-custom-aws-credentials" + tags: ["kms-test"] + commands: + - func: bootstrap-mongo-orchestration + vars: + TOPOLOGY: "server" + AUTH: "noauth" + SSL: "nossl" + - func: start-cse-servers + - func: run-custom-aws-credentials-test - name: "test-kms-kmip" tags: ["kms-kmip"] commands: diff --git a/Taskfile.yml b/Taskfile.yml index 73842ab9c9..d130317a6a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -100,6 +100,8 @@ tasks: - go test -exec "env PKG_CONFIG_PATH=${PKG_CONFIG_PATH} LD_LIBRARY_PATH=${LD_LIBRARY_PATH} DYLD_LIBRARY_PATH=${MACOS_LIBRARY_PATH}" ${BUILD_TAGS} -v -timeout {{.TEST_TIMEOUT}}s ./internal/integration -run TestClientSideEncryptionProse/kms_tls_options_test >> test.suite evg-test-kms: - go test -exec "env PKG_CONFIG_PATH=${PKG_CONFIG_PATH} LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" ${BUILD_TAGS} -v -timeout {{.TEST_TIMEOUT}}s ./internal/integration -run TestClientSideEncryptionProse/kms_tls_tests >> test.suite + evg-test-custom-aws-credentials: + - go test -exec "env PKG_CONFIG_PATH=${PKG_CONFIG_PATH} LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" ${BUILD_TAGS} -v -timeout {{.TEST_TIMEOUT}}s ./internal/integration -run TestCustomAwsCredentialsProse >> test.suite evg-test-retry-kms-requests: - go test -exec "env PKG_CONFIG_PATH=${PKG_CONFIG_PATH} LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" ${BUILD_TAGS} -v -timeout {{.TEST_TIMEOUT}}s ./internal/integration -run TestClientSideEncryptionProse/kms_retry_tests >> test.suite evg-test-load-balancers: diff --git a/internal/aws/types.go b/internal/aws/types.go index d4fc5cc2ec..52aecda76b 100644 --- a/internal/aws/types.go +++ b/internal/aws/types.go @@ -12,31 +12,8 @@ package aws import ( "io" - "time" ) -// Credentials represents AWS credentials. -type Credentials struct { - AccessKeyID string - SecretAccessKey string - SessionToken string - Source string - CanExpire bool - Expires time.Time - AccountID string -} - -func (v Credentials) 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 -} - // ReadSeekCloser wraps a io.Reader returning a ReaderSeekerCloser. Allows the // SDK to accept an io.Reader that is not also an io.Seeker for unsigned // streaming payload API operations. diff --git a/internal/credproviders/aws_provider.go b/internal/credproviders/aws_provider.go deleted file mode 100644 index 9e16b18068..0000000000 --- a/internal/credproviders/aws_provider.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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 credproviders - -import ( - "context" - - "go.mongodb.org/mongo-driver/v2/internal/aws" - "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" -) - -const awsProviderName = "AwsProvider" - -// AwsProvider retrieves credentials from the given AWS credentials provider. -type AwsProvider struct { - credentials *aws.Credentials - Provider func(context.Context) (aws.Credentials, error) -} - -// Retrieve retrieves the keys from the given AWS credentials provider. -func (a *AwsProvider) Retrieve(ctx context.Context) (credentials.Value, error) { - var value credentials.Value - if a.credentials == nil { - creds, err := a.Provider(ctx) - if err != nil { - return value, err - } - a.credentials = &creds - } - value.AccessKeyID = a.credentials.AccessKeyID - value.SecretAccessKey = a.credentials.SecretAccessKey - value.SessionToken = a.credentials.SessionToken - value.ProviderName = awsProviderName - return value, nil -} - -// IsExpired returns true if the credentials have not been retrieved. -func (a *AwsProvider) IsExpired() bool { - if a.credentials == nil { - return true - } - return a.credentials.Expired() -} diff --git a/internal/integration/client_side_encryption_prose_test.go b/internal/integration/client_side_encryption_prose_test.go index fd8902419b..db94daa8b8 100644 --- a/internal/integration/client_side_encryption_prose_test.go +++ b/internal/integration/client_side_encryption_prose_test.go @@ -3146,6 +3146,22 @@ func TestClientSideEncryptionProse(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 (p *awsCredentialsProvider) Expired() bool { + return false +} + func TestCustomAwsCredentialsProse(t *testing.T) { mt := mtest.New(t, mtest.NewOptions().CreateClient(false)) @@ -3155,6 +3171,7 @@ func TestCustomAwsCredentialsProse(t *testing.T) { 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{ @@ -3163,11 +3180,7 @@ func TestCustomAwsCredentialsProse(t *testing.T) { "secretAccessKey": awsSecretAccessKey, }, }). - SetCredentialProviders(map[string]options.CredentialsProvider{ - "aws": func(ctx context.Context) (options.Credentials, error) { - return options.Credentials{}, nil - }, - }) + SetAWSCredentialsProvider(&provider) _, err = mongo.NewClientEncryption(keyVaultClient, ceo) assert.ErrorContains(mt, err, "can only provide a custom AWS credential provider", "unexpected error: %v", err) @@ -3179,21 +3192,13 @@ func TestCustomAwsCredentialsProse(t *testing.T) { keyVaultClient, err := mongo.Connect(opts) assert.NoErrorf(mt, err, "error on Connect: %v", err) - var calledCount int + var provider awsCredentialsProvider ceo := options.ClientEncryption(). SetKeyVaultNamespace("keyvault.datakeys"). SetKmsProviders(map[string]map[string]any{ "aws": map[string]any{}, }). - SetCredentialProviders(map[string]options.CredentialsProvider{ - "aws": func(_ context.Context) (options.Credentials, error) { - calledCount++ - return options.Credentials{ - AccessKeyID: awsAccessKeyID, - SecretAccessKey: awsSecretAccessKey, - }, nil - }, - }) + SetAWSCredentialsProvider(&provider) clientEncryption, err := mongo.NewClientEncryption(keyVaultClient, ceo) assert.NoErrorf(mt, err, "error on NewClientEncryption: %v", err) @@ -3203,10 +3208,11 @@ func TestCustomAwsCredentialsProse(t *testing.T) { }) _, err = clientEncryption.CreateDataKey(context.Background(), "aws", dkOpts) assert.NoErrorf(mt, err, "unexpected error %v", err) - assert.Equal(mt, 1, calledCount, "expected credential provider to be called once") + 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{ @@ -3215,11 +3221,7 @@ func TestCustomAwsCredentialsProse(t *testing.T) { "secretAccessKey": awsSecretAccessKey, }, }). - SetCredentialProviders(map[string]options.CredentialsProvider{ - "aws": func(ctx context.Context) (options.Credentials, error) { - return options.Credentials{}, nil - }, - }) + SetAWSCredentialsProvider(&provider) co := options.Client().SetAutoEncryptionOptions(aeo).ApplyURI(mtest.ClusterURI()) integtest.AddTestServerAPIVersion(co) _, err := mongo.Connect(co) @@ -3236,21 +3238,13 @@ func TestCustomAwsCredentialsProse(t *testing.T) { keyVaultClient, err := mongo.Connect(opts) assert.NoErrorf(mt, err, "error on Connect: %v", err) - var calledCount int + var provider awsCredentialsProvider ceo := options.ClientEncryption(). SetKeyVaultNamespace("keyvault.datakeys"). SetKmsProviders(map[string]map[string]any{ "aws": map[string]any{}, }). - SetCredentialProviders(map[string]options.CredentialsProvider{ - "aws": func(ctx context.Context) (options.Credentials, error) { - calledCount++ - return options.Credentials{ - AccessKeyID: awsAccessKeyID, - SecretAccessKey: awsSecretAccessKey, - }, nil - }, - }) + SetAWSCredentialsProvider(&provider) clientEncryption, err := mongo.NewClientEncryption(keyVaultClient, ceo) assert.NoErrorf(mt, err, "error on NewClientEncryption: %v", err) @@ -3260,7 +3254,7 @@ func TestCustomAwsCredentialsProse(t *testing.T) { }) _, err = clientEncryption.CreateDataKey(context.Background(), "aws", dkOpts) assert.NoErrorf(mt, err, "unexpected error %v", err) - assert.Equal(mt, 1, calledCount, "expected credential provider to be called once") + assert.Equal(mt, 1, provider.cnt, "expected credential provider to be called once") }) } diff --git a/mongo/client.go b/mongo/client.go index 0e068da364..2095555a7d 100644 --- a/mongo/client.go +++ b/mongo/client.go @@ -17,9 +17,6 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" - "go.mongodb.org/mongo-driver/v2/internal/aws" - "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" - "go.mongodb.org/mongo-driver/v2/internal/credproviders" "go.mongodb.org/mongo-driver/v2/internal/httputil" "go.mongodb.org/mongo-driver/v2/internal/logger" "go.mongodb.org/mongo-driver/v2/internal/mongoutil" @@ -654,22 +651,7 @@ func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt bypassAutoEncryption := opts.BypassAutoEncryption != nil && *opts.BypassAutoEncryption bypassQueryAnalysis := opts.BypassQueryAnalysis != nil && *opts.BypassQueryAnalysis - providers := make(map[string]credentials.Provider) - for k, fn := range opts.CredentialProviders { - if k == "aws" && fn != nil { - providers[k] = &credproviders.AwsProvider{ - Provider: func(ctx context.Context) (aws.Credentials, error) { - c, err := fn(ctx) - if err != nil { - return aws.Credentials{}, err - } - return aws.Credentials(c), nil - }, - } - } - } - - mc, err := mongocrypt.NewMongoCrypt(mcopts.MongoCrypt(). + cryptOpts := mcopts.MongoCrypt(). SetKmsProviders(kmsProviders). SetLocalSchemaMap(cryptSchemaMap). SetBypassQueryAnalysis(bypassQueryAnalysis). @@ -677,8 +659,11 @@ func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt SetCryptSharedLibDisabled(cryptSharedLibDisabled || bypassAutoEncryption). SetCryptSharedLibOverridePath(cryptSharedLibPath). SetHTTPClient(opts.HTTPClient). - SetKeyExpiration(opts.KeyExpiration). - SetCredentialProviders(providers)) + SetKeyExpiration(opts.KeyExpiration) + if opts.AWSCredentialsProvider != nil { + cryptOpts = cryptOpts.SetAWSCredentialsProvider(awsCredentialsProvider{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 974793a2d1..23de245fa8 100644 --- a/mongo/client_encryption.go +++ b/mongo/client_encryption.go @@ -14,9 +14,7 @@ import ( "strings" "go.mongodb.org/mongo-driver/v2/bson" - "go.mongodb.org/mongo-driver/v2/internal/aws" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" - "go.mongodb.org/mongo-driver/v2/internal/credproviders" "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" @@ -33,6 +31,27 @@ type ClientEncryption struct { closed bool } +type awsCredentialsProvider struct { + provider options.AWSCredentialsProvider +} + +func (p awsCredentialsProvider) 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, + ProviderName: "AwsProvider", + }, nil +} + +func (p awsCredentialsProvider) IsExpired() bool { + return p.provider.Expired() +} + // NewClientEncryption creates a new ClientEncryption instance configured with the given options. func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options.ClientEncryptionOptions]) (*ClientEncryption, error) { if keyVaultClient == nil { @@ -56,30 +75,19 @@ func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options. return nil, fmt.Errorf("error creating KMS providers map: %w", err) } - providers := make(map[string]credentials.Provider) - for k, fn := range cea.CredentialProviders { - if k == "aws" && fn != nil { - providers[k] = &credproviders.AwsProvider{ - Provider: func(ctx context.Context) (aws.Credentials, error) { - c, err := fn(ctx) - if err != nil { - return aws.Credentials{}, err - } - return aws.Credentials(c), nil - }, - } - } - } - - mc, err := mongocrypt.NewMongoCrypt(mcopts.MongoCrypt(). + cryptOpts := mcopts.MongoCrypt(). SetKmsProviders(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 // have the crypt_shared library installed if they're using ClientEncryption. SetCryptSharedLibDisabled(true). SetHTTPClient(cea.HTTPClient). - SetKeyExpiration(cea.KeyExpiration). - SetCredentialProviders(providers)) + SetKeyExpiration(cea.KeyExpiration) + if cea.AWSCredentialsProvider != nil { + cryptOpts = cryptOpts.SetAWSCredentialsProvider(awsCredentialsProvider{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 d8ee6d723b..1e6e556e8e 100644 --- a/mongo/client_examples_test.go +++ b/mongo/client_examples_test.go @@ -246,6 +246,25 @@ 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 (*awsCredentialsProvider) Expired() bool { + return false +} + func ExampleConnect_aWS() { // Configure a Client with authentication using the MONGODB-AWS // authentication mechanism. Credentials for this mechanism can come from @@ -358,16 +377,15 @@ func ExampleConnect_aWS() { // 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: func(_ context.Context) ( - options.Credentials, error) { - return options.Credentials{ - AccessKeyID: accessKeyID, - SecretAccessKey: secretAccessKey, - SessionToken: sessionToken, - }, nil - }, + AuthMechanism: "MONGODB-AWS", + AWSCredentialsProvider: awsCredentialProvider, } awsClient, err := mongo.Connect( options.Client().SetAuth(credential)) diff --git a/mongo/options/autoencryptionoptions.go b/mongo/options/autoencryptionoptions.go index e98eeae162..69d6821fe1 100644 --- a/mongo/options/autoencryptionoptions.go +++ b/mongo/options/autoencryptionoptions.go @@ -31,18 +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 - CredentialProviders map[string]CredentialsProvider + 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. @@ -176,8 +176,8 @@ func (a *AutoEncryptionOptions) SetKeyExpiration(expiration time.Duration) *Auto return a } -// SetCredentialProviders specifies options for custom credential providers. -func (a *AutoEncryptionOptions) SetCredentialProviders(providers map[string]CredentialsProvider) *AutoEncryptionOptions { - a.CredentialProviders = providers +// 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 fc73d6daa2..76944be650 100644 --- a/mongo/options/clientencryptionoptions.go +++ b/mongo/options/clientencryptionoptions.go @@ -19,12 +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 - CredentialProviders map[string]CredentialsProvider + 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 @@ -95,10 +95,10 @@ func (c *ClientEncryptionOptionsBuilder) SetKeyExpiration(expiration time.Durati return c } -// SetCredentialProviders specifies options for custom credential providers. -func (c *ClientEncryptionOptionsBuilder) SetCredentialProviders(providers map[string]CredentialsProvider) *ClientEncryptionOptionsBuilder { +// 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.CredentialProviders = providers + opts.AWSCredentialsProvider = provider return nil }) return c diff --git a/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index 336c508773..a5a05eb7b3 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -116,7 +116,24 @@ type Credential struct { PasswordSet bool OIDCMachineCallback OIDCCallback OIDCHumanCallback OIDCCallback - AwsCredentialsProvider CredentialsProvider + AWSCredentialsProvider AWSCredentialsProvider +} + +// AWSCredentialsProvider is the interface used to retrieve AWS credentials. +type AWSCredentialsProvider interface { + Retrieve(ctx context.Context) (AWSCredentials, error) + Expired() bool +} + +// AWSCredentials represents AWS credentials. +type AWSCredentials struct { + AccessKeyID string + SecretAccessKey string + SessionToken string + Source string + CanExpire bool + Expires time.Time + AccountID string } // OIDCCallback is the type for both Human and Machine Callback flows. @@ -145,20 +162,6 @@ type IDPInfo struct { RequestScopes []string } -// CredentialsProvider is the function type that returns AWS credentials. -type CredentialsProvider func(context.Context) (Credentials, error) - -// Credentials represents AWS credentials. -type Credentials struct { - AccessKeyID string - SecretAccessKey string - SessionToken string - Source string - CanExpire bool - Expires time.Time - AccountID string -} - // BSONOptions are optional BSON marshaling and unmarshaling behaviors. type BSONOptions struct { // UseJSONStructTags causes the driver to fall back to using the "json" diff --git a/x/mongo/driver/auth/aws_conv.go b/x/mongo/driver/auth/aws_conv.go index 06dd04376c..d0eef0ee3b 100644 --- a/x/mongo/driver/auth/aws_conv.go +++ b/x/mongo/driver/auth/aws_conv.go @@ -18,8 +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,16 +30,37 @@ const ( clientDone ) -type awsConversation struct { - state clientState - valid bool - nonce []byte - credentials *credentials.Credentials +type awsSigner interface { + Sign(ctx context.Context, request *http.Request, body, service, region string, signTime time.Time) error } -type serverMessage struct { - Nonce bson.Binary `bson:"s"` - Host string `bson:"h"` +type awsSaslAdapter struct { + signer awsSigner + + 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 +} + +func (a *awsSaslAdapter) Completed() bool { + return a.state == clientDone } const ( @@ -51,40 +70,27 @@ const ( responceNonceLength = 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 +117,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 @@ -137,7 +146,7 @@ func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) { if len(sm.Nonce.Data) != responceNonceLength { return nil, fmt.Errorf("server reply nonce was not %v bytes", responceNonceLength) } - 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 +155,24 @@ 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" + timeStr := currentTime.Format(amzDateFormat) // 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-Amz-Date", timeStr) 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(context.Background(), req, body, "sts", region, currentTime) if err != nil { return nil, err } @@ -178,9 +180,9 @@ func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) { // create message 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) + msg = bsoncore.AppendStringElement(msg, "d", timeStr) + 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/mongodbaws.go b/x/mongo/driver/auth/mongodbaws.go index 11d78c0cfa..b33205f3ba 100644 --- a/x/mongo/driver/auth/mongodbaws.go +++ b/x/mongo/driver/auth/mongodbaws.go @@ -10,8 +10,11 @@ import ( "context" "errors" "net/http" + "strings" + "time" "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" @@ -27,41 +30,39 @@ func newMongoDBAWSAuthenticator(cred *Cred, httpClient *http.Client) (Authentica if httpClient == nil { return nil, errors.New("httpClient must not be nil") } - authenticator := MongoDBAWSAuthenticator{ - providers: []credentials.Provider{ - &credproviders.StaticProvider{ - Value: credentials.Value{ - AccessKeyID: cred.Username, - SecretAccessKey: cred.Password, - SessionToken: cred.Props["AWS_SESSION_TOKEN"], - }, + + 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 { - authenticator.providers = append(authenticator.providers, &credproviders.AwsProvider{ - Provider: cred.AwsCredentialsProvider, - }) + if cred.AWSCredentialsProvider != nil { + providers = append(providers, cred.AWSCredentialsProvider) } - return &authenticator, nil + + return &MongoDBAWSAuthenticator{ + signer: &builtInV4Signer{ + providers: providers, + httpClient: httpClient, + }, + }, nil } // MongoDBAWSAuthenticator uses AWS-IAM credentials over SASL to authenticate a connection. type MongoDBAWSAuthenticator struct { - providers []credentials.Provider - httpClient *http.Client + signer *builtInV4Signer } // Auth authenticates the connection. func (a *MongoDBAWSAuthenticator) Auth(ctx context.Context, cfg *driver.AuthConfig) error { - providers := creds.NewAWSCredentialProvider(a.httpClient, a.providers...) - adapter := &awsSaslAdapter{ - conversation: &awsConversation{ - credentials: providers.Cred, - }, + awsSasl := &awsSaslAdapter{ + signer: a.signer, } - err := ConductSaslConversation(ctx, cfg, sourceExternal, adapter) + err := ConductSaslConversation(ctx, cfg, sourceExternal, awsSasl) if err != nil { return newAuthError("sasl conversation error", err) } @@ -73,28 +74,14 @@ func (a *MongoDBAWSAuthenticator) Reauth(_ context.Context, _ *driver.AuthConfig 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 +type builtInV4Signer struct { + providers []credentials.Provider + httpClient *http.Client } -func (a *awsSaslAdapter) Completed() bool { - return a.conversation.Done() +func (b *builtInV4Signer) Sign(_ context.Context, req *http.Request, body, service, region string, signTime time.Time) error { + awsProvider := creds.NewAWSCredentialProvider(b.httpClient, b.providers...) + signer := v4signer.NewSigner(awsProvider.Cred) + _, err := signer.Sign(req, strings.NewReader(body), service, region, signTime) + return err } diff --git a/x/mongo/driver/auth/mongodbaws_test.go b/x/mongo/driver/auth/mongodbaws_test.go index 0c251f8f0f..ac4e53d428 100644 --- a/x/mongo/driver/auth/mongodbaws_test.go +++ b/x/mongo/driver/auth/mongodbaws_test.go @@ -14,7 +14,7 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/internal/assert" - "go.mongodb.org/mongo-driver/v2/internal/aws" + "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" @@ -58,11 +58,24 @@ func TestGetRegion(t *testing.T) { } +type awsCredentialsProvider struct { + cnt int +} + +func (a *awsCredentialsProvider) Retrieve(_ context.Context) (credentials.Value, error) { + a.cnt++ + return credentials.Value{}, nil +} + +func (*awsCredentialsProvider) IsExpired() bool { + return false +} + func TestAWSCustomCredentialProvider(t *testing.T) { t.Setenv("AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID") t.Setenv("AWS_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY") - var cnt int + provider := &awsCredentialsProvider{} for _, tc := range []struct { name string cred *Cred @@ -71,28 +84,22 @@ func TestAWSCustomCredentialProvider(t *testing.T) { { name: "provider with cred", cred: &Cred{ - Username: "user", - Password: "pass", - Props: map[string]string{"AWS_SESSION_TOKEN": "token"}, - AwsCredentialsProvider: func(_ context.Context) (aws.Credentials, error) { - cnt++ - return aws.Credentials{}, nil - }, + Username: "user", + Password: "pass", + Props: map[string]string{"AWS_SESSION_TOKEN": "token"}, + AWSCredentialsProvider: provider, }, cnt: 0, }, { name: "provider with empty cred", cred: &Cred{ - AwsCredentialsProvider: func(_ context.Context) (aws.Credentials, error) { - cnt++ - return aws.Credentials{}, nil - }, + AWSCredentialsProvider: provider, }, cnt: 1, }, } { - cnt = 0 + provider.cnt = 0 t.Run(tc.name, func(t *testing.T) { authenticator, err := newMongoDBAWSAuthenticator( tc.cred, @@ -125,7 +132,7 @@ func TestAWSCustomCredentialProvider(t *testing.T) { 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, cnt, "expected provider to be called %v times but got %v", tc.cnt, cnt) + assert.Equalf(t, tc.cnt, provider.cnt, "expected provider to be called %v times but got %v", tc.cnt, provider.cnt) }) } } diff --git a/x/mongo/driver/driver.go b/x/mongo/driver/driver.go index 62850903af..1ceb458fe4 100644 --- a/x/mongo/driver/driver.go +++ b/x/mongo/driver/driver.go @@ -17,7 +17,7 @@ import ( "context" "time" - "go.mongodb.org/mongo-driver/v2/internal/aws" + "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" @@ -81,7 +81,13 @@ type Cred struct { Props map[string]string OIDCMachineCallback OIDCCallback OIDCHumanCallback OIDCCallback - AwsCredentialsProvider func(context.Context) (aws.Credentials, error) + AWSCredentialsProvider AWSCredentialsProvider +} + +// AWSCredentialsProvider is the interface used to retrieve AWS credentials. +type AWSCredentialsProvider interface { + Retrieve(ctx context.Context) (credentials.Value, error) + IsExpired() bool } // 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 7b57f2506d..caf8f8f321 100644 --- a/x/mongo/driver/mongocrypt/mongocrypt.go +++ b/x/mongo/driver/mongocrypt/mongocrypt.go @@ -35,10 +35,9 @@ type kmsProvider interface { } type MongoCrypt struct { - wrapped *C.mongocrypt_t - kmsProviders map[string]kmsProvider - httpClient *http.Client - credProviders map[string]credentials.Provider + wrapped *C.mongocrypt_t + kmsProviders map[string]kmsProvider + httpClient *http.Client } // Version returns the version string for the loaded libmongocrypt, or an empty string @@ -64,14 +63,14 @@ func NewMongoCrypt(opts *options.MongoCryptOptions) (*MongoCrypt, error) { if needsKmsProvider(opts.KmsProviders, "gcp") { kmsProviders["gcp"] = creds.NewGCPCredentialProvider(httpClient) } - provider, ok := opts.CredentialProviders["aws"] + awsCredentialsProvider := opts.AWSCredentialsProvider if needsKmsProvider(opts.KmsProviders, "aws") { var providers []credentials.Provider - if ok { - providers = append(providers, provider) + if awsCredentialsProvider != nil { + providers = append(providers, awsCredentialsProvider) } kmsProviders["aws"] = creds.NewAWSCredentialProvider(httpClient, providers...) - } else if ok { + } 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") } diff --git a/x/mongo/driver/mongocrypt/options/mongocrypt_options.go b/x/mongo/driver/mongocrypt/options/mongocrypt_options.go index e2003c9598..20e8b9bd4e 100644 --- a/x/mongo/driver/mongocrypt/options/mongocrypt_options.go +++ b/x/mongo/driver/mongocrypt/options/mongocrypt_options.go @@ -24,7 +24,7 @@ type MongoCryptOptions struct { CryptSharedLibOverridePath string HTTPClient *http.Client KeyExpiration *time.Duration - CredentialProviders map[string]credentials.Provider + AWSCredentialsProvider credentials.Provider } // MongoCrypt creates a new MongoCryptOptions instance. @@ -82,8 +82,8 @@ func (mo *MongoCryptOptions) SetKeyExpiration(expiration *time.Duration) *MongoC return mo } -// SetCredentialProviders sets the custom credential providers. -func (mo *MongoCryptOptions) SetCredentialProviders(providers map[string]credentials.Provider) *MongoCryptOptions { - mo.CredentialProviders = providers +// SetAWSCredentialsProvider sets the custom AWS credential provider. +func (mo *MongoCryptOptions) SetAWSCredentialsProvider(provider credentials.Provider) *MongoCryptOptions { + mo.AWSCredentialsProvider = provider return mo } diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index beccf2771c..2ffaeb9f99 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -15,7 +15,7 @@ import ( "time" "go.mongodb.org/mongo-driver/v2/event" - "go.mongodb.org/mongo-driver/v2/internal/aws" + "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" "go.mongodb.org/mongo-driver/v2/internal/logger" "go.mongodb.org/mongo-driver/v2/internal/optionsutil" "go.mongodb.org/mongo-driver/v2/mongo/options" @@ -93,45 +93,65 @@ 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 } } - var awsCredentialsProvider func(context.Context) (aws.Credentials, error) - if cred.AwsCredentialsProvider != nil { - awsCredentialsProvider = func(ctx context.Context) (aws.Credentials, error) { - creds, err := cred.AwsCredentialsProvider(ctx) - return aws.Credentials(creds), err - } + cred := &auth.Cred{ + Source: credOpts.AuthSource, + Username: credOpts.Username, + Password: credOpts.Password, + PasswordSet: credOpts.PasswordSet, + Props: credOpts.AuthMechanismProperties, + OIDCMachineCallback: oidcMachineCallback, + OIDCHumanCallback: oidcHumanCallback, } - return &auth.Cred{ - Source: cred.AuthSource, - Username: cred.Username, - Password: cred.Password, - PasswordSet: cred.PasswordSet, - Props: cred.AuthMechanismProperties, - OIDCMachineCallback: oidcMachineCallback, - OIDCHumanCallback: oidcHumanCallback, - AwsCredentialsProvider: awsCredentialsProvider, + if credOpts.AWSCredentialsProvider != nil { + cred.AWSCredentialsProvider = awsCredentialsProvider{ + provider: credOpts.AWSCredentialsProvider, + } } + + return cred +} + +type awsCredentialsProvider struct { + provider options.AWSCredentialsProvider +} + +func (p awsCredentialsProvider) 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, + ProviderName: "AwsProvider", + }, nil +} + +func (p awsCredentialsProvider) IsExpired() bool { + return p.provider.Expired() } // NewConfig will translate data from client options into a topology config for From 28a624d2682a04cbfaccf8dd417958baea428ee6 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 9 Jan 2026 16:22:26 -0500 Subject: [PATCH 04/17] add AWS submodule --- go.work | 1 + mongoaws/go.mod | 21 +++++++++++++ mongoaws/go.sum | 52 +++++++++++++++++++++++++++++++ mongoaws/mongoaws.go | 43 +++++++++++++++++++++++++ mongoaws/mongoaws_example_test.go | 27 ++++++++++++++++ 5 files changed, 144 insertions(+) create mode 100644 mongoaws/go.mod create mode 100644 mongoaws/go.sum create mode 100644 mongoaws/mongoaws.go create mode 100644 mongoaws/mongoaws_example_test.go diff --git a/go.work b/go.work index 43568f4aa7..19d41d6776 100644 --- a/go.work +++ b/go.work @@ -10,4 +10,5 @@ use ( ./internal/cmd/faas/awslambda/mongodb ./internal/test/compilecheck ./internal/test/goleak + ./mongoaws ) diff --git a/mongoaws/go.mod b/mongoaws/go.mod new file mode 100644 index 0000000000..0a6c270508 --- /dev/null +++ b/mongoaws/go.mod @@ -0,0 +1,21 @@ +module go.mongodb.org/mongo-driver/v2/mongoaws + +go 1.23 + +require ( + github.com/aws/aws-sdk-go-v2 v1.40.1 + go.mongodb.org/mongo-driver/v2 v2.0.0-beta2 +) + +require ( + github.com/aws/smithy-go v1.24.0 // 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 +) diff --git a/mongoaws/go.sum b/mongoaws/go.sum new file mode 100644 index 0000000000..c94867c896 --- /dev/null +++ b/mongoaws/go.sum @@ -0,0 +1,52 @@ +github.com/aws/aws-sdk-go-v2 v1.40.1 h1:difXb4maDZkRH0x//Qkwcfpdg1XQVXEAEs2DdXldFFc= +github.com/aws/aws-sdk-go-v2 v1.40.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +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/mongoaws/mongoaws.go b/mongoaws/mongoaws.go new file mode 100644 index 0000000000..5f76e61716 --- /dev/null +++ b/mongoaws/mongoaws.go @@ -0,0 +1,43 @@ +// Copyright (C) MongoDB, Inc. 2026-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 mongoaws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +type CredentialsProvider struct { + cfg *aws.Config + + creds aws.Credentials +} + +func NewCredentialsProvider(cfg *aws.Config) *CredentialsProvider { + if cfg == nil { + cfg = aws.NewConfig() + } + return &CredentialsProvider{ + cfg: cfg, + creds: aws.Credentials{}, + } +} + +func (p *CredentialsProvider) Retrieve(ctx context.Context) (options.AWSCredentials, error) { + var err error + p.creds, err = p.cfg.Credentials.Retrieve(ctx) + if err != nil { + return options.AWSCredentials{}, err + } + return options.AWSCredentials(p.creds), nil +} + +func (p *CredentialsProvider) Expired() bool { + return p.creds.Expired() +} diff --git a/mongoaws/mongoaws_example_test.go b/mongoaws/mongoaws_example_test.go new file mode 100644 index 0000000000..59b6e21d0c --- /dev/null +++ b/mongoaws/mongoaws_example_test.go @@ -0,0 +1,27 @@ +// Copyright (C) MongoDB, Inc. 2026-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 mongoaws + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +func ExampleNewCredentialsProvider() { + awsCredentialProvider := NewCredentialsProvider(aws.NewConfig()) + credential := options.Credential{ + AuthMechanism: "MONGODB-AWS", + AWSCredentialsProvider: awsCredentialProvider, + } + client, err := mongo.Connect( + options.Client().SetAuth(credential)) + if err != nil { + panic(err) + } + _ = client +} From 1ece12e84ae596f405bfd1f337dec7178e2a1db7 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Wed, 14 Jan 2026 16:04:00 -0500 Subject: [PATCH 05/17] add signer interface --- go.work | 2 +- internal/aws/credentials/credentials.go | 13 ++ {mongoaws => modules/mongoaws}/go.mod | 2 +- {mongoaws => modules/mongoaws}/go.sum | 0 modules/mongoaws/mongoaws.go | 155 ++++++++++++++++++ .../mongoaws}/mongoaws_example_test.go | 18 ++ modules/mongoaws/mongoaws_test.go | 118 +++++++++++++ mongo/options/clientoptions.go | 39 +++-- mongoaws/mongoaws.go | 43 ----- x/mongo/driver/auth/auth.go | 2 +- x/mongo/driver/auth/aws_conv.go | 27 ++- x/mongo/driver/auth/conversation.go | 2 +- x/mongo/driver/auth/internal/gssapi/gss.go | 2 +- x/mongo/driver/auth/mongodbaws.go | 43 ++++- x/mongo/driver/auth/oidc.go | 4 +- x/mongo/driver/auth/plain.go | 2 +- x/mongo/driver/auth/sasl.go | 8 +- x/mongo/driver/auth/scram.go | 2 +- x/mongo/driver/auth/x509.go | 2 +- x/mongo/driver/driver.go | 8 + x/mongo/driver/topology/topology_options.go | 25 +++ 21 files changed, 421 insertions(+), 96 deletions(-) rename {mongoaws => modules/mongoaws}/go.mod (93%) rename {mongoaws => modules/mongoaws}/go.sum (100%) create mode 100644 modules/mongoaws/mongoaws.go rename {mongoaws => modules/mongoaws}/mongoaws_example_test.go (61%) create mode 100644 modules/mongoaws/mongoaws_test.go delete mode 100644 mongoaws/mongoaws.go diff --git a/go.work b/go.work index a3bfa145a5..8601c66ba5 100644 --- a/go.work +++ b/go.work @@ -9,5 +9,5 @@ use ( ./internal/cmd/faas/awslambda/mongodb ./internal/test/compilecheck ./internal/test/goleak - ./mongoaws + ./modules/mongoaws ) diff --git a/internal/aws/credentials/credentials.go b/internal/aws/credentials/credentials.go index 6d46bce6ec..f01ffadd23 100644 --- a/internal/aws/credentials/credentials.go +++ b/internal/aws/credentials/credentials.go @@ -33,6 +33,19 @@ type Value struct { // AWS Session Token SessionToken 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 + // Provider used to get credentials ProviderName string } diff --git a/mongoaws/go.mod b/modules/mongoaws/go.mod similarity index 93% rename from mongoaws/go.mod rename to modules/mongoaws/go.mod index 0a6c270508..fcc517e53f 100644 --- a/mongoaws/go.mod +++ b/modules/mongoaws/go.mod @@ -5,6 +5,7 @@ go 1.23 require ( github.com/aws/aws-sdk-go-v2 v1.40.1 go.mongodb.org/mongo-driver/v2 v2.0.0-beta2 + golang.org/x/sync v0.11.0 ) require ( @@ -16,6 +17,5 @@ require ( 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 ) diff --git a/mongoaws/go.sum b/modules/mongoaws/go.sum similarity index 100% rename from mongoaws/go.sum rename to modules/mongoaws/go.sum diff --git a/modules/mongoaws/mongoaws.go b/modules/mongoaws/mongoaws.go new file mode 100644 index 0000000000..7bad9efde7 --- /dev/null +++ b/modules/mongoaws/mongoaws.go @@ -0,0 +1,155 @@ +// Copyright (C) MongoDB, Inc. 2026-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 mongoaws + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "net/http" + "sync" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "golang.org/x/sync/singleflight" + + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +type CredentialsProvider struct { + sf singleflight.Group + m sync.RWMutex + provider aws.CredentialsProvider + creds *aws.Credentials +} + +func NewCredentialsProvider(cfg *aws.Config) *CredentialsProvider { + if cfg == nil { + cfg = aws.NewConfig() + } + return &CredentialsProvider{ + provider: cfg.Credentials, + } +} + +func (p *CredentialsProvider) Retrieve(ctx context.Context) (options.AWSCredentials, error) { + // Check if credentials are cached, and not expired. + select { + case curCreds, ok := <-p.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 options.AWSCredentials(curCreds), nil + } + case <-ctx.Done(): + return options.AWSCredentials{}, ctx.Err() + } + + // 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 := p.sf.DoChan("", func() (any, error) { + return p.singleRetrieve(&suppressedContext{ctx}) + }) + select { + case res := <-resCh: + if res.Err != nil { + return options.AWSCredentials{}, res.Err + } + creds := res.Val.(aws.Credentials) + return options.AWSCredentials(creds), nil + case <-ctx.Done(): + return options.AWSCredentials{}, ctx.Err() + } +} + +func (p *CredentialsProvider) singleRetrieve(ctx context.Context) (any, error) { + p.m.Lock() + defer p.m.Unlock() + + if !p.isExpired() { + curCreds := *p.creds + return curCreds, nil + } + + creds, err := p.provider.Retrieve(ctx) + if err == nil { + curCreds := creds + p.creds = &curCreds + } + + return creds, err +} + +// asyncIsExpired returns a channel of credentials Value. If the channel is +// closed the credentials are expired and credentials value are not empty. +func (p *CredentialsProvider) asyncIsExpired() <-chan aws.Credentials { + ch := make(chan aws.Credentials, 1) + go func() { + p.m.RLock() + defer p.m.RUnlock() + + if !p.isExpired() { + curCreds := *p.creds + ch <- curCreds + } + + close(ch) + }() + + return ch +} + +func (p *CredentialsProvider) isExpired() bool { + return p.creds == nil || p.creds.Expired() +} + +func (p *CredentialsProvider) Expired() bool { + p.m.RLock() + defer p.m.RUnlock() + return p.isExpired() +} + +type suppressedContext struct { + context.Context +} + +func (s *suppressedContext) Deadline() (deadline time.Time, ok bool) { + return time.Time{}, false +} + +func (s *suppressedContext) Done() <-chan struct{} { + return nil +} + +func (s *suppressedContext) Err() error { + return nil +} + +type Signer struct { + signer v4.HTTPSigner +} + +func NewSigner(httpSigner v4.HTTPSigner) Signer { + return Signer{ + signer: httpSigner, + } +} + +func (s Signer) Sign( + ctx context.Context, creds options.AWSCredentials, r *http.Request, + payload, service, region string, signingTime time.Time) error { + if len(payload) == 0 { + payload = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } else { + hash := sha256.Sum256([]byte(payload)) + payload = hex.EncodeToString(hash[:]) + } + r.Header.Set("X-Amz-Security-Token", creds.SessionToken) + return s.signer.SignHTTP(ctx, aws.Credentials(creds), r, payload, service, region, signingTime) +} diff --git a/mongoaws/mongoaws_example_test.go b/modules/mongoaws/mongoaws_example_test.go similarity index 61% rename from mongoaws/mongoaws_example_test.go rename to modules/mongoaws/mongoaws_example_test.go index 59b6e21d0c..192421db81 100644 --- a/mongoaws/mongoaws_example_test.go +++ b/modules/mongoaws/mongoaws_example_test.go @@ -8,6 +8,7 @@ package mongoaws import ( "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) @@ -25,3 +26,20 @@ func ExampleNewCredentialsProvider() { } _ = client } + +func ExampleNewSigner() { + awsCredentialProvider := NewCredentialsProvider(aws.NewConfig()) + awsSigner := NewSigner(v4.NewSigner()) + _ = awsSigner + credential := options.Credential{ + AuthMechanism: "MONGODB-AWS", + AWSCredentialsProvider: awsCredentialProvider, + AWSSigner: awsSigner, + } + client, err := mongo.Connect( + options.Client().SetAuth(credential)) + if err != nil { + panic(err) + } + _ = client +} diff --git a/modules/mongoaws/mongoaws_test.go b/modules/mongoaws/mongoaws_test.go new file mode 100644 index 0000000000..1a8ca37e14 --- /dev/null +++ b/modules/mongoaws/mongoaws_test.go @@ -0,0 +1,118 @@ +// Copyright (C) MongoDB, Inc. 2026-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 mongoaws + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +type stubProvider struct { + creds aws.Credentials + err error +} + +func (s *stubProvider) Retrieve(_ context.Context) (aws.Credentials, error) { + return s.creds, s.err +} + +func TestCredentialsRetrieve(t *testing.T) { + p := &CredentialsProvider{ + provider: &stubProvider{ + creds: aws.Credentials{ + AccessKeyID: "AKID", + SecretAccessKey: "SECRET", + SessionToken: "Token", + }, + }, + } + + creds, err := p.Retrieve(context.Background()) + if err != nil { + t.Errorf("Expected no error, got %q", err) + } + if e, a := "AKID", creds.AccessKeyID; e != a { + t.Errorf("Expect access key ID to match, %q got %q", e, a) + } + if e, a := "SECRET", creds.SecretAccessKey; e != a { + t.Errorf("Expect secret access key to match, %q got %q", e, a) + } + if e, a := "Token", creds.SessionToken; e != a { + t.Errorf("Expect session token to match, %q got %q", e, a) + } +} + +func TestCredentialsRetrieveWithError(t *testing.T) { + p := &CredentialsProvider{ + provider: &stubProvider{ + err: fmt.Errorf("provider error"), + }, + } + + _, err := p.Retrieve(context.Background()) + if e, a := "provider error", err.Error(); e != a { + t.Errorf("Expected provider error, %q got %q", e, a) + } +} + +func TestCredentialsExpire(t *testing.T) { + stub := &stubProvider{} + p := &CredentialsProvider{ + provider: stub, + } + + if !p.Expired() { + t.Errorf("Expected to start out expired") + } + + _, err := p.Retrieve(context.Background()) + if err != nil { + t.Errorf("Expected no err, got %v", err) + } + if p.Expired() { + t.Errorf("Expected not to be expired") + } +} + +type stubProviderConcurrent struct { + stubProvider + done chan struct{} +} + +func (s *stubProviderConcurrent) Retrieve(ctx context.Context) (aws.Credentials, error) { + <-s.done + return s.stubProvider.Retrieve(ctx) +} + +func TestCredentialsRetrieveConcurrent(t *testing.T) { + stub := &stubProviderConcurrent{ + done: make(chan struct{}), + } + + p := &CredentialsProvider{ + provider: stub, + } + done := make(chan struct{}) + + for range 2 { + go func() { + _, err := p.Retrieve(context.Background()) + if err != nil { + t.Errorf("Expected no err, got %q", err) + } + done <- struct{}{} + }() + } + + // Validates that a single call to Retrieve is shared between two calls to Get + stub.done <- struct{}{} + <-done + <-done +} diff --git a/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index a5a05eb7b3..22e6bf292a 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -117,6 +117,25 @@ type Credential struct { OIDCMachineCallback OIDCCallback OIDCHumanCallback OIDCCallback AWSCredentialsProvider AWSCredentialsProvider + AWSSigner AWSSigner +} + +// OIDCCallback is the type for both Human and Machine Callback flows. +// RefreshToken will always be nil in the OIDCArgs for the Machine flow. +type OIDCCallback func(context.Context, *OIDCArgs) (*OIDCCredential, error) + +// OIDCArgs contains the arguments for the OIDC callback. +type OIDCArgs struct { + Version int + IDPInfo *IDPInfo + RefreshToken *string +} + +// OIDCCredential contains the access token and refresh token. +type OIDCCredential struct { + AccessToken string + ExpiresAt *time.Time + RefreshToken *string } // AWSCredentialsProvider is the interface used to retrieve AWS credentials. @@ -136,22 +155,10 @@ type AWSCredentials struct { AccountID string } -// OIDCCallback is the type for both Human and Machine Callback flows. -// RefreshToken will always be nil in the OIDCArgs for the Machine flow. -type OIDCCallback func(context.Context, *OIDCArgs) (*OIDCCredential, error) - -// OIDCArgs contains the arguments for the OIDC callback. -type OIDCArgs struct { - Version int - IDPInfo *IDPInfo - RefreshToken *string -} - -// OIDCCredential contains the access token and refresh token. -type OIDCCredential struct { - AccessToken string - ExpiresAt *time.Time - RefreshToken *string +// AWSSigner is an interface to a AWS SigV4 signer that can sign HTTP requests. +type AWSSigner interface { + Sign(ctx context.Context, creds AWSCredentials, r *http.Request, + payload, service, region string, signingTime time.Time) error } // IDPInfo contains the information needed to perform OIDC authentication with diff --git a/mongoaws/mongoaws.go b/mongoaws/mongoaws.go deleted file mode 100644 index 5f76e61716..0000000000 --- a/mongoaws/mongoaws.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) MongoDB, Inc. 2026-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 mongoaws - -import ( - "context" - - "github.com/aws/aws-sdk-go-v2/aws" - "go.mongodb.org/mongo-driver/v2/mongo/options" -) - -type CredentialsProvider struct { - cfg *aws.Config - - creds aws.Credentials -} - -func NewCredentialsProvider(cfg *aws.Config) *CredentialsProvider { - if cfg == nil { - cfg = aws.NewConfig() - } - return &CredentialsProvider{ - cfg: cfg, - creds: aws.Credentials{}, - } -} - -func (p *CredentialsProvider) Retrieve(ctx context.Context) (options.AWSCredentials, error) { - var err error - p.creds, err = p.cfg.Credentials.Retrieve(ctx) - if err != nil { - return options.AWSCredentials{}, err - } - return options.AWSCredentials(p.creds), nil -} - -func (p *CredentialsProvider) Expired() bool { - return p.creds.Expired() -} diff --git a/x/mongo/driver/auth/auth.go b/x/mongo/driver/auth/auth.go index bfc7bbdb9f..918d23d16a 100644 --- a/x/mongo/driver/auth/auth.go +++ b/x/mongo/driver/auth/auth.go @@ -122,7 +122,7 @@ func (ah *authHandshaker) GetHandshakeInformation( // cannot perform speculative authentication. An example of this is MONGODB-OIDC when there is // no AccessToken in the cache. if ah.conversation != nil { - firstMsg, err := ah.conversation.FirstMessage() + firstMsg, err := ah.conversation.FirstMessage(ctx) if err != nil { return driver.HandshakeInformation{}, newAuthError("failed to create speculative authentication message", err) } diff --git a/x/mongo/driver/auth/aws_conv.go b/x/mongo/driver/auth/aws_conv.go index d0eef0ee3b..92e91d7b84 100644 --- a/x/mongo/driver/auth/aws_conv.go +++ b/x/mongo/driver/auth/aws_conv.go @@ -43,16 +43,16 @@ type awsSaslAdapter struct { var _ SaslClient = (*awsSaslAdapter)(nil) -func (a *awsSaslAdapter) Start() (string, []byte, error) { - step, err := a.step(nil) +func (a *awsSaslAdapter) Start(ctx context.Context) (string, []byte, error) { + step, err := a.step(ctx, 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) +func (a *awsSaslAdapter) Next(ctx context.Context, challenge []byte) ([]byte, error) { + step, err := a.step(ctx, challenge) if err != nil { return nil, err } @@ -67,7 +67,7 @@ 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 @@ -75,14 +75,14 @@ const ( // 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 (a *awsSaslAdapter) step(challenge []byte) (response []byte, err error) { +func (a *awsSaslAdapter) step(ctx context.Context, challenge []byte) (response []byte, err error) { switch a.state { case clientStarting: a.state = clientFirst response = a.firstMsg() case clientFirst: a.state = clientFinal - response, err = a.finalMsg(challenge) + response, err = a.finalMsg(ctx, challenge) case clientFinal: a.state = clientDone default: @@ -129,7 +129,7 @@ func (a *awsSaslAdapter) firstMsg() []byte { return msg } -func (a *awsSaslAdapter) finalMsg(s1 []byte) ([]byte, error) { +func (a *awsSaslAdapter) finalMsg(ctx context.Context, s1 []byte) ([]byte, error) { var sm struct { Nonce bson.Binary `bson:"s"` Host string `bson:"h"` @@ -143,8 +143,8 @@ func (a *awsSaslAdapter) 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, a.nonce) { return nil, errors.New("server nonce did not extend client nonce") @@ -158,7 +158,6 @@ func (a *awsSaslAdapter) finalMsg(s1 []byte) ([]byte, error) { currentTime := time.Now().UTC() body := "Action=GetCallerIdentity&Version=2011-06-15" - timeStr := currentTime.Format(amzDateFormat) // Create http.Request req, err := http.NewRequest("POST", "/", strings.NewReader(body)) if err != nil { @@ -167,12 +166,12 @@ func (a *awsSaslAdapter) finalMsg(s1 []byte) ([]byte, error) { req.Host = sm.Host req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Length", "43") - req.Header.Set("X-Amz-Date", timeStr) + req.Header.Set("X-Amz-Date", currentTime.Format(amzDateFormat)) req.Header.Set("X-MongoDB-Server-Nonce", base64.StdEncoding.EncodeToString(sm.Nonce.Data)) req.Header.Set("X-MongoDB-GS2-CB-Flag", "n") // Get signed header - err = a.signer.Sign(context.Background(), req, body, "sts", region, currentTime) + err = a.signer.Sign(ctx, req, body, "sts", region, currentTime) if err != nil { return nil, err } @@ -180,7 +179,7 @@ func (a *awsSaslAdapter) finalMsg(s1 []byte) ([]byte, error) { // create message idx, msg := bsoncore.AppendDocumentStart(nil) msg = bsoncore.AppendStringElement(msg, "a", req.Header.Get("Authorization")) - msg = bsoncore.AppendStringElement(msg, "d", timeStr) + msg = bsoncore.AppendStringElement(msg, "d", req.Header.Get("X-Amz-Date")) if sessionToken := req.Header.Get("X-Amz-Security-Token"); len(sessionToken) > 0 { msg = bsoncore.AppendStringElement(msg, "t", sessionToken) } diff --git a/x/mongo/driver/auth/conversation.go b/x/mongo/driver/auth/conversation.go index 13839d7f8d..17bd84c8aa 100644 --- a/x/mongo/driver/auth/conversation.go +++ b/x/mongo/driver/auth/conversation.go @@ -22,7 +22,7 @@ import ( // Finish takes the server response to the initial message and conducts the remainder of the conversation to // authenticate the provided connection. type SpeculativeConversation interface { - FirstMessage() (bsoncore.Document, error) + FirstMessage(ctx context.Context) (bsoncore.Document, error) Finish(ctx context.Context, cfg *driver.AuthConfig, firstResponse bsoncore.Document) error } diff --git a/x/mongo/driver/auth/internal/gssapi/gss.go b/x/mongo/driver/auth/internal/gssapi/gss.go index 496057882d..95d85cfc9d 100644 --- a/x/mongo/driver/auth/internal/gssapi/gss.go +++ b/x/mongo/driver/auth/internal/gssapi/gss.go @@ -71,7 +71,7 @@ func (sc *SaslClient) Close() { C.gssapi_client_destroy(&sc.state) } -func (sc *SaslClient) Start() (string, []byte, error) { +func (sc *SaslClient) Start(context.Context) (string, []byte, error) { const mechName = "GSSAPI" cservicePrincipalName := C.CString(sc.servicePrincipalName) diff --git a/x/mongo/driver/auth/mongodbaws.go b/x/mongo/driver/auth/mongodbaws.go index b33205f3ba..0bcd7c4329 100644 --- a/x/mongo/driver/auth/mongodbaws.go +++ b/x/mongo/driver/auth/mongodbaws.go @@ -27,6 +27,19 @@ 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 cred.AWSSigner != nil { + if cred.AWSCredentialsProvider == nil { + return nil, errors.New("AWSCredentialsProvider is required when AWSSigner is set") + } + return &MongoDBAWSAuthenticator{ + signer: customSigner{ + provider: cred.AWSCredentialsProvider, + signer: cred.AWSSigner, + }, + }, nil + } + if httpClient == nil { return nil, errors.New("httpClient must not be nil") } @@ -45,16 +58,15 @@ func newMongoDBAWSAuthenticator(cred *Cred, httpClient *http.Client) (Authentica } return &MongoDBAWSAuthenticator{ - signer: &builtInV4Signer{ - providers: providers, - httpClient: httpClient, + signer: builtInV4Signer{ + creds: creds.NewAWSCredentialProvider(httpClient, providers...).Cred, }, }, nil } // MongoDBAWSAuthenticator uses AWS-IAM credentials over SASL to authenticate a connection. type MongoDBAWSAuthenticator struct { - signer *builtInV4Signer + signer awsSigner } // Auth authenticates the connection. @@ -75,13 +87,26 @@ func (a *MongoDBAWSAuthenticator) Reauth(_ context.Context, _ *driver.AuthConfig } type builtInV4Signer struct { - providers []credentials.Provider - httpClient *http.Client + creds *credentials.Credentials } -func (b *builtInV4Signer) Sign(_ context.Context, req *http.Request, body, service, region string, signTime time.Time) error { - awsProvider := creds.NewAWSCredentialProvider(b.httpClient, b.providers...) - signer := v4signer.NewSigner(awsProvider.Cred) +func (s builtInV4Signer) Sign(_ context.Context, req *http.Request, body, service, region string, signTime time.Time) error { + signer := v4signer.NewSigner(s.creds) _, err := signer.Sign(req, strings.NewReader(body), service, region, signTime) return err } + +type customSigner struct { + provider credentials.Provider + signer driver.AWSSigner +} + +func (s customSigner) Sign( + ctx context.Context, req *http.Request, body, service, region string, + signTime time.Time) error { + creds, err := s.provider.Retrieve(ctx) + if err != nil { + return err + } + return s.signer.Sign(ctx, creds, req, body, service, region, signTime) +} diff --git a/x/mongo/driver/auth/oidc.go b/x/mongo/driver/auth/oidc.go index 11d526210c..fc4bf98265 100644 --- a/x/mongo/driver/auth/oidc.go +++ b/x/mongo/driver/auth/oidc.go @@ -230,7 +230,7 @@ func principalStepRequest(principal string) []byte { return doc.Build() } -func (oos *oidcOneStep) Start() (string, []byte, error) { +func (oos *oidcOneStep) Start(context.Context) (string, []byte, error) { return MongoDBOIDC, jwtStepRequest(oos.accessToken), nil } @@ -242,7 +242,7 @@ func (*oidcOneStep) Completed() bool { return true } -func (ots *oidcTwoStep) Start() (string, []byte, error) { +func (ots *oidcTwoStep) Start(context.Context) (string, []byte, error) { return MongoDBOIDC, principalStepRequest(ots.oa.userName), nil } diff --git a/x/mongo/driver/auth/plain.go b/x/mongo/driver/auth/plain.go index 69342bb1e5..32edefeb2d 100644 --- a/x/mongo/driver/auth/plain.go +++ b/x/mongo/driver/auth/plain.go @@ -64,7 +64,7 @@ type plainSaslClient struct { var _ SaslClient = (*plainSaslClient)(nil) -func (c *plainSaslClient) Start() (string, []byte, error) { +func (c *plainSaslClient) Start(context.Context) (string, []byte, error) { b := []byte("\x00" + c.username + "\x00" + c.password) return PLAIN, b, nil } diff --git a/x/mongo/driver/auth/sasl.go b/x/mongo/driver/auth/sasl.go index 659fe45e2d..344cdab4b4 100644 --- a/x/mongo/driver/auth/sasl.go +++ b/x/mongo/driver/auth/sasl.go @@ -18,7 +18,7 @@ import ( // SaslClient is the client piece of a sasl conversation. type SaslClient interface { - Start() (string, []byte, error) + Start(ctx context.Context) (string, []byte, error) Next(ctx context.Context, challenge []byte) ([]byte, error) Completed() bool } @@ -59,10 +59,10 @@ func newSaslConversation(client SaslClient, source string, speculative bool) *sa // FirstMessage returns the first message to be sent to the server. This message contains a "db" field so it can be used // for speculative authentication. -func (sc *saslConversation) FirstMessage() (bsoncore.Document, error) { +func (sc *saslConversation) FirstMessage(ctx context.Context) (bsoncore.Document, error) { var payload []byte var err error - sc.mechanism, payload, err = sc.client.Start() + sc.mechanism, payload, err = sc.client.Start(ctx) if err != nil { return nil, err } @@ -156,7 +156,7 @@ func (sc *saslConversation) Finish(ctx context.Context, cfg *driver.AuthConfig, func ConductSaslConversation(ctx context.Context, cfg *driver.AuthConfig, authSource string, client SaslClient) error { // Create a non-speculative SASL conversation. conversation := newSaslConversation(client, authSource, false) - saslStartDoc, err := conversation.FirstMessage() + saslStartDoc, err := conversation.FirstMessage(ctx) if err != nil { return newError(err, conversation.mechanism) } diff --git a/x/mongo/driver/auth/scram.go b/x/mongo/driver/auth/scram.go index 2896a8facc..6663ae0779 100644 --- a/x/mongo/driver/auth/scram.go +++ b/x/mongo/driver/auth/scram.go @@ -119,7 +119,7 @@ type scramSaslAdapter struct { var _ SaslClient = (*scramSaslAdapter)(nil) var _ ExtraOptionsSaslClient = (*scramSaslAdapter)(nil) -func (a *scramSaslAdapter) Start() (string, []byte, error) { +func (a *scramSaslAdapter) Start(context.Context) (string, []byte, error) { step, err := a.conversation.Step("") if err != nil { return a.mechanism, nil, err diff --git a/x/mongo/driver/auth/x509.go b/x/mongo/driver/auth/x509.go index f839023435..8f3b5df066 100644 --- a/x/mongo/driver/auth/x509.go +++ b/x/mongo/driver/auth/x509.go @@ -39,7 +39,7 @@ type x509Conversation struct{} var _ SpeculativeConversation = (*x509Conversation)(nil) // FirstMessage returns the first message to be sent to the server. -func (c *x509Conversation) FirstMessage() (bsoncore.Document, error) { +func (c *x509Conversation) FirstMessage(context.Context) (bsoncore.Document, error) { return createFirstX509Message(), nil } diff --git a/x/mongo/driver/driver.go b/x/mongo/driver/driver.go index 1ceb458fe4..227df2b06c 100644 --- a/x/mongo/driver/driver.go +++ b/x/mongo/driver/driver.go @@ -15,6 +15,7 @@ package driver import ( "context" + "net/http" "time" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" @@ -82,6 +83,7 @@ type Cred struct { OIDCMachineCallback OIDCCallback OIDCHumanCallback OIDCCallback AWSCredentialsProvider AWSCredentialsProvider + AWSSigner AWSSigner } // AWSCredentialsProvider is the interface used to retrieve AWS credentials. @@ -90,6 +92,12 @@ type AWSCredentialsProvider interface { IsExpired() bool } +// AWSSigner is an interface to a AWS SigV4 signer that can sign HTTP requests. +type AWSSigner interface { + Sign(ctx context.Context, creds credentials.Value, r *http.Request, + payload, service, region string, signingTime time.Time) error +} + // Deployment is implemented by types that can select a server from a deployment. type Deployment interface { SelectServer(context.Context, description.ServerSelector) (Server, error) diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index 2ffaeb9f99..309da23ed5 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -130,6 +130,10 @@ func ConvertCreds(credOpts *options.Credential) *driver.Cred { } } + if credOpts.AWSSigner != nil { + cred.AWSSigner = awsSigner{credOpts.AWSSigner} + } + return cred } @@ -146,6 +150,10 @@ func (p awsCredentialsProvider) Retrieve(ctx context.Context) (credentials.Value AccessKeyID: creds.AccessKeyID, SecretAccessKey: creds.SecretAccessKey, SessionToken: creds.SessionToken, + Source: creds.Source, + CanExpire: creds.CanExpire, + Expires: creds.Expires, + AccountID: creds.AccountID, ProviderName: "AwsProvider", }, nil } @@ -154,6 +162,23 @@ func (p awsCredentialsProvider) IsExpired() bool { return p.provider.Expired() } +type awsSigner struct { + signer options.AWSSigner +} + +func (s awsSigner) Sign(ctx context.Context, creds credentials.Value, r *http.Request, + payload, service, region string, signingTime time.Time) error { + return s.signer.Sign(ctx, options.AWSCredentials{ + AccessKeyID: creds.AccessKeyID, + SecretAccessKey: creds.SecretAccessKey, + SessionToken: creds.SessionToken, + Source: creds.Source, + CanExpire: creds.CanExpire, + Expires: creds.Expires, + AccountID: creds.AccountID, + }, r, payload, service, region, signingTime) +} + // NewConfig will translate data from client options into a topology config for // building non-default deployments. Server and topology options are not honored // if a custom deployment is used. From 875316d88c6d970c2663b8d74f06595e6eafd04d Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 15 Jan 2026 11:33:38 -0500 Subject: [PATCH 06/17] add custom signer test --- modules/mongoaws/mongoaws.go | 2 +- x/mongo/driver/auth/mongodbaws_test.go | 66 ++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/modules/mongoaws/mongoaws.go b/modules/mongoaws/mongoaws.go index 7bad9efde7..fc30bfae63 100644 --- a/modules/mongoaws/mongoaws.go +++ b/modules/mongoaws/mongoaws.go @@ -41,7 +41,7 @@ func (p *CredentialsProvider) Retrieve(ctx context.Context) (options.AWSCredenti // Check if credentials are cached, and not expired. select { case curCreds, ok := <-p.asyncIsExpired(): - // ok will only be true, of the credentials were not expired. ok will + // ok will only be true, if the credentials were not expired. ok will // be false and have no value if the credentials are expired. if ok { return options.AWSCredentials(curCreds), nil diff --git a/x/mongo/driver/auth/mongodbaws_test.go b/x/mongo/driver/auth/mongodbaws_test.go index ac4e53d428..33726059b2 100644 --- a/x/mongo/driver/auth/mongodbaws_test.go +++ b/x/mongo/driver/auth/mongodbaws_test.go @@ -11,6 +11,7 @@ import ( "errors" "net/http" "testing" + "time" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/internal/assert" @@ -58,24 +59,33 @@ func TestGetRegion(t *testing.T) { } -type awsCredentialsProvider struct { +type testAWSCredentialsProvider struct { cnt int } -func (a *awsCredentialsProvider) Retrieve(_ context.Context) (credentials.Value, error) { +func (a *testAWSCredentialsProvider) Retrieve(_ context.Context) (credentials.Value, error) { a.cnt++ return credentials.Value{}, nil } -func (*awsCredentialsProvider) IsExpired() bool { +func (*testAWSCredentialsProvider) IsExpired() bool { return false } -func TestAWSCustomCredentialProvider(t *testing.T) { +type testAWSSigner struct { + cnt int +} + +func (s *testAWSSigner) Sign(_ context.Context, _ credentials.Value, _ *http.Request, _, _, _ string, _ time.Time) error { + s.cnt++ + return 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 := &awsCredentialsProvider{} + provider := &testAWSCredentialsProvider{} for _, tc := range []struct { name string cred *Cred @@ -137,6 +147,52 @@ func TestAWSCustomCredentialProvider(t *testing.T) { } } +func TestAWSCustomSigner(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{} + signer := &testAWSSigner{} + cred := &Cred{ + Username: "user", + Password: "pass", + Props: map[string]string{"AWS_SESSION_TOKEN": "token"}, + AWSCredentialsProvider: provider, + AWSSigner: signer, + } + + authenticator, err := newMongoDBAWSAuthenticator(cred, nil) + 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, 1, provider.cnt, "expected provider to be called once but got %v", provider.cnt) + assert.Equalf(t, 1, signer.cnt, "expected signer to be called once but got %v", signer.cnt) +} + func processWm(resps, written chan []byte, errChan chan error) { buf := <-written buf, ok := extractPayload(buf) From 38501d340a3d9e7ba0c0f6d731993d13048f61bd Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 15 Jan 2026 21:50:50 -0500 Subject: [PATCH 07/17] fixes; aws submodule updates --- .../client_side_encryption_prose_test.go | 4 +-- modules/mongoaws/doc.go | 20 +++++++++++ modules/mongoaws/mongoaws.go | 34 +++++++++++++++---- modules/mongoaws/mongoaws_example_test.go | 6 ++-- modules/mongoaws/mongoaws_test.go | 2 +- mongo/client_encryption.go | 4 +++ 6 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 modules/mongoaws/doc.go diff --git a/internal/integration/client_side_encryption_prose_test.go b/internal/integration/client_side_encryption_prose_test.go index db94daa8b8..0c9a55e5b0 100644 --- a/internal/integration/client_side_encryption_prose_test.go +++ b/internal/integration/client_side_encryption_prose_test.go @@ -3230,8 +3230,8 @@ func TestCustomAwsCredentialsProse(t *testing.T) { }) 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_SECRET_ACCESS_KEY")) - mt.Setenv("AWS_SECRET_ACCESS_KEY", os.Getenv("FLE_AWS_ACCESS_KEY_ID")) + 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) diff --git a/modules/mongoaws/doc.go b/modules/mongoaws/doc.go new file mode 100644 index 0000000000..a0843a9fff --- /dev/null +++ b/modules/mongoaws/doc.go @@ -0,0 +1,20 @@ +// 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 v2 AWS SDK to be +// supplied to the MongoDB Go Driver for use with the MONGODB-AWS authentication +// mechanism. +// +// NewCredentialsProvider() adapts an AWS CredentialsProvider to be used in: +// +// ClientOptions +// ClinetEncryptionOptions +// AutoEncryptionOptions +// +// NewSigner() adapts an AWS HTTPSigner to be used in: +// +// ClientOptions +package mongoaws diff --git a/modules/mongoaws/mongoaws.go b/modules/mongoaws/mongoaws.go index fc30bfae63..31842170fe 100644 --- a/modules/mongoaws/mongoaws.go +++ b/modules/mongoaws/mongoaws.go @@ -1,4 +1,4 @@ -// Copyright (C) MongoDB, Inc. 2026-present. +// 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 @@ -21,6 +21,16 @@ import ( "go.mongodb.org/mongo-driver/v2/mongo/options" ) +const ( + // emptyStringSHA256 is a SHA256 of an empty string + emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` +) + +var _ options.AWSCredentialsProvider = (*CredentialsProvider)(nil) + +// CredentialsProvider is an implementation of options.AWSCredentialsProvider in +// MongoDB Go driver. It provides caching and concurrency safe credentials retrieval +// via the provider's retrieve method. type CredentialsProvider struct { sf singleflight.Group m sync.RWMutex @@ -28,15 +38,18 @@ type CredentialsProvider struct { creds *aws.Credentials } -func NewCredentialsProvider(cfg *aws.Config) *CredentialsProvider { - if cfg == nil { - cfg = aws.NewConfig() - } +// NewCredentialsProvider returns a CredentialsProvider that wraps AWS +// CredentialsProvider. Provider is expected to not be nil. +func NewCredentialsProvider(provider aws.CredentialsProvider) *CredentialsProvider { return &CredentialsProvider{ - provider: cfg.Credentials, + provider: provider, } } +// Retrieve returns the credentials. If the credentials have already been +// retrieved, and not expired the cached credentials will be returned. If the +// credentials have not been retrieved yet, or expired the provider's Retrieve +// method will be called. func (p *CredentialsProvider) Retrieve(ctx context.Context) (options.AWSCredentials, error) { // Check if credentials are cached, and not expired. select { @@ -109,6 +122,8 @@ func (p *CredentialsProvider) isExpired() bool { return p.creds == nil || p.creds.Expired() } +// Expired returns if the cached credentials are no longer valid, and need +// to be retrieved. func (p *CredentialsProvider) Expired() bool { p.m.RLock() defer p.m.RUnlock() @@ -131,21 +146,26 @@ func (s *suppressedContext) Err() error { return nil } +var _ options.AWSSigner = (*Signer)(nil) + +// Signer is an implementation of options.AWSSigner in MongoDB Go driver. type Signer struct { signer v4.HTTPSigner } +// NewSigner creates a new Signer from the provided AWS HTTPSigner. func NewSigner(httpSigner v4.HTTPSigner) Signer { return Signer{ signer: httpSigner, } } +// Sign signs AWS v4 requests. func (s Signer) Sign( ctx context.Context, creds options.AWSCredentials, r *http.Request, payload, service, region string, signingTime time.Time) error { if len(payload) == 0 { - payload = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + payload = emptyStringSHA256 } else { hash := sha256.Sum256([]byte(payload)) payload = hex.EncodeToString(hash[:]) diff --git a/modules/mongoaws/mongoaws_example_test.go b/modules/mongoaws/mongoaws_example_test.go index 192421db81..d7d6676d7a 100644 --- a/modules/mongoaws/mongoaws_example_test.go +++ b/modules/mongoaws/mongoaws_example_test.go @@ -1,4 +1,4 @@ -// Copyright (C) MongoDB, Inc. 2026-present. +// 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 @@ -14,7 +14,7 @@ import ( ) func ExampleNewCredentialsProvider() { - awsCredentialProvider := NewCredentialsProvider(aws.NewConfig()) + awsCredentialProvider := NewCredentialsProvider(aws.NewConfig().Credentials) credential := options.Credential{ AuthMechanism: "MONGODB-AWS", AWSCredentialsProvider: awsCredentialProvider, @@ -28,7 +28,7 @@ func ExampleNewCredentialsProvider() { } func ExampleNewSigner() { - awsCredentialProvider := NewCredentialsProvider(aws.NewConfig()) + awsCredentialProvider := NewCredentialsProvider(aws.NewConfig().Credentials) awsSigner := NewSigner(v4.NewSigner()) _ = awsSigner credential := options.Credential{ diff --git a/modules/mongoaws/mongoaws_test.go b/modules/mongoaws/mongoaws_test.go index 1a8ca37e14..f55b344b7a 100644 --- a/modules/mongoaws/mongoaws_test.go +++ b/modules/mongoaws/mongoaws_test.go @@ -1,4 +1,4 @@ -// Copyright (C) MongoDB, Inc. 2026-present. +// 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 diff --git a/mongo/client_encryption.go b/mongo/client_encryption.go index aea0a8fd81..7c7513a89c 100644 --- a/mongo/client_encryption.go +++ b/mongo/client_encryption.go @@ -44,6 +44,10 @@ func (p awsCredentialsProvider) Retrieve(ctx context.Context) (credentials.Value AccessKeyID: creds.AccessKeyID, SecretAccessKey: creds.SecretAccessKey, SessionToken: creds.SessionToken, + Source: creds.Source, + CanExpire: creds.CanExpire, + Expires: creds.Expires, + AccountID: creds.AccountID, ProviderName: "AwsProvider", }, nil } From ec2df0904e4fc88b54297b428cd17c56cc593dcf Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 16 Jan 2026 10:59:32 -0500 Subject: [PATCH 08/17] update provider interface --- internal/aws/credentials/chain_provider.go | 31 +---- .../aws/credentials/chain_provider_test.go | 50 +------- internal/aws/credentials/credentials.go | 95 ++++++-------- internal/aws/credentials/credentials_test.go | 97 ++------------ internal/aws/signer/v4/v4.go | 2 +- .../credproviders/assume_role_provider.go | 11 +- internal/credproviders/ec2_provider.go | 11 +- internal/credproviders/ecs_provider.go | 11 +- internal/credproviders/env_provider.go | 14 +-- internal/credproviders/imds_provider.go | 10 +- internal/credproviders/static_provider.go | 7 -- .../client_side_encryption_prose_test.go | 4 - modules/mongoaws/go.mod | 2 +- modules/mongoaws/mongoaws.go | 103 +-------------- modules/mongoaws/mongoaws_test.go | 118 ------------------ mongo/client_encryption.go | 4 - mongo/client_examples_test.go | 4 - mongo/options/clientoptions.go | 1 - x/mongo/driver/auth/creds/awscreds.go | 2 +- x/mongo/driver/auth/creds/azurecreds.go | 2 +- .../driver/auth/creds/credscaching_test.go | 18 ++- x/mongo/driver/auth/mongodbaws_test.go | 4 - x/mongo/driver/driver.go | 1 - x/mongo/driver/topology/topology_options.go | 4 - 24 files changed, 88 insertions(+), 518 deletions(-) delete mode 100644 modules/mongoaws/mongoaws_test.go diff --git a/internal/aws/credentials/chain_provider.go b/internal/aws/credentials/chain_provider.go index aca9ec77d9..e271a02857 100644 --- a/internal/aws/credentials/chain_provider.go +++ b/internal/aws/credentials/chain_provider.go @@ -12,26 +12,19 @@ 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 @@ -44,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(ctx context.Context) (Value, error) { var errs = make([]error, 0, len(c.Providers)) for _, p := range c.Providers { 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 var 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 4514a8f122..f3099a7227 100644 --- a/internal/aws/credentials/chain_provider_test.go +++ b/internal/aws/credentials/chain_provider_test.go @@ -19,19 +19,14 @@ import ( ) type secondStubProvider struct { - creds Value - expired bool - err error + creds Value + err error } func (s *secondStubProvider) Retrieve(_ context.Context) (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) { p := &ChainProvider{ @@ -106,49 +101,11 @@ func TestChainProviderGet(t *testing.T) { } } -func TestChainProviderIsExpired(t *testing.T) { - stubProvider := &stubProvider{expired: true} - p := &ChainProvider{ - Providers: []Provider{ - stubProvider, - }, - } - - ctx := context.Background() - - if !p.IsExpired() { - t.Errorf("Expect expired to be true before any Retrieve") - } - _, err := p.Retrieve(ctx) - 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(ctx) - 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(context.Background()) if err.Error() != "NoCredentialProviders: no valid providers in chain" { t.Errorf("Expect no providers error returned, got %v", err) @@ -167,9 +124,6 @@ func TestChainProviderWithNoValidProvider(t *testing.T) { }, } - if !p.IsExpired() { - t.Errorf("Expect expired with no providers") - } _, err := p.Retrieve(context.Background()) expectErr := awserr.NewBatchError("NoCredentialProviders", "no valid providers in chain", errs) diff --git a/internal/aws/credentials/credentials.go b/internal/aws/credentials/credentials.go index f01ffadd23..aaec9c8400 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" @@ -50,6 +50,18 @@ type Value struct { ProviderName 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 // SecretAccessKey value set. func (v Value) HasKeys() bool { @@ -66,10 +78,6 @@ type Provider interface { // Retrieve returns nil if it successfully retrieved the value. // Error is returned if the value were not obtainable, or empty. Retrieve(context.Context) (Value, error) - - // IsExpired returns if the credentials are no longer valid, and need - // to be retrieved. - IsExpired() bool } // A Credentials provides concurrency safe retrieval of AWS credentials Value. @@ -85,13 +93,12 @@ type Provider 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. @@ -102,34 +109,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 { @@ -141,43 +132,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 } - creds, err := c.provider.Retrieve(ctx) + 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 4847441264..a6a887a5e9 100644 --- a/internal/aws/credentials/credentials_test.go +++ b/internal/aws/credentials/credentials_test.go @@ -12,36 +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(_ context.Context) (Value, error) { - s.retrievedCount++ - s.expired = false s.creds.ProviderName = "stubProvider" return s.creds, s.err } -func (s *stubProvider) IsExpired() bool { - return s.expired -} func TestCredentialsGet(t *testing.T) { c := NewCredentials(&stubProvider{ @@ -50,10 +35,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) } @@ -69,43 +53,20 @@ 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()) + creds, err := c.Get(context.Background()) if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -114,49 +75,6 @@ func TestCredentialsGetWithProviderName(t *testing.T) { } } -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(_ context.Context) (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{} @@ -177,7 +95,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) } @@ -186,6 +104,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..69a2813ea8 100644 --- a/internal/aws/signer/v4/v4.go +++ b/internal/aws/signer/v4/v4.go @@ -135,7 +135,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/credproviders/assume_role_provider.go b/internal/credproviders/assume_role_provider.go index 36aa939336..f8027c2e5e 100644 --- a/internal/credproviders/assume_role_provider.go +++ b/internal/credproviders/assume_role_provider.go @@ -33,12 +33,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 + // So a ExpiryWindow of 10s would cause calls to Expired() to return true // 10 seconds before the credentials are actually expired. expiryWindow time.Duration } @@ -131,13 +130,9 @@ func (a *AssumeRoleProvider) Retrieve(ctx context.Context) (credentials.Value, e 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 } - -// 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 3b299f894c..b6852eb093 100644 --- a/internal/credproviders/ec2_provider.go +++ b/internal/credproviders/ec2_provider.go @@ -32,12 +32,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 + // So a ExpiryWindow of 10s would cause calls to Expired() to return true // 10 seconds before the credentials are actually expired. expiryWindow time.Duration } @@ -167,12 +166,8 @@ func (e *EC2Provider) Retrieve(ctx context.Context) (credentials.Value, error) { 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 } - -// 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 a292fe2ee3..e61f5156c3 100644 --- a/internal/credproviders/ecs_provider.go +++ b/internal/credproviders/ecs_provider.go @@ -29,12 +29,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 + // So a ExpiryWindow of 10s would cause calls to Expired() to return true // 10 seconds before the credentials are actually expired. expiryWindow time.Duration } @@ -96,12 +95,8 @@ func (e *ECSProvider) Retrieve(ctx context.Context) (credentials.Value, error) { 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 } - -// 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 7c77c18b98..fa70e376f2 100644 --- a/internal/credproviders/env_provider.go +++ b/internal/credproviders/env_provider.go @@ -30,8 +30,6 @@ type EnvProvider struct { AwsAccessKeyIDEnv EnvVar AwsSecretAccessKeyEnv EnvVar AwsSessionTokenEnv EnvVar - - retrieved bool } // NewEnvProvider returns a pointer to an ECS credential provider. @@ -48,23 +46,17 @@ func NewEnvProvider() *EnvProvider { // Retrieve retrieves the keys from the environment. func (e *EnvProvider) Retrieve(_ context.Context) (credentials.Value, error) { - e.retrieved = false - 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 { + 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 f2a801a9e2..5e91d10970 100644 --- a/internal/credproviders/imds_provider.go +++ b/internal/credproviders/imds_provider.go @@ -28,7 +28,6 @@ const ( // An AzureProvider retrieves credentials from Azure IMDS. type AzureProvider struct { httpClient *http.Client - expiration time.Time expiryWindow time.Duration } @@ -36,7 +35,6 @@ type AzureProvider struct { func NewAzureProvider(httpClient *http.Client, expiryWindow time.Duration) *AzureProvider { return &AzureProvider{ httpClient: httpClient, - expiration: time.Time{}, expiryWindow: expiryWindow, } } @@ -85,14 +83,10 @@ func (a *AzureProvider) Retrieve(ctx context.Context) (credentials.Value, error) 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 } - -// 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 4a12664f4a..51c8847a25 100644 --- a/internal/credproviders/static_provider.go +++ b/internal/credproviders/static_provider.go @@ -51,10 +51,3 @@ func (s *StaticProvider) Retrieve(_ context.Context) (credentials.Value, error) } 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/integration/client_side_encryption_prose_test.go b/internal/integration/client_side_encryption_prose_test.go index 0c9a55e5b0..dec7693054 100644 --- a/internal/integration/client_side_encryption_prose_test.go +++ b/internal/integration/client_side_encryption_prose_test.go @@ -3158,10 +3158,6 @@ func (p *awsCredentialsProvider) Retrieve(ctx context.Context) (options.AWSCrede }, nil } -func (p *awsCredentialsProvider) Expired() bool { - return false -} - func TestCustomAwsCredentialsProse(t *testing.T) { mt := mtest.New(t, mtest.NewOptions().CreateClient(false)) diff --git a/modules/mongoaws/go.mod b/modules/mongoaws/go.mod index fcc517e53f..0a6c270508 100644 --- a/modules/mongoaws/go.mod +++ b/modules/mongoaws/go.mod @@ -5,7 +5,6 @@ go 1.23 require ( github.com/aws/aws-sdk-go-v2 v1.40.1 go.mongodb.org/mongo-driver/v2 v2.0.0-beta2 - golang.org/x/sync v0.11.0 ) require ( @@ -17,5 +16,6 @@ require ( 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 ) diff --git a/modules/mongoaws/mongoaws.go b/modules/mongoaws/mongoaws.go index 31842170fe..1edb2e6315 100644 --- a/modules/mongoaws/mongoaws.go +++ b/modules/mongoaws/mongoaws.go @@ -11,12 +11,10 @@ import ( "crypto/sha256" "encoding/hex" "net/http" - "sync" "time" "github.com/aws/aws-sdk-go-v2/aws" v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" - "golang.org/x/sync/singleflight" "go.mongodb.org/mongo-driver/v2/mongo/options" ) @@ -32,10 +30,7 @@ var _ options.AWSCredentialsProvider = (*CredentialsProvider)(nil) // MongoDB Go driver. It provides caching and concurrency safe credentials retrieval // via the provider's retrieve method. type CredentialsProvider struct { - sf singleflight.Group - m sync.RWMutex provider aws.CredentialsProvider - creds *aws.Credentials } // NewCredentialsProvider returns a CredentialsProvider that wraps AWS @@ -46,104 +41,10 @@ func NewCredentialsProvider(provider aws.CredentialsProvider) *CredentialsProvid } } -// Retrieve returns the credentials. If the credentials have already been -// retrieved, and not expired the cached credentials will be returned. If the -// credentials have not been retrieved yet, or expired the provider's Retrieve -// method will be called. +// Retrieve returns the credentials. func (p *CredentialsProvider) Retrieve(ctx context.Context) (options.AWSCredentials, error) { - // Check if credentials are cached, and not expired. - select { - case curCreds, ok := <-p.asyncIsExpired(): - // ok will only be true, if the credentials were not expired. ok will - // be false and have no value if the credentials are expired. - if ok { - return options.AWSCredentials(curCreds), nil - } - case <-ctx.Done(): - return options.AWSCredentials{}, ctx.Err() - } - - // 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 := p.sf.DoChan("", func() (any, error) { - return p.singleRetrieve(&suppressedContext{ctx}) - }) - select { - case res := <-resCh: - if res.Err != nil { - return options.AWSCredentials{}, res.Err - } - creds := res.Val.(aws.Credentials) - return options.AWSCredentials(creds), nil - case <-ctx.Done(): - return options.AWSCredentials{}, ctx.Err() - } -} - -func (p *CredentialsProvider) singleRetrieve(ctx context.Context) (any, error) { - p.m.Lock() - defer p.m.Unlock() - - if !p.isExpired() { - curCreds := *p.creds - return curCreds, nil - } - creds, err := p.provider.Retrieve(ctx) - if err == nil { - curCreds := creds - p.creds = &curCreds - } - - return creds, err -} - -// asyncIsExpired returns a channel of credentials Value. If the channel is -// closed the credentials are expired and credentials value are not empty. -func (p *CredentialsProvider) asyncIsExpired() <-chan aws.Credentials { - ch := make(chan aws.Credentials, 1) - go func() { - p.m.RLock() - defer p.m.RUnlock() - - if !p.isExpired() { - curCreds := *p.creds - ch <- curCreds - } - - close(ch) - }() - - return ch -} - -func (p *CredentialsProvider) isExpired() bool { - return p.creds == nil || p.creds.Expired() -} - -// Expired returns if the cached credentials are no longer valid, and need -// to be retrieved. -func (p *CredentialsProvider) Expired() bool { - p.m.RLock() - defer p.m.RUnlock() - return p.isExpired() -} - -type suppressedContext struct { - context.Context -} - -func (s *suppressedContext) Deadline() (deadline time.Time, ok bool) { - return time.Time{}, false -} - -func (s *suppressedContext) Done() <-chan struct{} { - return nil -} - -func (s *suppressedContext) Err() error { - return nil + return options.AWSCredentials(creds), err } var _ options.AWSSigner = (*Signer)(nil) diff --git a/modules/mongoaws/mongoaws_test.go b/modules/mongoaws/mongoaws_test.go deleted file mode 100644 index f55b344b7a..0000000000 --- a/modules/mongoaws/mongoaws_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// 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 mongoaws - -import ( - "context" - "fmt" - "testing" - - "github.com/aws/aws-sdk-go-v2/aws" -) - -type stubProvider struct { - creds aws.Credentials - err error -} - -func (s *stubProvider) Retrieve(_ context.Context) (aws.Credentials, error) { - return s.creds, s.err -} - -func TestCredentialsRetrieve(t *testing.T) { - p := &CredentialsProvider{ - provider: &stubProvider{ - creds: aws.Credentials{ - AccessKeyID: "AKID", - SecretAccessKey: "SECRET", - SessionToken: "Token", - }, - }, - } - - creds, err := p.Retrieve(context.Background()) - if err != nil { - t.Errorf("Expected no error, got %q", err) - } - if e, a := "AKID", creds.AccessKeyID; e != a { - t.Errorf("Expect access key ID to match, %q got %q", e, a) - } - if e, a := "SECRET", creds.SecretAccessKey; e != a { - t.Errorf("Expect secret access key to match, %q got %q", e, a) - } - if e, a := "Token", creds.SessionToken; e != a { - t.Errorf("Expect session token to match, %q got %q", e, a) - } -} - -func TestCredentialsRetrieveWithError(t *testing.T) { - p := &CredentialsProvider{ - provider: &stubProvider{ - err: fmt.Errorf("provider error"), - }, - } - - _, err := p.Retrieve(context.Background()) - if e, a := "provider error", err.Error(); e != a { - t.Errorf("Expected provider error, %q got %q", e, a) - } -} - -func TestCredentialsExpire(t *testing.T) { - stub := &stubProvider{} - p := &CredentialsProvider{ - provider: stub, - } - - if !p.Expired() { - t.Errorf("Expected to start out expired") - } - - _, err := p.Retrieve(context.Background()) - if err != nil { - t.Errorf("Expected no err, got %v", err) - } - if p.Expired() { - t.Errorf("Expected not to be expired") - } -} - -type stubProviderConcurrent struct { - stubProvider - done chan struct{} -} - -func (s *stubProviderConcurrent) Retrieve(ctx context.Context) (aws.Credentials, error) { - <-s.done - return s.stubProvider.Retrieve(ctx) -} - -func TestCredentialsRetrieveConcurrent(t *testing.T) { - stub := &stubProviderConcurrent{ - done: make(chan struct{}), - } - - p := &CredentialsProvider{ - provider: stub, - } - done := make(chan struct{}) - - for range 2 { - go func() { - _, err := p.Retrieve(context.Background()) - if err != nil { - t.Errorf("Expected no err, got %q", err) - } - done <- struct{}{} - }() - } - - // Validates that a single call to Retrieve is shared between two calls to Get - stub.done <- struct{}{} - <-done - <-done -} diff --git a/mongo/client_encryption.go b/mongo/client_encryption.go index 7c7513a89c..34066bcf96 100644 --- a/mongo/client_encryption.go +++ b/mongo/client_encryption.go @@ -52,10 +52,6 @@ func (p awsCredentialsProvider) Retrieve(ctx context.Context) (credentials.Value }, nil } -func (p awsCredentialsProvider) IsExpired() bool { - return p.provider.Expired() -} - // NewClientEncryption creates a new ClientEncryption instance configured with the given options. func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options.ClientEncryptionOptions]) (*ClientEncryption, error) { if keyVaultClient == nil { diff --git a/mongo/client_examples_test.go b/mongo/client_examples_test.go index 1e6e556e8e..05314d4e08 100644 --- a/mongo/client_examples_test.go +++ b/mongo/client_examples_test.go @@ -261,10 +261,6 @@ func (a *awsCredentialsProvider) Retrieve(_ context.Context) ( }, nil } -func (*awsCredentialsProvider) Expired() bool { - return false -} - func ExampleConnect_aWS() { // Configure a Client with authentication using the MONGODB-AWS // authentication mechanism. Credentials for this mechanism can come from diff --git a/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index 22e6bf292a..ec49ccf95e 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -141,7 +141,6 @@ type OIDCCredential struct { // AWSCredentialsProvider is the interface used to retrieve AWS credentials. type AWSCredentialsProvider interface { Retrieve(ctx context.Context) (AWSCredentials, error) - Expired() bool } // AWSCredentials represents AWS credentials. 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_test.go b/x/mongo/driver/auth/mongodbaws_test.go index 33726059b2..bdd333288e 100644 --- a/x/mongo/driver/auth/mongodbaws_test.go +++ b/x/mongo/driver/auth/mongodbaws_test.go @@ -68,10 +68,6 @@ func (a *testAWSCredentialsProvider) Retrieve(_ context.Context) (credentials.Va return credentials.Value{}, nil } -func (*testAWSCredentialsProvider) IsExpired() bool { - return false -} - type testAWSSigner struct { cnt int } diff --git a/x/mongo/driver/driver.go b/x/mongo/driver/driver.go index 227df2b06c..7c662a99a1 100644 --- a/x/mongo/driver/driver.go +++ b/x/mongo/driver/driver.go @@ -89,7 +89,6 @@ type Cred struct { // AWSCredentialsProvider is the interface used to retrieve AWS credentials. type AWSCredentialsProvider interface { Retrieve(ctx context.Context) (credentials.Value, error) - IsExpired() bool } // AWSSigner is an interface to a AWS SigV4 signer that can sign HTTP requests. diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index 309da23ed5..ddcfc7cb78 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -158,10 +158,6 @@ func (p awsCredentialsProvider) Retrieve(ctx context.Context) (credentials.Value }, nil } -func (p awsCredentialsProvider) IsExpired() bool { - return p.provider.Expired() -} - type awsSigner struct { signer options.AWSSigner } From 46417a239f387e897d2a719c879a9a7301291966 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 16 Jan 2026 18:28:40 -0500 Subject: [PATCH 09/17] rename aws submodule --- {modules/mongoaws => ext/awsauth}/doc.go | 2 +- {modules/mongoaws => ext/awsauth}/go.mod | 2 +- {modules/mongoaws => ext/awsauth}/go.sum | 0 {modules/mongoaws => ext/awsauth}/mongoaws.go | 2 +- {modules/mongoaws => ext/awsauth}/mongoaws_example_test.go | 2 +- go.work | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename {modules/mongoaws => ext/awsauth}/doc.go (97%) rename {modules/mongoaws => ext/awsauth}/go.mod (92%) rename {modules/mongoaws => ext/awsauth}/go.sum (100%) rename {modules/mongoaws => ext/awsauth}/mongoaws.go (99%) rename {modules/mongoaws => ext/awsauth}/mongoaws_example_test.go (98%) diff --git a/modules/mongoaws/doc.go b/ext/awsauth/doc.go similarity index 97% rename from modules/mongoaws/doc.go rename to ext/awsauth/doc.go index a0843a9fff..5a36c8c247 100644 --- a/modules/mongoaws/doc.go +++ b/ext/awsauth/doc.go @@ -17,4 +17,4 @@ // NewSigner() adapts an AWS HTTPSigner to be used in: // // ClientOptions -package mongoaws +package awsauth diff --git a/modules/mongoaws/go.mod b/ext/awsauth/go.mod similarity index 92% rename from modules/mongoaws/go.mod rename to ext/awsauth/go.mod index 0a6c270508..c63de1aa34 100644 --- a/modules/mongoaws/go.mod +++ b/ext/awsauth/go.mod @@ -1,4 +1,4 @@ -module go.mongodb.org/mongo-driver/v2/mongoaws +module go.mongodb.org/mongo-driver/v2/awsauth go 1.23 diff --git a/modules/mongoaws/go.sum b/ext/awsauth/go.sum similarity index 100% rename from modules/mongoaws/go.sum rename to ext/awsauth/go.sum diff --git a/modules/mongoaws/mongoaws.go b/ext/awsauth/mongoaws.go similarity index 99% rename from modules/mongoaws/mongoaws.go rename to ext/awsauth/mongoaws.go index 1edb2e6315..7cbcb644f8 100644 --- a/modules/mongoaws/mongoaws.go +++ b/ext/awsauth/mongoaws.go @@ -4,7 +4,7 @@ // 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 mongoaws +package awsauth import ( "context" diff --git a/modules/mongoaws/mongoaws_example_test.go b/ext/awsauth/mongoaws_example_test.go similarity index 98% rename from modules/mongoaws/mongoaws_example_test.go rename to ext/awsauth/mongoaws_example_test.go index d7d6676d7a..0669af0035 100644 --- a/modules/mongoaws/mongoaws_example_test.go +++ b/ext/awsauth/mongoaws_example_test.go @@ -4,7 +4,7 @@ // 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 mongoaws +package awsauth import ( "github.com/aws/aws-sdk-go-v2/aws" diff --git a/go.work b/go.work index 8601c66ba5..631018661c 100644 --- a/go.work +++ b/go.work @@ -9,5 +9,5 @@ use ( ./internal/cmd/faas/awslambda/mongodb ./internal/test/compilecheck ./internal/test/goleak - ./modules/mongoaws + ./ext/awsauth ) From 0eff7042d599278f5e1d93f8af0e2d592bbb9ad5 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 16 Jan 2026 19:08:27 -0500 Subject: [PATCH 10/17] fix typo --- ext/awsauth/doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/awsauth/doc.go b/ext/awsauth/doc.go index 5a36c8c247..00a4f81ea8 100644 --- a/ext/awsauth/doc.go +++ b/ext/awsauth/doc.go @@ -11,7 +11,7 @@ // NewCredentialsProvider() adapts an AWS CredentialsProvider to be used in: // // ClientOptions -// ClinetEncryptionOptions +// ClientEncryptionOptions // AutoEncryptionOptions // // NewSigner() adapts an AWS HTTPSigner to be used in: From 26c055dee2c23c64b93e90e8bd26d242147dba56 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 22 Jan 2026 15:53:33 -0500 Subject: [PATCH 11/17] fmt --- internal/integration/client_side_encryption_prose_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/integration/client_side_encryption_prose_test.go b/internal/integration/client_side_encryption_prose_test.go index aff09ab30b..c42e2cae13 100644 --- a/internal/integration/client_side_encryption_prose_test.go +++ b/internal/integration/client_side_encryption_prose_test.go @@ -3168,7 +3168,7 @@ func TestClientSideEncryptionProse_26_custom_aws_credentials(t *testing.T) { ceo := options.ClientEncryption(). SetKeyVaultNamespace("keyvault.datakeys"). SetKmsProviders(map[string]map[string]any{ - "aws": map[string]any{}, + "aws": {}, }). SetAWSCredentialsProvider(&provider) clientEncryption, err := mongo.NewClientEncryption(keyVaultClient, ceo) @@ -3214,7 +3214,7 @@ func TestClientSideEncryptionProse_26_custom_aws_credentials(t *testing.T) { ceo := options.ClientEncryption(). SetKeyVaultNamespace("keyvault.datakeys"). SetKmsProviders(map[string]map[string]any{ - "aws": map[string]any{}, + "aws": {}, }). SetAWSCredentialsProvider(&provider) clientEncryption, err := mongo.NewClientEncryption(keyVaultClient, ceo) From 6b6899535e6e40252a7a85fdf4d7dc57efba218b Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 29 Jan 2026 17:31:26 -0500 Subject: [PATCH 12/17] cleanup code --- ext/awsauth/doc.go | 6 ++- ext/awsauth/go.mod | 14 +++++- ext/awsauth/go.sum | 28 ++++++++++- ext/awsauth/mongoaws.go | 43 +++++++++++------ ext/awsauth/mongoaws_example_test.go | 27 +++++++++-- .../aws/credentials/chain_provider_test.go | 48 +------------------ internal/aws/credentials/credentials.go | 3 -- internal/aws/credentials/credentials_test.go | 15 ------ internal/aws/signer/v4/v4.go | 19 +++++--- internal/aws/signer/v4/v4_test.go | 16 +++---- .../credproviders/assume_role_provider.go | 7 +-- internal/credproviders/ec2_provider.go | 9 ++-- internal/credproviders/ecs_provider.go | 7 +-- internal/credproviders/env_provider.go | 5 +- internal/credproviders/imds_provider.go | 5 +- internal/credproviders/static_provider.go | 4 -- mongo/client_encryption.go | 1 - .../auth/{aws_conv.go => aws_sasl_adapter.go} | 0 x/mongo/driver/auth/mongodbaws.go | 15 +----- x/mongo/driver/topology/topology_options.go | 1 - 20 files changed, 128 insertions(+), 145 deletions(-) rename x/mongo/driver/auth/{aws_conv.go => aws_sasl_adapter.go} (100%) diff --git a/ext/awsauth/doc.go b/ext/awsauth/doc.go index 00a4f81ea8..b06e2b6561 100644 --- a/ext/awsauth/doc.go +++ b/ext/awsauth/doc.go @@ -4,10 +4,14 @@ // 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 v2 AWS SDK to be +// 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 diff --git a/ext/awsauth/go.mod b/ext/awsauth/go.mod index c63de1aa34..49801c15c6 100644 --- a/ext/awsauth/go.mod +++ b/ext/awsauth/go.mod @@ -3,11 +3,23 @@ module go.mongodb.org/mongo-driver/v2/awsauth go 1.23 require ( - github.com/aws/aws-sdk-go-v2 v1.40.1 + github.com/aws/aws-sdk-go-v2 v1.41.1 + github.com/aws/aws-sdk-go-v2/config v1.32.7 go.mongodb.org/mongo-driver/v2 v2.0.0-beta2 ) require ( + github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect github.com/aws/smithy-go v1.24.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/klauspost/compress v1.17.6 // indirect diff --git a/ext/awsauth/go.sum b/ext/awsauth/go.sum index c94867c896..bab1d7b254 100644 --- a/ext/awsauth/go.sum +++ b/ext/awsauth/go.sum @@ -1,5 +1,29 @@ -github.com/aws/aws-sdk-go-v2 v1.40.1 h1:difXb4maDZkRH0x//Qkwcfpdg1XQVXEAEs2DdXldFFc= -github.com/aws/aws-sdk-go-v2 v1.40.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= +github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/ext/awsauth/mongoaws.go b/ext/awsauth/mongoaws.go index 7e5e91f63f..40d6818420 100644 --- a/ext/awsauth/mongoaws.go +++ b/ext/awsauth/mongoaws.go @@ -10,6 +10,7 @@ import ( "context" "crypto/sha256" "encoding/hex" + "errors" "net/http" "time" @@ -26,19 +27,21 @@ const ( var _ options.AWSCredentialsProvider = (*CredentialsProvider)(nil) -// CredentialsProvider is an implementation of options.AWSCredentialsProvider in -// MongoDB Go driver. It provides caching and concurrency safe credentials retrieval -// via the provider's retrieve method. +// CredentialsProvider adapts an AWS SDK v2 CredentialsProvider to the +// options.AWSCredentialsProvider interface used by the MongoDB Go Driver. type CredentialsProvider struct { provider aws.CredentialsProvider } // NewCredentialsProvider returns a CredentialsProvider that wraps AWS -// CredentialsProvider. Provider is expected to not be nil. -func NewCredentialsProvider(provider aws.CredentialsProvider) *CredentialsProvider { - return &CredentialsProvider{ - provider: provider, +// CredentialsProvider. CredentialsProvider is expected to not be nil. +func NewCredentialsProvider(credentialsProvider aws.CredentialsProvider) (*CredentialsProvider, error) { + if credentialsProvider == nil { + return nil, errors.New("nil provider") } + return &CredentialsProvider{ + provider: credentialsProvider, + }, nil } // Retrieve returns the credentials. @@ -49,20 +52,28 @@ func (p *CredentialsProvider) Retrieve(ctx context.Context) (options.AWSCredenti var _ options.AWSSigner = (*Signer)(nil) -// Signer is an implementation of options.AWSSigner in MongoDB Go driver. +// Signer adapts an AWS SDK v2 SigV4 HTTP signer to the options.AWSSigner +// interface used by the MongoDB Go Driver. type Signer struct { signer v4.HTTPSigner } -// NewSigner creates a new Signer from the provided AWS HTTPSigner. -func NewSigner(httpSigner v4.HTTPSigner) Signer { - return Signer{ - signer: httpSigner, +// NewSigner creates a new Signer from the provided AWS HTTPSigner. HTTPSigner +// is expected to not be nil. +func NewSigner(httpSigner v4.HTTPSigner) (*Signer, error) { + if httpSigner == nil { + return nil, errors.New("nil httpSigner") } + return &Signer{ + signer: httpSigner, + }, nil } -// Sign signs AWS v4 requests. -func (s Signer) Sign( +// Sign signs AWS SigV4 requests. The payload parameter should be the raw +// request body. This method computes the SHA256 hash of the payload before +// signing. If payload is empty, the precomputed hash of an empty string is +// used. +func (s *Signer) Sign( ctx context.Context, creds options.AWSCredentials, r *http.Request, payload, service, region string, signingTime time.Time, ) error { @@ -72,6 +83,8 @@ func (s Signer) Sign( hash := sha256.Sum256([]byte(payload)) payload = hex.EncodeToString(hash[:]) } - r.Header.Set("X-Amz-Security-Token", creds.SessionToken) + if creds.SessionToken != "" { + r.Header.Set("X-Amz-Security-Token", creds.SessionToken) + } return s.signer.SignHTTP(ctx, aws.Credentials(creds), r, payload, service, region, signingTime) } diff --git a/ext/awsauth/mongoaws_example_test.go b/ext/awsauth/mongoaws_example_test.go index 0669af0035..8968fc133a 100644 --- a/ext/awsauth/mongoaws_example_test.go +++ b/ext/awsauth/mongoaws_example_test.go @@ -7,14 +7,23 @@ package awsauth import ( - "github.com/aws/aws-sdk-go-v2/aws" + "context" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/aws-sdk-go-v2/config" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) func ExampleNewCredentialsProvider() { - awsCredentialProvider := NewCredentialsProvider(aws.NewConfig().Credentials) + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + panic(err) + } + awsCredentialProvider, err := NewCredentialsProvider(cfg.Credentials) + if err != nil { + panic(err) + } credential := options.Credential{ AuthMechanism: "MONGODB-AWS", AWSCredentialsProvider: awsCredentialProvider, @@ -28,8 +37,18 @@ func ExampleNewCredentialsProvider() { } func ExampleNewSigner() { - awsCredentialProvider := NewCredentialsProvider(aws.NewConfig().Credentials) - awsSigner := NewSigner(v4.NewSigner()) + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + panic(err) + } + awsCredentialProvider, err := NewCredentialsProvider(cfg.Credentials) + if err != nil { + panic(err) + } + awsSigner, err := NewSigner(v4.NewSigner()) + if err != nil { + panic(err) + } _ = awsSigner credential := options.Credential{ AuthMechanism: "MONGODB-AWS", diff --git a/internal/aws/credentials/chain_provider_test.go b/internal/aws/credentials/chain_provider_test.go index 1fd3d618f9..9c6662ccce 100644 --- a/internal/aws/credentials/chain_provider_test.go +++ b/internal/aws/credentials/chain_provider_test.go @@ -18,22 +18,12 @@ import ( "go.mongodb.org/mongo-driver/v2/internal/aws/awserr" ) -type secondStubProvider struct { - creds Value - err error -} - -func (s *secondStubProvider) Retrieve(_ context.Context) (Value, error) { - s.creds.ProviderName = "secondStubProvider" - return s.creds, s.err -} - -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", @@ -54,11 +44,7 @@ func TestChainProviderWithNames(t *testing.T) { 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) } @@ -70,36 +56,6 @@ 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(context.Background()) - 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 TestChainProviderWithNoProvider(t *testing.T) { p := &ChainProvider{ Providers: []Provider{}, diff --git a/internal/aws/credentials/credentials.go b/internal/aws/credentials/credentials.go index aaec9c8400..f64fc223f8 100644 --- a/internal/aws/credentials/credentials.go +++ b/internal/aws/credentials/credentials.go @@ -45,9 +45,6 @@ type Value struct { // The ID of the account for the credentials. AccountID string - - // Provider used to get credentials - ProviderName string } // Expired returns if the credentials have expired. diff --git a/internal/aws/credentials/credentials_test.go b/internal/aws/credentials/credentials_test.go index a6a887a5e9..42a2290ed0 100644 --- a/internal/aws/credentials/credentials_test.go +++ b/internal/aws/credentials/credentials_test.go @@ -24,7 +24,6 @@ type stubProvider struct { } func (s *stubProvider) Retrieve(_ context.Context) (Value, error) { - s.creds.ProviderName = "stubProvider" return s.creds, s.err } @@ -61,20 +60,6 @@ func TestCredentialsGetWithError(t *testing.T) { } } -func TestCredentialsGetWithProviderName(t *testing.T) { - stub := &stubProvider{} - - c := NewCredentials(stub) - - creds, err := c.Get(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 stubProviderConcurrent struct { stubProvider done chan struct{} diff --git a/internal/aws/signer/v4/v4.go b/internal/aws/signer/v4/v4.go index 69a2813ea8..c0aa293b9b 100644 --- a/internal/aws/signer/v4/v4.go +++ b/internal/aws/signer/v4/v4.go @@ -11,6 +11,7 @@ package v4 import ( + "context" "crypto/hmac" "crypto/sha256" "encoding/hex" @@ -97,11 +98,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 @@ -112,8 +112,13 @@ type signingCtx struct { // generated. To bypass the signer computing the hash you can set the // "X-Amz-Content-Sha256" header with a precomputed value. The signer will // only compute the hash if the request header value is empty. -func (v4 Signer) Sign(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) { - return v4.signWithBody(r, body, service, region, signTime) +func (v4 Signer) Sign(_ context.Context, r *http.Request, body, service, region string, signTime time.Time) error { + var bodyReader io.ReadSeeker + if len(body) > 0 { + bodyReader = strings.NewReader(body) + } + _, err := v4.signWithBody(r, bodyReader, service, region, signTime) + return err } func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) { diff --git a/internal/aws/signer/v4/v4_test.go b/internal/aws/signer/v4/v4_test.go index 8497574261..e97c91278a 100644 --- a/internal/aws/signer/v4/v4_test.go +++ b/internal/aws/signer/v4/v4_test.go @@ -133,7 +133,7 @@ func newTestStaticCredentials() *credentials.Credentials { func TestSignRequest(t *testing.T) { req, body := buildRequest("{}") signer := buildSigner() - _, err := signer.Sign(req, body, "dynamodb", "us-east-1", epochTime()) + _, err := signer.signWithBody(req, body, "dynamodb", "us-east-1", epochTime()) if err != nil { t.Errorf("Expected no err, got %v", err) } @@ -153,7 +153,7 @@ func TestSignRequest(t *testing.T) { func TestSignUnseekableBody(t *testing.T) { req, body := buildRequestWithBodyReader("mock-service", "mock-region", bytes.NewBuffer([]byte("hello"))) signer := buildSigner() - _, err := signer.Sign(req, body, "mock-service", "mock-region", time.Now()) + _, err := signer.signWithBody(req, body, "mock-service", "mock-region", time.Now()) if err == nil { t.Fatalf("expect error signing request") } @@ -169,7 +169,7 @@ func TestSignPreComputedHashUnseekableBody(t *testing.T) { signer := buildSigner() req.Header.Set("X-Amz-Content-Sha256", "some-content-sha256") - _, err := signer.Sign(req, body, "mock-service", "mock-region", time.Now()) + _, err := signer.signWithBody(req, body, "mock-service", "mock-region", time.Now()) if err != nil { t.Fatalf("expect no error, got %v", err) } @@ -184,7 +184,7 @@ func TestSignPrecomputedBodyChecksum(t *testing.T) { req, body := buildRequest("hello") req.Header.Set("X-Amz-Content-Sha256", "PRECOMPUTED") signer := buildSigner() - _, 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 { t.Errorf("Expected no err, got %v", err) } @@ -218,7 +218,7 @@ func TestSignWithRequestBody(t *testing.T) { t.Errorf("expect not no error, got %v", err) } - _, err = signer.Sign(req, bytes.NewReader(expectBody), "service", "region", time.Now()) + _, err = signer.signWithBody(req, bytes.NewReader(expectBody), "service", "region", time.Now()) if err != nil { t.Errorf("expect not no error, got %v", err) } @@ -256,7 +256,7 @@ func TestSignWithRequestBody_Overwrite(t *testing.T) { t.Errorf("expect not no error, got %v", err) } - _, err = signer.Sign(req, nil, "service", "region", time.Now()) + _, err = signer.signWithBody(req, nil, "service", "region", time.Now()) req.ContentLength = 0 if err != nil { @@ -299,7 +299,7 @@ func TestSignWithBody_ReplaceRequestBody(t *testing.T) { s := NewSigner(creds) origBody := req.Body - _, err := s.Sign(req, seekerBody, "dynamodb", "us-east-1", time.Now()) + _, err := s.signWithBody(req, seekerBody, "dynamodb", "us-east-1", time.Now()) if err != nil { t.Fatalf("expect no error, got %v", err) } @@ -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 f8027c2e5e..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` ) @@ -37,7 +34,7 @@ type AssumeRoleProvider struct { // 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 Expired() 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 } @@ -60,7 +57,7 @@ func NewAssumeRoleProvider(httpClient *http.Client, expiryWindow time.Duration) 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() diff --git a/internal/credproviders/ec2_provider.go b/internal/credproviders/ec2_provider.go index b6852eb093..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" @@ -36,7 +33,7 @@ type EC2Provider struct { // 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 Expired() 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 } @@ -107,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) @@ -147,7 +144,7 @@ func (e *EC2Provider) getCredentials(ctx context.Context, token string, role str // Retrieve retrieves the keys from the AWS service. func (e *EC2Provider) Retrieve(ctx context.Context) (credentials.Value, error) { - v := credentials.Value{ProviderName: ec2ProviderName} + var v credentials.Value token, err := e.getToken(ctx) if err != nil { diff --git a/internal/credproviders/ecs_provider.go b/internal/credproviders/ecs_provider.go index e61f5156c3..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/" ) @@ -33,7 +30,7 @@ type ECSProvider struct { // 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 Expired() 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 } @@ -52,7 +49,7 @@ func NewECSProvider(httpClient *http.Client, expiryWindow time.Duration) *ECSPro 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 { diff --git a/internal/credproviders/env_provider.go b/internal/credproviders/env_provider.go index fa70e376f2..2483c2dcd0 100644 --- a/internal/credproviders/env_provider.go +++ b/internal/credproviders/env_provider.go @@ -13,9 +13,6 @@ import ( "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 @@ -50,11 +47,11 @@ func (e *EnvProvider) Retrieve(_ context.Context) (credentials.Value, error) { AccessKeyID: e.AwsAccessKeyIDEnv.Get(), SecretAccessKey: e.AwsSecretAccessKeyEnv.Get(), SessionToken: e.AwsSessionTokenEnv.Get(), - ProviderName: envProviderName, CanExpire: false, } err := verify(v) if err != nil { + // Expire the credentials if invalid. v.CanExpire = true } diff --git a/internal/credproviders/imds_provider.go b/internal/credproviders/imds_provider.go index 5e91d10970..005b61da10 100644 --- a/internal/credproviders/imds_provider.go +++ b/internal/credproviders/imds_provider.go @@ -19,9 +19,6 @@ import ( ) const ( - // AzureProviderName provides a name of Azure provider - AzureProviderName = "AzureProvider" - azureURI = "http://169.254.169.254/metadata/identity/oauth2/token" ) @@ -41,7 +38,7 @@ func NewAzureProvider(httpClient *http.Client, expiryWindow time.Duration) *Azur // Retrieve retrieves the keys from the Azure service. func (a *AzureProvider) Retrieve(ctx context.Context) (credentials.Value, error) { - v := credentials.Value{ProviderName: AzureProviderName} + 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) diff --git a/internal/credproviders/static_provider.go b/internal/credproviders/static_provider.go index 7f4f925db4..2484e493cd 100644 --- a/internal/credproviders/static_provider.go +++ b/internal/credproviders/static_provider.go @@ -13,9 +13,6 @@ import ( "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 { @@ -45,7 +42,6 @@ func verify(v 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 diff --git a/mongo/client_encryption.go b/mongo/client_encryption.go index e1051bb789..a0fa962dc7 100644 --- a/mongo/client_encryption.go +++ b/mongo/client_encryption.go @@ -48,7 +48,6 @@ func (p awsCredentialsProvider) Retrieve(ctx context.Context) (credentials.Value CanExpire: creds.CanExpire, Expires: creds.Expires, AccountID: creds.AccountID, - ProviderName: "AwsProvider", }, nil } diff --git a/x/mongo/driver/auth/aws_conv.go b/x/mongo/driver/auth/aws_sasl_adapter.go similarity index 100% rename from x/mongo/driver/auth/aws_conv.go rename to x/mongo/driver/auth/aws_sasl_adapter.go diff --git a/x/mongo/driver/auth/mongodbaws.go b/x/mongo/driver/auth/mongodbaws.go index 0cb2c1c7b8..bdcb0085a1 100644 --- a/x/mongo/driver/auth/mongodbaws.go +++ b/x/mongo/driver/auth/mongodbaws.go @@ -10,7 +10,6 @@ import ( "context" "errors" "net/http" - "strings" "time" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" @@ -58,8 +57,8 @@ func newMongoDBAWSAuthenticator(cred *Cred, httpClient *http.Client) (Authentica } return &MongoDBAWSAuthenticator{ - signer: builtInV4Signer{ - creds: creds.NewAWSCredentialProvider(httpClient, providers...).Cred, + signer: v4signer.Signer{ + Credentials: creds.NewAWSCredentialProvider(httpClient, providers...).Cred, }, }, nil } @@ -86,16 +85,6 @@ func (a *MongoDBAWSAuthenticator) Reauth(_ context.Context, _ *driver.AuthConfig return newAuthError("AWS authentication does not support reauthentication", nil) } -type builtInV4Signer struct { - creds *credentials.Credentials -} - -func (s builtInV4Signer) Sign(_ context.Context, req *http.Request, body, service, region string, signTime time.Time) error { - signer := v4signer.NewSigner(s.creds) - _, err := signer.Sign(req, strings.NewReader(body), service, region, signTime) - return err -} - type customSigner struct { provider credentials.Provider signer driver.AWSSigner diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index 0c3c458055..a57f467335 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -156,7 +156,6 @@ func (p awsCredentialsProvider) Retrieve(ctx context.Context) (credentials.Value CanExpire: creds.CanExpire, Expires: creds.Expires, AccountID: creds.AccountID, - ProviderName: "AwsProvider", }, nil } From a61c360f2ec6b95e110bba9959414f658c9c2e73 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 6 Feb 2026 17:43:34 -0500 Subject: [PATCH 13/17] remove AWSSigner interface --- ext/awsauth/awsauth.go | 37 ++++++++ ...xample_test.go => awsauth_example_test.go} | 33 +------ ext/awsauth/go.mod | 2 +- ext/awsauth/mongoaws.go | 90 ------------------- internal/aws/signer/v4/v4.go | 10 +-- internal/aws/signer/v4/v4_test.go | 14 +-- mongo/options/clientoptions.go | 7 -- x/mongo/driver/auth/auth.go | 2 +- x/mongo/driver/auth/aws_sasl_adapter.go | 23 +++-- x/mongo/driver/auth/conversation.go | 2 +- x/mongo/driver/auth/internal/gssapi/gss.go | 2 +- x/mongo/driver/auth/mongodbaws.go | 37 +------- x/mongo/driver/auth/mongodbaws_test.go | 56 ------------ x/mongo/driver/auth/oidc.go | 4 +- x/mongo/driver/auth/plain.go | 2 +- x/mongo/driver/auth/sasl.go | 12 +-- x/mongo/driver/auth/scram.go | 2 +- x/mongo/driver/auth/x509.go | 2 +- x/mongo/driver/driver.go | 8 -- x/mongo/driver/topology/topology_options.go | 22 ----- 20 files changed, 77 insertions(+), 290 deletions(-) create mode 100644 ext/awsauth/awsauth.go rename ext/awsauth/{mongoaws_example_test.go => awsauth_example_test.go} (53%) delete mode 100644 ext/awsauth/mongoaws.go diff --git a/ext/awsauth/awsauth.go b/ext/awsauth/awsauth.go new file mode 100644 index 0000000000..9668fca89d --- /dev/null +++ b/ext/awsauth/awsauth.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 awsauth + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +var _ options.AWSCredentialsProvider = (*CredentialsProvider)(nil) + +// CredentialsProvider adapts an AWS SDK v2 CredentialsProvider to the +// options.AWSCredentialsProvider interface used by the MongoDB Go Driver. +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, error) { + return &CredentialsProvider{ + provider: credentialsProvider, + }, nil +} + +// Retrieve returns the credentials. +func (p *CredentialsProvider) Retrieve(ctx context.Context) (options.AWSCredentials, error) { + creds, err := p.provider.Retrieve(ctx) + return options.AWSCredentials(creds), err +} diff --git a/ext/awsauth/mongoaws_example_test.go b/ext/awsauth/awsauth_example_test.go similarity index 53% rename from ext/awsauth/mongoaws_example_test.go rename to ext/awsauth/awsauth_example_test.go index 8968fc133a..c98773c098 100644 --- a/ext/awsauth/mongoaws_example_test.go +++ b/ext/awsauth/awsauth_example_test.go @@ -4,13 +4,13 @@ // 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 +package awsauth_test import ( "context" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "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" ) @@ -20,7 +20,7 @@ func ExampleNewCredentialsProvider() { if err != nil { panic(err) } - awsCredentialProvider, err := NewCredentialsProvider(cfg.Credentials) + awsCredentialProvider, err := awsauth.NewCredentialsProvider(cfg.Credentials) if err != nil { panic(err) } @@ -35,30 +35,3 @@ func ExampleNewCredentialsProvider() { } _ = client } - -func ExampleNewSigner() { - cfg, err := config.LoadDefaultConfig(context.TODO()) - if err != nil { - panic(err) - } - awsCredentialProvider, err := NewCredentialsProvider(cfg.Credentials) - if err != nil { - panic(err) - } - awsSigner, err := NewSigner(v4.NewSigner()) - if err != nil { - panic(err) - } - _ = awsSigner - credential := options.Credential{ - AuthMechanism: "MONGODB-AWS", - AWSCredentialsProvider: awsCredentialProvider, - AWSSigner: awsSigner, - } - client, err := mongo.Connect( - options.Client().SetAuth(credential)) - if err != nil { - panic(err) - } - _ = client -} diff --git a/ext/awsauth/go.mod b/ext/awsauth/go.mod index 49801c15c6..75495fb2d4 100644 --- a/ext/awsauth/go.mod +++ b/ext/awsauth/go.mod @@ -1,4 +1,4 @@ -module go.mongodb.org/mongo-driver/v2/awsauth +module go.mongodb.org/mongo-driver/ext/awsauth go 1.23 diff --git a/ext/awsauth/mongoaws.go b/ext/awsauth/mongoaws.go deleted file mode 100644 index 40d6818420..0000000000 --- a/ext/awsauth/mongoaws.go +++ /dev/null @@ -1,90 +0,0 @@ -// 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" - "crypto/sha256" - "encoding/hex" - "errors" - "net/http" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" - - "go.mongodb.org/mongo-driver/v2/mongo/options" -) - -const ( - // emptyStringSHA256 is a SHA256 of an empty string - emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` -) - -var _ options.AWSCredentialsProvider = (*CredentialsProvider)(nil) - -// CredentialsProvider adapts an AWS SDK v2 CredentialsProvider to the -// options.AWSCredentialsProvider interface used by the MongoDB Go Driver. -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, error) { - if credentialsProvider == nil { - return nil, errors.New("nil provider") - } - return &CredentialsProvider{ - provider: credentialsProvider, - }, nil -} - -// Retrieve returns the credentials. -func (p *CredentialsProvider) Retrieve(ctx context.Context) (options.AWSCredentials, error) { - creds, err := p.provider.Retrieve(ctx) - return options.AWSCredentials(creds), err -} - -var _ options.AWSSigner = (*Signer)(nil) - -// Signer adapts an AWS SDK v2 SigV4 HTTP signer to the options.AWSSigner -// interface used by the MongoDB Go Driver. -type Signer struct { - signer v4.HTTPSigner -} - -// NewSigner creates a new Signer from the provided AWS HTTPSigner. HTTPSigner -// is expected to not be nil. -func NewSigner(httpSigner v4.HTTPSigner) (*Signer, error) { - if httpSigner == nil { - return nil, errors.New("nil httpSigner") - } - return &Signer{ - signer: httpSigner, - }, nil -} - -// Sign signs AWS SigV4 requests. The payload parameter should be the raw -// request body. This method computes the SHA256 hash of the payload before -// signing. If payload is empty, the precomputed hash of an empty string is -// used. -func (s *Signer) Sign( - ctx context.Context, creds options.AWSCredentials, r *http.Request, - payload, service, region string, signingTime time.Time, -) error { - if len(payload) == 0 { - payload = emptyStringSHA256 - } else { - hash := sha256.Sum256([]byte(payload)) - payload = hex.EncodeToString(hash[:]) - } - if creds.SessionToken != "" { - r.Header.Set("X-Amz-Security-Token", creds.SessionToken) - } - return s.signer.SignHTTP(ctx, aws.Credentials(creds), r, payload, service, region, signingTime) -} diff --git a/internal/aws/signer/v4/v4.go b/internal/aws/signer/v4/v4.go index c0aa293b9b..4bc9fa130e 100644 --- a/internal/aws/signer/v4/v4.go +++ b/internal/aws/signer/v4/v4.go @@ -11,7 +11,6 @@ package v4 import ( - "context" "crypto/hmac" "crypto/sha256" "encoding/hex" @@ -112,13 +111,8 @@ type signingCtx struct { // generated. To bypass the signer computing the hash you can set the // "X-Amz-Content-Sha256" header with a precomputed value. The signer will // only compute the hash if the request header value is empty. -func (v4 Signer) Sign(_ context.Context, r *http.Request, body, service, region string, signTime time.Time) error { - var bodyReader io.ReadSeeker - if len(body) > 0 { - bodyReader = strings.NewReader(body) - } - _, err := v4.signWithBody(r, bodyReader, service, region, signTime) - return err +func (v4 Signer) Sign(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) { + return v4.signWithBody(r, body, service, region, signTime) } func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) { diff --git a/internal/aws/signer/v4/v4_test.go b/internal/aws/signer/v4/v4_test.go index e97c91278a..efd7a6f29e 100644 --- a/internal/aws/signer/v4/v4_test.go +++ b/internal/aws/signer/v4/v4_test.go @@ -133,7 +133,7 @@ func newTestStaticCredentials() *credentials.Credentials { func TestSignRequest(t *testing.T) { req, body := buildRequest("{}") signer := buildSigner() - _, err := signer.signWithBody(req, body, "dynamodb", "us-east-1", epochTime()) + _, err := signer.Sign(req, body, "dynamodb", "us-east-1", epochTime()) if err != nil { t.Errorf("Expected no err, got %v", err) } @@ -153,7 +153,7 @@ func TestSignRequest(t *testing.T) { func TestSignUnseekableBody(t *testing.T) { req, body := buildRequestWithBodyReader("mock-service", "mock-region", bytes.NewBuffer([]byte("hello"))) signer := buildSigner() - _, err := signer.signWithBody(req, body, "mock-service", "mock-region", time.Now()) + _, err := signer.Sign(req, body, "mock-service", "mock-region", time.Now()) if err == nil { t.Fatalf("expect error signing request") } @@ -169,7 +169,7 @@ func TestSignPreComputedHashUnseekableBody(t *testing.T) { signer := buildSigner() req.Header.Set("X-Amz-Content-Sha256", "some-content-sha256") - _, err := signer.signWithBody(req, body, "mock-service", "mock-region", time.Now()) + _, err := signer.Sign(req, body, "mock-service", "mock-region", time.Now()) if err != nil { t.Fatalf("expect no error, got %v", err) } @@ -184,7 +184,7 @@ func TestSignPrecomputedBodyChecksum(t *testing.T) { req, body := buildRequest("hello") req.Header.Set("X-Amz-Content-Sha256", "PRECOMPUTED") signer := buildSigner() - _, err := signer.signWithBody(req, body, "dynamodb", "us-east-1", time.Now()) + _, err := signer.Sign(req, body, "dynamodb", "us-east-1", time.Now()) if err != nil { t.Errorf("Expected no err, got %v", err) } @@ -218,7 +218,7 @@ func TestSignWithRequestBody(t *testing.T) { t.Errorf("expect not no error, got %v", err) } - _, err = signer.signWithBody(req, bytes.NewReader(expectBody), "service", "region", time.Now()) + _, err = signer.Sign(req, bytes.NewReader(expectBody), "service", "region", time.Now()) if err != nil { t.Errorf("expect not no error, got %v", err) } @@ -256,7 +256,7 @@ func TestSignWithRequestBody_Overwrite(t *testing.T) { t.Errorf("expect not no error, got %v", err) } - _, err = signer.signWithBody(req, nil, "service", "region", time.Now()) + _, err = signer.Sign(req, nil, "service", "region", time.Now()) req.ContentLength = 0 if err != nil { @@ -299,7 +299,7 @@ func TestSignWithBody_ReplaceRequestBody(t *testing.T) { s := NewSigner(creds) origBody := req.Body - _, err := s.signWithBody(req, seekerBody, "dynamodb", "us-east-1", time.Now()) + _, err := s.Sign(req, seekerBody, "dynamodb", "us-east-1", time.Now()) if err != nil { t.Fatalf("expect no error, got %v", err) } diff --git a/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index ec49ccf95e..d3d906141b 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -117,7 +117,6 @@ type Credential struct { OIDCMachineCallback OIDCCallback OIDCHumanCallback OIDCCallback AWSCredentialsProvider AWSCredentialsProvider - AWSSigner AWSSigner } // OIDCCallback is the type for both Human and Machine Callback flows. @@ -154,12 +153,6 @@ type AWSCredentials struct { AccountID string } -// AWSSigner is an interface to a AWS SigV4 signer that can sign HTTP requests. -type AWSSigner interface { - Sign(ctx context.Context, creds AWSCredentials, r *http.Request, - payload, service, region string, signingTime time.Time) error -} - // IDPInfo contains the information needed to perform OIDC authentication with // an Identity Provider. type IDPInfo struct { diff --git a/x/mongo/driver/auth/auth.go b/x/mongo/driver/auth/auth.go index 918d23d16a..bfc7bbdb9f 100644 --- a/x/mongo/driver/auth/auth.go +++ b/x/mongo/driver/auth/auth.go @@ -122,7 +122,7 @@ func (ah *authHandshaker) GetHandshakeInformation( // cannot perform speculative authentication. An example of this is MONGODB-OIDC when there is // no AccessToken in the cache. if ah.conversation != nil { - firstMsg, err := ah.conversation.FirstMessage(ctx) + firstMsg, err := ah.conversation.FirstMessage() if err != nil { return driver.HandshakeInformation{}, newAuthError("failed to create speculative authentication message", err) } diff --git a/x/mongo/driver/auth/aws_sasl_adapter.go b/x/mongo/driver/auth/aws_sasl_adapter.go index 92e91d7b84..3e97009ec7 100644 --- a/x/mongo/driver/auth/aws_sasl_adapter.go +++ b/x/mongo/driver/auth/aws_sasl_adapter.go @@ -18,6 +18,7 @@ import ( "time" "go.mongodb.org/mongo-driver/v2/bson" + v4signer "go.mongodb.org/mongo-driver/v2/internal/aws/signer/v4" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" ) @@ -30,12 +31,8 @@ const ( clientDone ) -type awsSigner interface { - Sign(ctx context.Context, request *http.Request, body, service, region string, signTime time.Time) error -} - type awsSaslAdapter struct { - signer awsSigner + signer *v4signer.Signer state clientState nonce []byte @@ -43,16 +40,16 @@ type awsSaslAdapter struct { var _ SaslClient = (*awsSaslAdapter)(nil) -func (a *awsSaslAdapter) Start(ctx context.Context) (string, []byte, error) { - step, err := a.step(ctx, 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(ctx context.Context, challenge []byte) ([]byte, error) { - step, err := a.step(ctx, challenge) +func (a *awsSaslAdapter) Next(_ context.Context, challenge []byte) ([]byte, error) { + step, err := a.step(challenge) if err != nil { return nil, err } @@ -75,14 +72,14 @@ const ( // 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 (a *awsSaslAdapter) step(ctx context.Context, challenge []byte) (response []byte, err error) { +func (a *awsSaslAdapter) step(challenge []byte) (response []byte, err error) { switch a.state { case clientStarting: a.state = clientFirst response = a.firstMsg() case clientFirst: a.state = clientFinal - response, err = a.finalMsg(ctx, challenge) + response, err = a.finalMsg(challenge) case clientFinal: a.state = clientDone default: @@ -129,7 +126,7 @@ func (a *awsSaslAdapter) firstMsg() []byte { return msg } -func (a *awsSaslAdapter) finalMsg(ctx context.Context, s1 []byte) ([]byte, error) { +func (a *awsSaslAdapter) finalMsg(s1 []byte) ([]byte, error) { var sm struct { Nonce bson.Binary `bson:"s"` Host string `bson:"h"` @@ -171,7 +168,7 @@ func (a *awsSaslAdapter) finalMsg(ctx context.Context, s1 []byte) ([]byte, error req.Header.Set("X-MongoDB-GS2-CB-Flag", "n") // Get signed header - err = a.signer.Sign(ctx, req, body, "sts", region, currentTime) + _, err = a.signer.Sign(req, strings.NewReader(body), "sts", region, currentTime) if err != nil { return nil, err } diff --git a/x/mongo/driver/auth/conversation.go b/x/mongo/driver/auth/conversation.go index 17bd84c8aa..13839d7f8d 100644 --- a/x/mongo/driver/auth/conversation.go +++ b/x/mongo/driver/auth/conversation.go @@ -22,7 +22,7 @@ import ( // Finish takes the server response to the initial message and conducts the remainder of the conversation to // authenticate the provided connection. type SpeculativeConversation interface { - FirstMessage(ctx context.Context) (bsoncore.Document, error) + FirstMessage() (bsoncore.Document, error) Finish(ctx context.Context, cfg *driver.AuthConfig, firstResponse bsoncore.Document) error } diff --git a/x/mongo/driver/auth/internal/gssapi/gss.go b/x/mongo/driver/auth/internal/gssapi/gss.go index f6e1f9e924..43f6d93235 100644 --- a/x/mongo/driver/auth/internal/gssapi/gss.go +++ b/x/mongo/driver/auth/internal/gssapi/gss.go @@ -72,7 +72,7 @@ func (sc *SaslClient) Close() { C.gssapi_client_destroy(&sc.state) } -func (sc *SaslClient) Start(context.Context) (string, []byte, error) { +func (sc *SaslClient) Start() (string, []byte, error) { const mechName = "GSSAPI" cservicePrincipalName := C.CString(sc.servicePrincipalName) diff --git a/x/mongo/driver/auth/mongodbaws.go b/x/mongo/driver/auth/mongodbaws.go index bdcb0085a1..2f890d84c3 100644 --- a/x/mongo/driver/auth/mongodbaws.go +++ b/x/mongo/driver/auth/mongodbaws.go @@ -10,7 +10,6 @@ import ( "context" "errors" "net/http" - "time" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" v4signer "go.mongodb.org/mongo-driver/v2/internal/aws/signer/v4" @@ -27,18 +26,6 @@ func newMongoDBAWSAuthenticator(cred *Cred, httpClient *http.Client) (Authentica return nil, newAuthError("MONGODB-AWS source must be empty or $external", nil) } - if cred.AWSSigner != nil { - if cred.AWSCredentialsProvider == nil { - return nil, errors.New("AWSCredentialsProvider is required when AWSSigner is set") - } - return &MongoDBAWSAuthenticator{ - signer: customSigner{ - provider: cred.AWSCredentialsProvider, - signer: cred.AWSSigner, - }, - }, nil - } - if httpClient == nil { return nil, errors.New("httpClient must not be nil") } @@ -57,21 +44,19 @@ func newMongoDBAWSAuthenticator(cred *Cred, httpClient *http.Client) (Authentica } return &MongoDBAWSAuthenticator{ - signer: v4signer.Signer{ - Credentials: creds.NewAWSCredentialProvider(httpClient, providers...).Cred, - }, + credentials: creds.NewAWSCredentialProvider(httpClient, providers...).Cred, }, nil } // MongoDBAWSAuthenticator uses AWS-IAM credentials over SASL to authenticate a connection. type MongoDBAWSAuthenticator struct { - signer awsSigner + credentials *credentials.Credentials } // Auth authenticates the connection. func (a *MongoDBAWSAuthenticator) Auth(ctx context.Context, cfg *driver.AuthConfig) error { awsSasl := &awsSaslAdapter{ - signer: a.signer, + signer: v4signer.NewSigner(a.credentials), } err := ConductSaslConversation(ctx, cfg, sourceExternal, awsSasl) if err != nil { @@ -84,19 +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 customSigner struct { - provider credentials.Provider - signer driver.AWSSigner -} - -func (s customSigner) Sign( - ctx context.Context, req *http.Request, body, service, region string, - signTime time.Time, -) error { - creds, err := s.provider.Retrieve(ctx) - if err != nil { - return err - } - return s.signer.Sign(ctx, creds, req, body, service, region, signTime) -} diff --git a/x/mongo/driver/auth/mongodbaws_test.go b/x/mongo/driver/auth/mongodbaws_test.go index 10e723b481..74d6b05e33 100644 --- a/x/mongo/driver/auth/mongodbaws_test.go +++ b/x/mongo/driver/auth/mongodbaws_test.go @@ -11,7 +11,6 @@ import ( "errors" "net/http" "testing" - "time" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/internal/assert" @@ -67,15 +66,6 @@ func (a *testAWSCredentialsProvider) Retrieve(_ context.Context) (credentials.Va return credentials.Value{}, nil } -type testAWSSigner struct { - cnt int -} - -func (s *testAWSSigner) Sign(_ context.Context, _ credentials.Value, _ *http.Request, _, _, _ string, _ time.Time) error { - s.cnt++ - return 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") @@ -142,52 +132,6 @@ func TestAWSCustomCredentialsProvider(t *testing.T) { } } -func TestAWSCustomSigner(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{} - signer := &testAWSSigner{} - cred := &Cred{ - Username: "user", - Password: "pass", - Props: map[string]string{"AWS_SESSION_TOKEN": "token"}, - AWSCredentialsProvider: provider, - AWSSigner: signer, - } - - authenticator, err := newMongoDBAWSAuthenticator(cred, nil) - 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, 1, provider.cnt, "expected provider to be called once but got %v", provider.cnt) - assert.Equalf(t, 1, signer.cnt, "expected signer to be called once but got %v", signer.cnt) -} - func processWm(resps, written chan []byte, errChan chan error) { buf := <-written buf, ok := extractPayload(buf) diff --git a/x/mongo/driver/auth/oidc.go b/x/mongo/driver/auth/oidc.go index 448478da9f..571f3f3960 100644 --- a/x/mongo/driver/auth/oidc.go +++ b/x/mongo/driver/auth/oidc.go @@ -233,7 +233,7 @@ func principalStepRequest(principal string) []byte { return doc.Build() } -func (oos *oidcOneStep) Start(context.Context) (string, []byte, error) { +func (oos *oidcOneStep) Start() (string, []byte, error) { return MongoDBOIDC, jwtStepRequest(oos.accessToken), nil } @@ -245,7 +245,7 @@ func (*oidcOneStep) Completed() bool { return true } -func (ots *oidcTwoStep) Start(context.Context) (string, []byte, error) { +func (ots *oidcTwoStep) Start() (string, []byte, error) { return MongoDBOIDC, principalStepRequest(ots.oa.userName), nil } diff --git a/x/mongo/driver/auth/plain.go b/x/mongo/driver/auth/plain.go index 32edefeb2d..69342bb1e5 100644 --- a/x/mongo/driver/auth/plain.go +++ b/x/mongo/driver/auth/plain.go @@ -64,7 +64,7 @@ type plainSaslClient struct { var _ SaslClient = (*plainSaslClient)(nil) -func (c *plainSaslClient) Start(context.Context) (string, []byte, error) { +func (c *plainSaslClient) Start() (string, []byte, error) { b := []byte("\x00" + c.username + "\x00" + c.password) return PLAIN, b, nil } diff --git a/x/mongo/driver/auth/sasl.go b/x/mongo/driver/auth/sasl.go index 344cdab4b4..8aef5c0b08 100644 --- a/x/mongo/driver/auth/sasl.go +++ b/x/mongo/driver/auth/sasl.go @@ -18,7 +18,7 @@ import ( // SaslClient is the client piece of a sasl conversation. type SaslClient interface { - Start(ctx context.Context) (string, []byte, error) + Start() (string, []byte, error) Next(ctx context.Context, challenge []byte) ([]byte, error) Completed() bool } @@ -59,10 +59,10 @@ func newSaslConversation(client SaslClient, source string, speculative bool) *sa // FirstMessage returns the first message to be sent to the server. This message contains a "db" field so it can be used // for speculative authentication. -func (sc *saslConversation) FirstMessage(ctx context.Context) (bsoncore.Document, error) { +func (sc *saslConversation) FirstMessage() (bsoncore.Document, error) { var payload []byte var err error - sc.mechanism, payload, err = sc.client.Start(ctx) + sc.mechanism, payload, err = sc.client.Start() if err != nil { return nil, err } @@ -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) @@ -156,13 +156,13 @@ func (sc *saslConversation) Finish(ctx context.Context, cfg *driver.AuthConfig, func ConductSaslConversation(ctx context.Context, cfg *driver.AuthConfig, authSource string, client SaslClient) error { // Create a non-speculative SASL conversation. conversation := newSaslConversation(client, authSource, false) - saslStartDoc, err := conversation.FirstMessage(ctx) + saslStartDoc, err := conversation.FirstMessage() if err != nil { return newError(err, conversation.mechanism) } 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/auth/scram.go b/x/mongo/driver/auth/scram.go index 2e0787eb4f..c6440b3a11 100644 --- a/x/mongo/driver/auth/scram.go +++ b/x/mongo/driver/auth/scram.go @@ -119,7 +119,7 @@ var ( _ ExtraOptionsSaslClient = (*scramSaslAdapter)(nil) ) -func (a *scramSaslAdapter) Start(context.Context) (string, []byte, error) { +func (a *scramSaslAdapter) Start() (string, []byte, error) { step, err := a.conversation.Step("") if err != nil { return a.mechanism, nil, err diff --git a/x/mongo/driver/auth/x509.go b/x/mongo/driver/auth/x509.go index 8f3b5df066..f839023435 100644 --- a/x/mongo/driver/auth/x509.go +++ b/x/mongo/driver/auth/x509.go @@ -39,7 +39,7 @@ type x509Conversation struct{} var _ SpeculativeConversation = (*x509Conversation)(nil) // FirstMessage returns the first message to be sent to the server. -func (c *x509Conversation) FirstMessage(context.Context) (bsoncore.Document, error) { +func (c *x509Conversation) FirstMessage() (bsoncore.Document, error) { return createFirstX509Message(), nil } diff --git a/x/mongo/driver/driver.go b/x/mongo/driver/driver.go index fa34bc6387..fa55dd6a72 100644 --- a/x/mongo/driver/driver.go +++ b/x/mongo/driver/driver.go @@ -15,7 +15,6 @@ package driver import ( "context" - "net/http" "time" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" @@ -83,7 +82,6 @@ type Cred struct { OIDCMachineCallback OIDCCallback OIDCHumanCallback OIDCCallback AWSCredentialsProvider AWSCredentialsProvider - AWSSigner AWSSigner } // AWSCredentialsProvider is the interface used to retrieve AWS credentials. @@ -91,12 +89,6 @@ type AWSCredentialsProvider interface { Retrieve(ctx context.Context) (credentials.Value, error) } -// AWSSigner is an interface to a AWS SigV4 signer that can sign HTTP requests. -type AWSSigner interface { - Sign(ctx context.Context, creds credentials.Value, r *http.Request, - payload, service, region string, signingTime time.Time) error -} - // Deployment is implemented by types that can select a server from a deployment. type Deployment interface { SelectServer(context.Context, description.ServerSelector) (Server, error) diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index a57f467335..d96c9a83ec 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -132,10 +132,6 @@ func ConvertCreds(credOpts *options.Credential) *driver.Cred { } } - if credOpts.AWSSigner != nil { - cred.AWSSigner = awsSigner{credOpts.AWSSigner} - } - return cred } @@ -159,24 +155,6 @@ func (p awsCredentialsProvider) Retrieve(ctx context.Context) (credentials.Value }, nil } -type awsSigner struct { - signer options.AWSSigner -} - -func (s awsSigner) Sign(ctx context.Context, creds credentials.Value, r *http.Request, - payload, service, region string, signingTime time.Time, -) error { - return s.signer.Sign(ctx, options.AWSCredentials{ - AccessKeyID: creds.AccessKeyID, - SecretAccessKey: creds.SecretAccessKey, - SessionToken: creds.SessionToken, - Source: creds.Source, - CanExpire: creds.CanExpire, - Expires: creds.Expires, - AccountID: creds.AccountID, - }, r, payload, service, region, signingTime) -} - // NewConfig will translate data from client options into a topology config for // building non-default deployments. Server and topology options are not honored // if a custom deployment is used. From 81cc95d444e7d81d2638b68ced94f638951c487e Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Mon, 22 Jun 2026 17:45:03 -0400 Subject: [PATCH 14/17] address comments --- ext/awsauth/awsauth.go | 4 +-- ext/awsauth/awsauth_example_test.go | 5 +-- ext/awsauth/doc.go | 2 -- internal/credutil/aws_options_provider.go | 37 +++++++++++++++++++++ mongo/client.go | 3 +- mongo/client_encryption.go | 24 ++----------- x/mongo/driver/topology/topology_options.go | 26 ++------------- 7 files changed, 47 insertions(+), 54 deletions(-) create mode 100644 internal/credutil/aws_options_provider.go diff --git a/ext/awsauth/awsauth.go b/ext/awsauth/awsauth.go index 9668fca89d..8e00c51ec4 100644 --- a/ext/awsauth/awsauth.go +++ b/ext/awsauth/awsauth.go @@ -24,10 +24,10 @@ type CredentialsProvider struct { // NewCredentialsProvider returns a CredentialsProvider that wraps AWS // CredentialsProvider. CredentialsProvider is expected to not be nil. -func NewCredentialsProvider(credentialsProvider aws.CredentialsProvider) (*CredentialsProvider, error) { +func NewCredentialsProvider(credentialsProvider aws.CredentialsProvider) *CredentialsProvider { return &CredentialsProvider{ provider: credentialsProvider, - }, nil + } } // Retrieve returns the credentials. diff --git a/ext/awsauth/awsauth_example_test.go b/ext/awsauth/awsauth_example_test.go index c98773c098..f9357cd824 100644 --- a/ext/awsauth/awsauth_example_test.go +++ b/ext/awsauth/awsauth_example_test.go @@ -20,10 +20,7 @@ func ExampleNewCredentialsProvider() { if err != nil { panic(err) } - awsCredentialProvider, err := awsauth.NewCredentialsProvider(cfg.Credentials) - if err != nil { - panic(err) - } + awsCredentialProvider := awsauth.NewCredentialsProvider(cfg.Credentials) credential := options.Credential{ AuthMechanism: "MONGODB-AWS", AWSCredentialsProvider: awsCredentialProvider, diff --git a/ext/awsauth/doc.go b/ext/awsauth/doc.go index b06e2b6561..1aa583a5b3 100644 --- a/ext/awsauth/doc.go +++ b/ext/awsauth/doc.go @@ -18,7 +18,5 @@ // ClientEncryptionOptions // AutoEncryptionOptions // -// NewSigner() adapts an AWS HTTPSigner to be used in: -// // ClientOptions package awsauth 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/mongo/client.go b/mongo/client.go index 141f2fe361..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" @@ -674,7 +675,7 @@ func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt KeyExpiration: opts.KeyExpiration, } if opts.AWSCredentialsProvider != nil { - cryptOpts.AWSCredentialsProvider = awsCredentialsProvider{opts.AWSCredentialsProvider} + cryptOpts.AWSCredentialsProvider = credutil.AWSOptionsProvider{Provider: opts.AWSCredentialsProvider} } mc, err := mongocrypt.NewMongoCrypt(cryptOpts) if err != nil { diff --git a/mongo/client_encryption.go b/mongo/client_encryption.go index a0fa962dc7..07169d592b 100644 --- a/mongo/client_encryption.go +++ b/mongo/client_encryption.go @@ -14,7 +14,7 @@ import ( "strings" "go.mongodb.org/mongo-driver/v2/bson" - "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" + "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" @@ -31,26 +31,6 @@ type ClientEncryption struct { closed bool } -type awsCredentialsProvider struct { - provider options.AWSCredentialsProvider -} - -func (p awsCredentialsProvider) 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 -} - // NewClientEncryption creates a new ClientEncryption instance configured with the given options. func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options.ClientEncryptionOptions]) (*ClientEncryption, error) { if keyVaultClient == nil { @@ -84,7 +64,7 @@ func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options. KeyExpiration: cea.KeyExpiration, } if cea.AWSCredentialsProvider != nil { - cryptOpts.AWSCredentialsProvider = awsCredentialsProvider{cea.AWSCredentialsProvider} + cryptOpts.AWSCredentialsProvider = credutil.AWSOptionsProvider{Provider: cea.AWSCredentialsProvider} } mc, err := mongocrypt.NewMongoCrypt(cryptOpts) if err != nil { diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index d96c9a83ec..0eb276c998 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -15,7 +15,7 @@ import ( "time" "go.mongodb.org/mongo-driver/v2/event" - "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" + "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" @@ -127,34 +127,14 @@ func ConvertCreds(credOpts *options.Credential) *driver.Cred { } if credOpts.AWSCredentialsProvider != nil { - cred.AWSCredentialsProvider = awsCredentialsProvider{ - provider: credOpts.AWSCredentialsProvider, + cred.AWSCredentialsProvider = credutil.AWSOptionsProvider{ + Provider: credOpts.AWSCredentialsProvider, } } return cred } -type awsCredentialsProvider struct { - provider options.AWSCredentialsProvider -} - -func (p awsCredentialsProvider) 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 -} - // NewConfig will translate data from client options into a topology config for // building non-default deployments. Server and topology options are not honored // if a custom deployment is used. From 38e7d0b2a21f35cb11d3485ae2170f385cb3fb34 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Wed, 24 Jun 2026 17:32:28 -0400 Subject: [PATCH 15/17] move aws auth examples to submodule --- .../example_test.go} | 5 +- ext/awsauth/examples/go.mod | 36 +++++++++ ext/awsauth/examples/go.sum | 76 +++++++++++++++++++ ext/awsauth/go.mod | 12 --- ext/awsauth/go.sum | 24 ------ go.work | 1 + 6 files changed, 116 insertions(+), 38 deletions(-) rename ext/awsauth/{awsauth_example_test.go => examples/example_test.go} (91%) create mode 100644 ext/awsauth/examples/go.mod create mode 100644 ext/awsauth/examples/go.sum diff --git a/ext/awsauth/awsauth_example_test.go b/ext/awsauth/examples/example_test.go similarity index 91% rename from ext/awsauth/awsauth_example_test.go rename to ext/awsauth/examples/example_test.go index f9357cd824..e83106c675 100644 --- a/ext/awsauth/awsauth_example_test.go +++ b/ext/awsauth/examples/example_test.go @@ -15,7 +15,7 @@ import ( "go.mongodb.org/mongo-driver/v2/mongo/options" ) -func ExampleNewCredentialsProvider() { +func Example_defaultConfig() { cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { panic(err) @@ -26,7 +26,8 @@ func ExampleNewCredentialsProvider() { AWSCredentialsProvider: awsCredentialProvider, } client, err := mongo.Connect( - options.Client().SetAuth(credential)) + options.Client().SetAuth(credential), + ) if err != nil { panic(err) } 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 index 75495fb2d4..46d5bde6ed 100644 --- a/ext/awsauth/go.mod +++ b/ext/awsauth/go.mod @@ -4,22 +4,10 @@ go 1.23 require ( github.com/aws/aws-sdk-go-v2 v1.41.1 - github.com/aws/aws-sdk-go-v2/config v1.32.7 go.mongodb.org/mongo-driver/v2 v2.0.0-beta2 ) require ( - github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect github.com/aws/smithy-go v1.24.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/klauspost/compress v1.17.6 // indirect diff --git a/ext/awsauth/go.sum b/ext/awsauth/go.sum index bab1d7b254..e6c9f856b6 100644 --- a/ext/awsauth/go.sum +++ b/ext/awsauth/go.sum @@ -1,29 +1,5 @@ github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= -github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= -github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/go.work b/go.work index a48c323220..14eaffe5d7 100644 --- a/go.work +++ b/go.work @@ -8,4 +8,5 @@ use ( ./internal/test/compilecheck ./internal/test/goleak ./ext/awsauth + ./ext/awsauth/examples ) From 7bede644352632f62a2f49540d55df0dcb40faab Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 25 Jun 2026 13:34:29 -0400 Subject: [PATCH 16/17] update AWSCredentialsProvider interface --- ext/awsauth/awsauth.go | 33 ++++++++++++++--- ext/awsauth/go.mod | 20 ++-------- ext/awsauth/go.sum | 67 ++++++++-------------------------- mongo/options/clientoptions.go | 2 +- 4 files changed, 46 insertions(+), 76 deletions(-) diff --git a/ext/awsauth/awsauth.go b/ext/awsauth/awsauth.go index 8e00c51ec4..31a1033e8d 100644 --- a/ext/awsauth/awsauth.go +++ b/ext/awsauth/awsauth.go @@ -8,16 +8,18 @@ package awsauth import ( "context" + "time" "github.com/aws/aws-sdk-go-v2/aws" - - "go.mongodb.org/mongo-driver/v2/mongo/options" ) -var _ options.AWSCredentialsProvider = (*CredentialsProvider)(nil) - // 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 } @@ -30,8 +32,27 @@ func NewCredentialsProvider(credentialsProvider aws.CredentialsProvider) *Creden } } +// 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) (options.AWSCredentials, error) { +func (p *CredentialsProvider) Retrieve(ctx context.Context) (AWSCredentials, error) { creds, err := p.provider.Retrieve(ctx) - return options.AWSCredentials(creds), err + 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/go.mod b/ext/awsauth/go.mod index 46d5bde6ed..9f8d556b8a 100644 --- a/ext/awsauth/go.mod +++ b/ext/awsauth/go.mod @@ -1,21 +1,7 @@ module go.mongodb.org/mongo-driver/ext/awsauth -go 1.23 +go 1.19 -require ( - github.com/aws/aws-sdk-go-v2 v1.41.1 - go.mongodb.org/mongo-driver/v2 v2.0.0-beta2 -) +require github.com/aws/aws-sdk-go-v2 v1.0.0 -require ( - github.com/aws/smithy-go v1.24.0 // 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 -) +require github.com/aws/smithy-go v1.0.0 // indirect diff --git a/ext/awsauth/go.sum b/ext/awsauth/go.sum index e6c9f856b6..a4ccc4d32f 100644 --- a/ext/awsauth/go.sum +++ b/ext/awsauth/go.sum @@ -1,52 +1,15 @@ -github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= -github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= -github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= -github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= -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= +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/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index 680bb325d8..5273a67c16 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -143,7 +143,7 @@ type AWSCredentialsProvider interface { } // AWSCredentials represents AWS credentials. -type AWSCredentials struct { +type AWSCredentials = struct { AccessKeyID string SecretAccessKey string SessionToken string From eb9de767830294817fdbe269dcdde3ac1b9469f4 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 25 Jun 2026 13:40:34 -0400 Subject: [PATCH 17/17] add prose tests --- .evergreen/config.yml | 2 + etc/run-mongodb-aws-test.sh | 5 + ext/awsauth/test/aws_test.go | 97 +++++++++++++++++ ext/awsauth/test/go.mod | 31 ++++++ ext/awsauth/test/go.sum | 66 ++++++++++++ go.work | 1 + internal/test/aws/aws_test.go | 191 +++++++++++++++++++++++++++++++++- 7 files changed, 389 insertions(+), 4 deletions(-) create mode 100644 ext/awsauth/test/aws_test.go create mode 100644 ext/awsauth/test/go.mod create mode 100644 ext/awsauth/test/go.sum 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/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 14eaffe5d7..998a35c2d9 100644 --- a/go.work +++ b/go.work @@ -9,4 +9,5 @@ use ( ./internal/test/goleak ./ext/awsauth ./ext/awsauth/examples + ./ext/awsauth/test ) 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") }