diff --git a/internal/aws/credentials/chain_provider.go b/internal/aws/credentials/chain_provider.go index 4f593c097b..4becf8aa03 100644 --- a/internal/aws/credentials/chain_provider.go +++ b/internal/aws/credentials/chain_provider.go @@ -11,25 +11,20 @@ package credentials import ( + "context" + "errors" + "go.mongodb.org/mongo-driver/v2/internal/aws/awserr" ) -// A ChainProvider will search for a provider which returns credentials -// and cache that provider until Retrieve is called again. -// // The ChainProvider provides a way of chaining multiple providers together // which will pick the first available using priority order of the Providers // in the list. // // If none of the Providers retrieve valid credentials Value, ChainProvider's -// Retrieve() will return the error ErrNoValidProvidersFoundInChain. -// -// If a Provider is found which returns valid credentials Value ChainProvider -// will cache that Provider for all calls to IsExpired(), until Retrieve is -// called again. +// Retrieve() will return an error. type ChainProvider struct { Providers []Provider - curr Provider } // NewChainCredentials returns a pointer to a new Credentials object @@ -42,31 +37,19 @@ func NewChainCredentials(providers []Provider) *Credentials { // Retrieve returns the credentials value or error if no provider returned // without error. -// -// If a provider is found it will be cached and any calls to IsExpired() -// will return the expired state of the cached provider. -func (c *ChainProvider) Retrieve() (Value, error) { +func (c *ChainProvider) Retrieve(ctx context.Context) (Value, error) { errs := make([]error, 0, len(c.Providers)) for _, p := range c.Providers { - creds, err := p.Retrieve() + creds, err := p.Retrieve(ctx) if err == nil { - c.curr = p - return creds, nil + if !creds.Expired() && creds.HasKeys() { + return creds, nil + } + err = errors.New("credentials are invalid") } errs = append(errs, err) } - c.curr = nil err := awserr.NewBatchError("NoCredentialProviders", "no valid providers in chain", errs) return Value{}, err } - -// IsExpired will returned the expired state of the currently cached provider -// if there is one. If there is no current provider, true will be returned. -func (c *ChainProvider) IsExpired() bool { - if c.curr != nil { - return c.curr.IsExpired() - } - - return true -} diff --git a/internal/aws/credentials/chain_provider_test.go b/internal/aws/credentials/chain_provider_test.go index 20644def01..bc026173c4 100644 --- a/internal/aws/credentials/chain_provider_test.go +++ b/internal/aws/credentials/chain_provider_test.go @@ -11,34 +11,19 @@ package credentials import ( + "context" "reflect" "testing" "go.mongodb.org/mongo-driver/v2/internal/aws/awserr" ) -type secondStubProvider struct { - creds Value - expired bool - err error -} - -func (s *secondStubProvider) Retrieve() (Value, error) { - s.expired = false - s.creds.ProviderName = "secondStubProvider" - return s.creds, s.err -} - -func (s *secondStubProvider) IsExpired() bool { - return s.expired -} - -func TestChainProviderWithNames(t *testing.T) { +func TestChainProviderRetrieve(t *testing.T) { p := &ChainProvider{ Providers: []Provider{ &stubProvider{err: awserr.New("FirstError", "first provider error", nil)}, &stubProvider{err: awserr.New("SecondError", "second provider error", nil)}, - &secondStubProvider{ + &stubProvider{ creds: Value{ AccessKeyID: "AKIF", SecretAccessKey: "NOSECRET", @@ -55,13 +40,10 @@ func TestChainProviderWithNames(t *testing.T) { }, } - creds, err := p.Retrieve() + creds, err := p.Retrieve(context.Background()) if err != nil { t.Errorf("Expect no error, got %v", err) } - if e, a := "secondStubProvider", creds.ProviderName; e != a { - t.Errorf("Expect provider name to match, %v got, %v", e, a) - } // Also check credentials if e, a := "AKIF", creds.AccessKeyID; e != a { @@ -75,78 +57,12 @@ func TestChainProviderWithNames(t *testing.T) { } } -func TestChainProviderGet(t *testing.T) { - p := &ChainProvider{ - Providers: []Provider{ - &stubProvider{err: awserr.New("FirstError", "first provider error", nil)}, - &stubProvider{err: awserr.New("SecondError", "second provider error", nil)}, - &stubProvider{ - creds: Value{ - AccessKeyID: "AKID", - SecretAccessKey: "SECRET", - SessionToken: "", - }, - }, - }, - } - - creds, err := p.Retrieve() - if err != nil { - t.Errorf("Expect no error, got %v", err) - } - if e, a := "AKID", creds.AccessKeyID; e != a { - t.Errorf("Expect access key ID to match, %v got %v", e, a) - } - if e, a := "SECRET", creds.SecretAccessKey; e != a { - t.Errorf("Expect secret access key to match, %v got %v", e, a) - } - if v := creds.SessionToken; len(v) != 0 { - t.Errorf("Expect session token to be empty, %v", v) - } -} - -func TestChainProviderIsExpired(t *testing.T) { - stubProvider := &stubProvider{expired: true} - p := &ChainProvider{ - Providers: []Provider{ - stubProvider, - }, - } - - if !p.IsExpired() { - t.Errorf("Expect expired to be true before any Retrieve") - } - _, err := p.Retrieve() - if err != nil { - t.Errorf("Expect no error, got %v", err) - } - if p.IsExpired() { - t.Errorf("Expect not expired after retrieve") - } - - stubProvider.expired = true - if !p.IsExpired() { - t.Errorf("Expect return of expired provider") - } - - _, err = p.Retrieve() - if err != nil { - t.Errorf("Expect no error, got %v", err) - } - if p.IsExpired() { - t.Errorf("Expect not expired after retrieve") - } -} - func TestChainProviderWithNoProvider(t *testing.T) { p := &ChainProvider{ Providers: []Provider{}, } - if !p.IsExpired() { - t.Errorf("Expect expired with no providers") - } - _, err := p.Retrieve() + _, err := p.Retrieve(context.Background()) if err.Error() != "NoCredentialProviders: no valid providers in chain" { t.Errorf("Expect no providers error returned, got %v", err) } @@ -164,10 +80,7 @@ func TestChainProviderWithNoValidProvider(t *testing.T) { }, } - if !p.IsExpired() { - t.Errorf("Expect expired with no providers") - } - _, err := p.Retrieve() + _, err := p.Retrieve(context.Background()) expectErr := awserr.NewBatchError("NoCredentialProviders", "no valid providers in chain", errs) if e, a := expectErr, err; !reflect.DeepEqual(e, a) { diff --git a/internal/aws/credentials/credentials.go b/internal/aws/credentials/credentials.go index 919d0819b1..f64fc223f8 100644 --- a/internal/aws/credentials/credentials.go +++ b/internal/aws/credentials/credentials.go @@ -12,7 +12,7 @@ package credentials import ( "context" - "sync" + "sync/atomic" "time" "go.mongodb.org/mongo-driver/v2/internal/aws/awserr" @@ -33,8 +33,30 @@ type Value struct { // AWS Session Token SessionToken string - // Provider used to get credentials - ProviderName string + // Source of the credentials + Source string + + // States if the credentials can expire or not. + CanExpire bool + + // The time the credentials will expire at. Should be ignored if CanExpire + // is false. + Expires time.Time + + // The ID of the account for the credentials. + AccountID string +} + +// Expired returns if the credentials have expired. +func (v Value) Expired() bool { + if v.CanExpire { + // Calling Round(0) on the current time will truncate the monotonic + // reading only. Ensures credential expiry time is always based on + // reported wall-clock time. + return !v.Expires.After(time.Now().Round(0)) + } + + return false } // HasKeys returns if the credentials Value has both AccessKeyID and @@ -52,18 +74,7 @@ func (v Value) HasKeys() bool { type Provider interface { // Retrieve returns nil if it successfully retrieved the value. // Error is returned if the value were not obtainable, or empty. - Retrieve() (Value, error) - - // IsExpired returns if the credentials are no longer valid, and need - // to be retrieved. - IsExpired() bool -} - -// ProviderWithContext is a Provider that can retrieve credentials with a Context -type ProviderWithContext interface { - Provider - - RetrieveWithContext(context.Context) (Value, error) + Retrieve(context.Context) (Value, error) } // A Credentials provides concurrency safe retrieval of AWS credentials Value. @@ -79,13 +90,12 @@ type ProviderWithContext interface { // // The first Credentials.Get() will always call Provider.Retrieve() to get the // first instance of the credentials Value. All calls to Get() after that -// will return the cached credentials Value until IsExpired() returns true. +// will return the cached credentials Value until Expired() returns true. type Credentials struct { - sf singleflight.Group - - m sync.RWMutex - creds Value provider Provider + + creds atomic.Value + sf singleflight.Group } // NewCredentials returns a pointer to a new Credentials with the provider set. @@ -96,34 +106,18 @@ func NewCredentials(provider Provider) *Credentials { return c } -// GetWithContext returns the credentials value, or error if the credentials +// Get returns the credentials value, or error if the credentials // Value failed to be retrieved. Will return early if the passed in context is // canceled. // // Will return the cached credentials Value if it has not expired. If the // credentials Value has expired the Provider's Retrieve() will be called // to refresh the credentials. -// -// If Credentials.Expire() was called the credentials Value will be force -// expired, and the next call to Get() will cause them to be refreshed. -func (c *Credentials) GetWithContext(ctx context.Context) (Value, error) { - // Check if credentials are cached, and not expired. - select { - case curCreds, ok := <-c.asyncIsExpired(): - // ok will only be true, of the credentials were not expired. ok will - // be false and have no value if the credentials are expired. - if ok { - return curCreds, nil - } - case <-ctx.Done(): - return Value{}, awserr.New("RequestCanceled", - "request context canceled", ctx.Err()) - } - +func (c *Credentials) Get(ctx context.Context) (Value, error) { // Cannot pass context down to the actual retrieve, because the first // context would cancel the whole group when there is not direct // association of items in the group. - resCh := c.sf.DoChan("", func() (interface{}, error) { + resCh := c.sf.DoChan("", func() (any, error) { return c.singleRetrieve(&suppressedContext{ctx}) }) select { @@ -135,49 +129,33 @@ func (c *Credentials) GetWithContext(ctx context.Context) (Value, error) { } } -func (c *Credentials) singleRetrieve(ctx context.Context) (interface{}, error) { - c.m.Lock() - defer c.m.Unlock() - - if curCreds := c.creds; !c.isExpiredLocked(curCreds) { - return curCreds, nil +func (c *Credentials) singleRetrieve(ctx context.Context) (any, error) { + if currCreds, ok := c.getCreds(); ok && !currCreds.Expired() { + return currCreds, nil } - var creds Value - var err error - if p, ok := c.provider.(ProviderWithContext); ok { - creds, err = p.RetrieveWithContext(ctx) - } else { - creds, err = c.provider.Retrieve() - } + newCreds, err := c.provider.Retrieve(ctx) if err == nil { - c.creds = creds + c.creds.Store(&newCreds) } - return creds, err + return newCreds, err } -// asyncIsExpired returns a channel of credentials Value. If the channel is -// closed the credentials are expired and credentials value are not empty. -func (c *Credentials) asyncIsExpired() <-chan Value { - ch := make(chan Value, 1) - go func() { - c.m.RLock() - defer c.m.RUnlock() - - if curCreds := c.creds; !c.isExpiredLocked(curCreds) { - ch <- curCreds - } - - close(ch) - }() +// getCreds returns the currently stored credentials and true. Returning false +// if no credentials were stored. +func (c *Credentials) getCreds() (Value, bool) { + v := c.creds.Load() + if v == nil { + return Value{}, false + } - return ch -} + val := v.(*Value) + if val == nil || !val.HasKeys() { + return Value{}, false + } -// isExpiredLocked helper method wrapping the definition of expired credentials. -func (c *Credentials) isExpiredLocked(creds interface{}) bool { - return creds == nil || creds.(Value) == Value{} || c.provider.IsExpired() + return *val, true } type suppressedContext struct { diff --git a/internal/aws/credentials/credentials_test.go b/internal/aws/credentials/credentials_test.go index cbe19ce15f..42a2290ed0 100644 --- a/internal/aws/credentials/credentials_test.go +++ b/internal/aws/credentials/credentials_test.go @@ -12,38 +12,21 @@ package credentials import ( "context" - "sync" "testing" "time" "go.mongodb.org/mongo-driver/v2/internal/aws/awserr" ) -func isExpired(c *Credentials) bool { - c.m.RLock() - defer c.m.RUnlock() - - return c.isExpiredLocked(c.creds) -} - type stubProvider struct { - creds Value - retrievedCount int - expired bool - err error + creds Value + err error } -func (s *stubProvider) Retrieve() (Value, error) { - s.retrievedCount++ - s.expired = false - s.creds.ProviderName = "stubProvider" +func (s *stubProvider) Retrieve(_ context.Context) (Value, error) { return s.creds, s.err } -func (s *stubProvider) IsExpired() bool { - return s.expired -} - func TestCredentialsGet(t *testing.T) { c := NewCredentials(&stubProvider{ creds: Value{ @@ -51,10 +34,9 @@ func TestCredentialsGet(t *testing.T) { SecretAccessKey: "SECRET", SessionToken: "", }, - expired: true, }) - creds, err := c.GetWithContext(context.Background()) + creds, err := c.Get(context.Background()) if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -70,102 +52,22 @@ func TestCredentialsGet(t *testing.T) { } func TestCredentialsGetWithError(t *testing.T) { - c := NewCredentials(&stubProvider{err: awserr.New("provider error", "", nil), expired: true}) + c := NewCredentials(&stubProvider{err: awserr.New("provider error", "", nil)}) - _, err := c.GetWithContext(context.Background()) + _, err := c.Get(context.Background()) if e, a := "provider error", err.(awserr.Error).Code(); e != a { t.Errorf("Expected provider error, %v got %v", e, a) } } -func TestCredentialsExpire(t *testing.T) { - stub := &stubProvider{} - c := NewCredentials(stub) - - stub.expired = false - if !isExpired(c) { - t.Errorf("Expected to start out expired") - } - - _, err := c.GetWithContext(context.Background()) - if err != nil { - t.Errorf("Expected no err, got %v", err) - } - if isExpired(c) { - t.Errorf("Expected not to be expired") - } - - stub.expired = true - if !isExpired(c) { - t.Errorf("Expected to be expired") - } -} - -func TestCredentialsGetWithProviderName(t *testing.T) { - stub := &stubProvider{} - - c := NewCredentials(stub) - - creds, err := c.GetWithContext(context.Background()) - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if e, a := creds.ProviderName, "stubProvider"; e != a { - t.Errorf("Expected provider name to match, %v got %v", e, a) - } -} - -type MockProvider struct { - // The date/time when to expire on - expiration time.Time - - // If set will be used by IsExpired to determine the current time. - // Defaults to time.Now if CurrentTime is not set. Available for testing - // to be able to mock out the current time. - CurrentTime func() time.Time -} - -// IsExpired returns if the credentials are expired. -func (e *MockProvider) IsExpired() bool { - curTime := e.CurrentTime - if curTime == nil { - curTime = time.Now - } - return e.expiration.Before(curTime()) -} - -func (*MockProvider) Retrieve() (Value, error) { - return Value{}, nil -} - -func TestCredentialsIsExpired_Race(_ *testing.T) { - creds := NewChainCredentials([]Provider{&MockProvider{}}) - - starter := make(chan struct{}) - var wg sync.WaitGroup - wg.Add(10) - for i := 0; i < 10; i++ { - go func() { - defer wg.Done() - <-starter - for i := 0; i < 100; i++ { - isExpired(creds) - } - }() - } - close(starter) - - wg.Wait() -} - type stubProviderConcurrent struct { stubProvider done chan struct{} } -func (s *stubProviderConcurrent) Retrieve() (Value, error) { +func (s *stubProviderConcurrent) Retrieve(ctx context.Context) (Value, error) { <-s.done - return s.stubProvider.Retrieve() + return s.stubProvider.Retrieve(ctx) } func TestCredentialsGetConcurrent(t *testing.T) { @@ -178,7 +80,7 @@ func TestCredentialsGetConcurrent(t *testing.T) { for i := 0; i < 2; i++ { go func() { - _, err := c.GetWithContext(context.Background()) + _, err := c.Get(context.Background()) if err != nil { t.Errorf("Expected no err, got %v", err) } @@ -187,6 +89,7 @@ func TestCredentialsGetConcurrent(t *testing.T) { } // Validates that a single call to Retrieve is shared between two calls to Get + time.Sleep(10 * time.Millisecond) stub.done <- struct{}{} <-done <-done diff --git a/internal/aws/signer/v4/v4.go b/internal/aws/signer/v4/v4.go index eaf5cca3ee..4bc9fa130e 100644 --- a/internal/aws/signer/v4/v4.go +++ b/internal/aws/signer/v4/v4.go @@ -97,11 +97,10 @@ type signingCtx struct { // is not needed as the full request context will be captured by the http.Request // value. It is included for reference though. // -// Sign will set the request's Body to be the `body` parameter passed in. If -// the body is not already an io.ReadCloser, it will be wrapped within one. If -// a `nil` body parameter passed to Sign, the request's Body field will be -// also set to nil. Its important to note that this functionality will not -// change the request's ContentLength of the request. +// Sign will set the request's Body to be the `body` parameter passed in. If an +// empty body parameter passed to Sign, the request's Body field will be set to +// nil. Its important to note that this functionality will not change the +// request's ContentLength of the request. // // Sign differs from Presign in that it will sign the request using HTTP // header values. This type of signing is intended for http.Request values that @@ -135,7 +134,7 @@ func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, regi } var err error - ctx.credValues, err = v4.Credentials.GetWithContext(r.Context()) + ctx.credValues, err = v4.Credentials.Get(r.Context()) if err != nil { return http.Header{}, err } diff --git a/internal/credproviders/assume_role_provider.go b/internal/credproviders/assume_role_provider.go index eec2247c70..1965799613 100644 --- a/internal/credproviders/assume_role_provider.go +++ b/internal/credproviders/assume_role_provider.go @@ -20,9 +20,6 @@ import ( ) const ( - // assumeRoleProviderName provides a name of assume role provider - assumeRoleProviderName = "AssumeRoleProvider" - stsURI = `https://sts.amazonaws.com/?Action=AssumeRoleWithWebIdentity&RoleSessionName=%s&RoleArn=%s&WebIdentityToken=%s&Version=2011-06-15` ) @@ -33,12 +30,11 @@ type AssumeRoleProvider struct { AwsRoleSessionNameEnv EnvVar httpClient *http.Client - expiration time.Time // expiryWindow will allow the credentials to trigger refreshing prior to the credentials actually expiring. // This is beneficial so expiring credentials do not cause request to fail unexpectedly due to exceptions. // - // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true + // E.g., an ExpiryWindow of 10s would cause calls to Expired() to return true // 10 seconds before the credentials are actually expired. expiryWindow time.Duration } @@ -57,11 +53,11 @@ func NewAssumeRoleProvider(httpClient *http.Client, expiryWindow time.Duration) } } -// RetrieveWithContext retrieves the keys from the AWS service. -func (a *AssumeRoleProvider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) { +// Retrieve retrieves the keys from the AWS service. +func (a *AssumeRoleProvider) Retrieve(ctx context.Context) (credentials.Value, error) { const defaultHTTPTimeout = 10 * time.Second - v := credentials.Value{ProviderName: assumeRoleProviderName} + var v credentials.Value roleArn := a.AwsRoleArnEnv.Get() tokenFile := a.AwsWebIdentityTokenFileEnv.Get() @@ -131,18 +127,9 @@ func (a *AssumeRoleProvider) RetrieveWithContext(ctx context.Context) (credentia if !v.HasKeys() { return v, errors.New("failed to retrieve web identity keys") } + v.CanExpire = true sec := int64(stsResp.Response.Result.Credentials.Expiration) - a.expiration = time.Unix(sec, 0).Add(-a.expiryWindow) + v.Expires = time.Unix(sec, 0).Add(-a.expiryWindow) return v, nil } - -// Retrieve retrieves the keys from the AWS service. -func (a *AssumeRoleProvider) Retrieve() (credentials.Value, error) { - return a.RetrieveWithContext(context.Background()) -} - -// IsExpired returns true if the credentials are expired. -func (a *AssumeRoleProvider) IsExpired() bool { - return a.expiration.Before(time.Now()) -} diff --git a/internal/credproviders/ec2_provider.go b/internal/credproviders/ec2_provider.go index df15a7f2c3..2de0a2857d 100644 --- a/internal/credproviders/ec2_provider.go +++ b/internal/credproviders/ec2_provider.go @@ -19,9 +19,6 @@ import ( ) const ( - // ec2ProviderName provides a name of EC2 provider - ec2ProviderName = "EC2Provider" - awsEC2URI = "http://169.254.169.254/" awsEC2RolePath = "latest/meta-data/iam/security-credentials/" awsEC2TokenPath = "latest/api/token" @@ -32,12 +29,11 @@ const ( // An EC2Provider retrieves credentials from EC2 metadata. type EC2Provider struct { httpClient *http.Client - expiration time.Time // expiryWindow will allow the credentials to trigger refreshing prior to the credentials actually expiring. // This is beneficial so expiring credentials do not cause request to fail unexpectedly due to exceptions. // - // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true + // E.g., an ExpiryWindow of 10s would cause calls to Expired() to return true // 10 seconds before the credentials are actually expired. expiryWindow time.Duration } @@ -108,7 +104,7 @@ func (e *EC2Provider) getRoleName(ctx context.Context, token string) (string, er } func (e *EC2Provider) getCredentials(ctx context.Context, token string, role string) (credentials.Value, time.Time, error) { - v := credentials.Value{ProviderName: ec2ProviderName} + var v credentials.Value pathWithRole := awsEC2URI + awsEC2RolePath + role req, err := http.NewRequest(http.MethodGet, pathWithRole, nil) @@ -146,9 +142,9 @@ func (e *EC2Provider) getCredentials(ctx context.Context, token string, role str return v, ec2Resp.Expiration, nil } -// RetrieveWithContext retrieves the keys from the AWS service. -func (e *EC2Provider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) { - v := credentials.Value{ProviderName: ec2ProviderName} +// Retrieve retrieves the keys from the AWS service. +func (e *EC2Provider) Retrieve(ctx context.Context) (credentials.Value, error) { + var v credentials.Value token, err := e.getToken(ctx) if err != nil { @@ -167,17 +163,8 @@ func (e *EC2Provider) RetrieveWithContext(ctx context.Context) (credentials.Valu if !v.HasKeys() { return v, errors.New("failed to retrieve EC2 keys") } - e.expiration = exp.Add(-e.expiryWindow) + v.CanExpire = true + v.Expires = exp.Add(-e.expiryWindow) return v, nil } - -// Retrieve retrieves the keys from the AWS service. -func (e *EC2Provider) Retrieve() (credentials.Value, error) { - return e.RetrieveWithContext(context.Background()) -} - -// IsExpired returns true if the credentials are expired. -func (e *EC2Provider) IsExpired() bool { - return e.expiration.Before(time.Now()) -} diff --git a/internal/credproviders/ecs_provider.go b/internal/credproviders/ecs_provider.go index 6816a3182d..e7b7ab643b 100644 --- a/internal/credproviders/ecs_provider.go +++ b/internal/credproviders/ecs_provider.go @@ -18,9 +18,6 @@ import ( ) const ( - // ecsProviderName provides a name of ECS provider - ecsProviderName = "ECSProvider" - awsRelativeURI = "http://169.254.170.2/" ) @@ -29,12 +26,11 @@ type ECSProvider struct { AwsContainerCredentialsRelativeURIEnv EnvVar httpClient *http.Client - expiration time.Time // expiryWindow will allow the credentials to trigger refreshing prior to the credentials actually expiring. // This is beneficial so expiring credentials do not cause request to fail unexpectedly due to exceptions. // - // So a ExpiryWindow of 10s would cause calls to IsExpired() to return true + // E.g., an ExpiryWindow of 10s would cause calls to Expired() to return true // 10 seconds before the credentials are actually expired. expiryWindow time.Duration } @@ -49,11 +45,11 @@ func NewECSProvider(httpClient *http.Client, expiryWindow time.Duration) *ECSPro } } -// RetrieveWithContext retrieves the keys from the AWS service. -func (e *ECSProvider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) { +// Retrieve retrieves the keys from the AWS service. +func (e *ECSProvider) Retrieve(ctx context.Context) (credentials.Value, error) { const defaultHTTPTimeout = 10 * time.Second - v := credentials.Value{ProviderName: ecsProviderName} + var v credentials.Value relativeEcsURI := e.AwsContainerCredentialsRelativeURIEnv.Get() if len(relativeEcsURI) == 0 { @@ -96,17 +92,8 @@ func (e *ECSProvider) RetrieveWithContext(ctx context.Context) (credentials.Valu if !v.HasKeys() { return v, errors.New("failed to retrieve ECS keys") } - e.expiration = ecsResp.Expiration.Add(-e.expiryWindow) + v.CanExpire = true + v.Expires = ecsResp.Expiration.Add(-e.expiryWindow) return v, nil } - -// Retrieve retrieves the keys from the AWS service. -func (e *ECSProvider) Retrieve() (credentials.Value, error) { - return e.RetrieveWithContext(context.Background()) -} - -// IsExpired returns true if the credentials are expired. -func (e *ECSProvider) IsExpired() bool { - return e.expiration.Before(time.Now()) -} diff --git a/internal/credproviders/env_provider.go b/internal/credproviders/env_provider.go index cf6bb60b31..2483c2dcd0 100644 --- a/internal/credproviders/env_provider.go +++ b/internal/credproviders/env_provider.go @@ -7,14 +7,12 @@ package credproviders import ( + "context" "os" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" ) -// envProviderName provides a name of Env provider -const envProviderName = "EnvProvider" - // EnvVar is an environment variable type EnvVar string @@ -29,8 +27,6 @@ type EnvProvider struct { AwsAccessKeyIDEnv EnvVar AwsSecretAccessKeyEnv EnvVar AwsSessionTokenEnv EnvVar - - retrieved bool } // NewEnvProvider returns a pointer to an ECS credential provider. @@ -46,24 +42,18 @@ func NewEnvProvider() *EnvProvider { } // Retrieve retrieves the keys from the environment. -func (e *EnvProvider) Retrieve() (credentials.Value, error) { - e.retrieved = false - +func (e *EnvProvider) Retrieve(_ context.Context) (credentials.Value, error) { v := credentials.Value{ AccessKeyID: e.AwsAccessKeyIDEnv.Get(), SecretAccessKey: e.AwsSecretAccessKeyEnv.Get(), SessionToken: e.AwsSessionTokenEnv.Get(), - ProviderName: envProviderName, + CanExpire: false, } err := verify(v) - if err == nil { - e.retrieved = true + if err != nil { + // Expire the credentials if invalid. + v.CanExpire = true } return v, err } - -// IsExpired returns true if the credentials have not been retrieved. -func (e *EnvProvider) IsExpired() bool { - return !e.retrieved -} diff --git a/internal/credproviders/imds_provider.go b/internal/credproviders/imds_provider.go index f3674c727b..005b61da10 100644 --- a/internal/credproviders/imds_provider.go +++ b/internal/credproviders/imds_provider.go @@ -19,16 +19,12 @@ import ( ) const ( - // AzureProviderName provides a name of Azure provider - AzureProviderName = "AzureProvider" - azureURI = "http://169.254.169.254/metadata/identity/oauth2/token" ) // An AzureProvider retrieves credentials from Azure IMDS. type AzureProvider struct { httpClient *http.Client - expiration time.Time expiryWindow time.Duration } @@ -36,14 +32,13 @@ type AzureProvider struct { func NewAzureProvider(httpClient *http.Client, expiryWindow time.Duration) *AzureProvider { return &AzureProvider{ httpClient: httpClient, - expiration: time.Time{}, expiryWindow: expiryWindow, } } -// RetrieveWithContext retrieves the keys from the Azure service. -func (a *AzureProvider) RetrieveWithContext(ctx context.Context) (credentials.Value, error) { - v := credentials.Value{ProviderName: AzureProviderName} +// Retrieve retrieves the keys from the Azure service. +func (a *AzureProvider) Retrieve(ctx context.Context) (credentials.Value, error) { + var v credentials.Value req, err := http.NewRequest(http.MethodGet, azureURI, nil) if err != nil { return v, fmt.Errorf("unable to retrieve Azure credentials: %w", err) @@ -85,19 +80,10 @@ func (a *AzureProvider) RetrieveWithContext(ctx context.Context) (credentials.Va if err != nil { return v, err } + v.CanExpire = true if expiration := expiresIn - a.expiryWindow; expiration > 0 { - a.expiration = time.Now().Add(expiration) + v.Expires = time.Now().Add(expiration) } return v, err } - -// Retrieve retrieves the keys from the Azure service. -func (a *AzureProvider) Retrieve() (credentials.Value, error) { - return a.RetrieveWithContext(context.Background()) -} - -// IsExpired returns if the credentials have been retrieved. -func (a *AzureProvider) IsExpired() bool { - return a.expiration.Before(time.Now()) -} diff --git a/internal/credproviders/static_provider.go b/internal/credproviders/static_provider.go index c7194cd0f5..2484e493cd 100644 --- a/internal/credproviders/static_provider.go +++ b/internal/credproviders/static_provider.go @@ -7,14 +7,12 @@ package credproviders import ( + "context" "errors" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" ) -// staticProviderName provides a name of Static provider -const staticProviderName = "StaticProvider" - // A StaticProvider is a set of credentials which are set programmatically, // and will never expire. type StaticProvider struct { @@ -41,18 +39,10 @@ func verify(v credentials.Value) error { } // Retrieve returns the credentials or error if the credentials are invalid. -func (s *StaticProvider) Retrieve() (credentials.Value, error) { +func (s *StaticProvider) Retrieve(_ context.Context) (credentials.Value, error) { if !s.verified { s.err = verify(s.Value) - s.ProviderName = staticProviderName s.verified = true } return s.Value, s.err } - -// IsExpired returns if the credentials are expired. -// -// For StaticProvider, the credentials never expired. -func (s *StaticProvider) IsExpired() bool { - return false -} diff --git a/x/mongo/driver/auth/aws_conv.go b/x/mongo/driver/auth/aws_sasl_adapter.go similarity index 64% rename from x/mongo/driver/auth/aws_conv.go rename to x/mongo/driver/auth/aws_sasl_adapter.go index 06dd04376c..3e97009ec7 100644 --- a/x/mongo/driver/auth/aws_conv.go +++ b/x/mongo/driver/auth/aws_sasl_adapter.go @@ -18,7 +18,6 @@ import ( "time" "go.mongodb.org/mongo-driver/v2/bson" - "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" v4signer "go.mongodb.org/mongo-driver/v2/internal/aws/signer/v4" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" ) @@ -32,59 +31,63 @@ const ( clientDone ) -type awsConversation struct { - state clientState - valid bool - nonce []byte - credentials *credentials.Credentials +type awsSaslAdapter struct { + signer *v4signer.Signer + + state clientState + nonce []byte +} + +var _ SaslClient = (*awsSaslAdapter)(nil) + +func (a *awsSaslAdapter) Start() (string, []byte, error) { + step, err := a.step(nil) + if err != nil { + return MongoDBAWS, nil, err + } + return MongoDBAWS, step, nil +} + +func (a *awsSaslAdapter) Next(_ context.Context, challenge []byte) ([]byte, error) { + step, err := a.step(challenge) + if err != nil { + return nil, err + } + return step, nil } -type serverMessage struct { - Nonce bson.Binary `bson:"s"` - Host string `bson:"h"` +func (a *awsSaslAdapter) Completed() bool { + return a.state == clientDone } const ( amzDateFormat = "20060102T150405Z" defaultRegion = "us-east-1" maxHostLength = 255 - responceNonceLength = 64 + responseNonceLength = 64 ) -// Step takes a string provided from a server (or just an empty string for the +// step takes a string provided from a server (or just an empty string for the // very first conversation step) and attempts to move the authentication // conversation forward. It returns a string to be sent to the server or an // error if the server message is invalid. Calling Step after a conversation // completes is also an error. -func (ac *awsConversation) Step(challenge []byte) (response []byte, err error) { - switch ac.state { +func (a *awsSaslAdapter) step(challenge []byte) (response []byte, err error) { + switch a.state { case clientStarting: - ac.state = clientFirst - response = ac.firstMsg() + a.state = clientFirst + response = a.firstMsg() case clientFirst: - ac.state = clientFinal - response, err = ac.finalMsg(challenge) + a.state = clientFinal + response, err = a.finalMsg(challenge) case clientFinal: - ac.state = clientDone - ac.valid = true + a.state = clientDone default: response, err = nil, errors.New("conversation already completed") } return } -// Done returns true if the conversation is completed or has errored. -func (ac *awsConversation) Done() bool { - return ac.state == clientDone -} - -// Valid returns true if the conversation successfully authenticated with the -// server, including counter-validation that the server actually has the -// user's stored credentials. -func (ac *awsConversation) Valid() bool { - return ac.valid -} - func getRegion(host string) (string, error) { region := defaultRegion @@ -111,20 +114,23 @@ func getRegion(host string) (string, error) { return region, nil } -func (ac *awsConversation) firstMsg() []byte { +func (a *awsSaslAdapter) firstMsg() []byte { // Values are cached for use in final message parameters - ac.nonce = make([]byte, 32) - _, _ = rand.Read(ac.nonce) + a.nonce = make([]byte, 32) + _, _ = rand.Read(a.nonce) idx, msg := bsoncore.AppendDocumentStart(nil) msg = bsoncore.AppendInt32Element(msg, "p", 110) - msg = bsoncore.AppendBinaryElement(msg, "r", 0x00, ac.nonce) + msg = bsoncore.AppendBinaryElement(msg, "r", 0x00, a.nonce) msg, _ = bsoncore.AppendDocumentEnd(msg, idx) return msg } -func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) { - var sm serverMessage +func (a *awsSaslAdapter) finalMsg(s1 []byte) ([]byte, error) { + var sm struct { + Nonce bson.Binary `bson:"s"` + Host string `bson:"h"` + } err := bson.Unmarshal(s1, &sm) if err != nil { return nil, err @@ -134,10 +140,10 @@ func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) { if sm.Nonce.Subtype != 0x00 { return nil, errors.New("server reply contained unexpected binary subtype") } - if len(sm.Nonce.Data) != responceNonceLength { - return nil, fmt.Errorf("server reply nonce was not %v bytes", responceNonceLength) + if len(sm.Nonce.Data) != responseNonceLength { + return nil, fmt.Errorf("server reply nonce was not %v bytes", responseNonceLength) } - if !bytes.HasPrefix(sm.Nonce.Data, ac.nonce) { + if !bytes.HasPrefix(sm.Nonce.Data, a.nonce) { return nil, errors.New("server nonce did not extend client nonce") } @@ -146,31 +152,23 @@ func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) { return nil, err } - creds, err := ac.credentials.GetWithContext(context.Background()) - if err != nil { - return nil, err - } - currentTime := time.Now().UTC() body := "Action=GetCallerIdentity&Version=2011-06-15" // Create http.Request - req, _ := http.NewRequest("POST", "/", strings.NewReader(body)) + req, err := http.NewRequest("POST", "/", strings.NewReader(body)) + if err != nil { + return nil, err + } + req.Host = sm.Host req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Length", "43") - req.Host = sm.Host req.Header.Set("X-Amz-Date", currentTime.Format(amzDateFormat)) - if len(creds.SessionToken) > 0 { - req.Header.Set("X-Amz-Security-Token", creds.SessionToken) - } req.Header.Set("X-MongoDB-Server-Nonce", base64.StdEncoding.EncodeToString(sm.Nonce.Data)) req.Header.Set("X-MongoDB-GS2-CB-Flag", "n") - // Create signer with credentials - signer := v4signer.NewSigner(ac.credentials) - // Get signed header - _, err = signer.Sign(req, strings.NewReader(body), "sts", region, currentTime) + _, err = a.signer.Sign(req, strings.NewReader(body), "sts", region, currentTime) if err != nil { return nil, err } @@ -179,8 +177,8 @@ func (ac *awsConversation) finalMsg(s1 []byte) ([]byte, error) { idx, msg := bsoncore.AppendDocumentStart(nil) msg = bsoncore.AppendStringElement(msg, "a", req.Header.Get("Authorization")) msg = bsoncore.AppendStringElement(msg, "d", req.Header.Get("X-Amz-Date")) - if len(creds.SessionToken) > 0 { - msg = bsoncore.AppendStringElement(msg, "t", creds.SessionToken) + if sessionToken := req.Header.Get("X-Amz-Security-Token"); len(sessionToken) > 0 { + msg = bsoncore.AppendStringElement(msg, "t", sessionToken) } msg, _ = bsoncore.AppendDocumentEnd(msg, idx) diff --git a/x/mongo/driver/auth/creds/awscreds.go b/x/mongo/driver/auth/creds/awscreds.go index 36595c2090..c156031a00 100644 --- a/x/mongo/driver/auth/creds/awscreds.go +++ b/x/mongo/driver/auth/creds/awscreds.go @@ -44,7 +44,7 @@ func NewAWSCredentialProvider(httpClient *http.Client, providers ...credentials. // GetCredentialsDoc generates AWS credentials. func (p AWSCredentialProvider) GetCredentialsDoc(ctx context.Context) (bsoncore.Document, error) { - creds, err := p.Cred.GetWithContext(ctx) + creds, err := p.Cred.Get(ctx) if err != nil { return nil, err } diff --git a/x/mongo/driver/auth/creds/azurecreds.go b/x/mongo/driver/auth/creds/azurecreds.go index c6d4b473af..d2f16ef982 100644 --- a/x/mongo/driver/auth/creds/azurecreds.go +++ b/x/mongo/driver/auth/creds/azurecreds.go @@ -30,7 +30,7 @@ func NewAzureCredentialProvider(httpClient *http.Client) AzureCredentialProvider // GetCredentialsDoc generates Azure credentials. func (p AzureCredentialProvider) GetCredentialsDoc(ctx context.Context) (bsoncore.Document, error) { - creds, err := p.cred.GetWithContext(ctx) + creds, err := p.cred.Get(ctx) if err != nil { return nil, err } diff --git a/x/mongo/driver/auth/creds/credscaching_test.go b/x/mongo/driver/auth/creds/credscaching_test.go index ad464fe7ed..b7a4f0d9b3 100644 --- a/x/mongo/driver/auth/creds/credscaching_test.go +++ b/x/mongo/driver/auth/creds/credscaching_test.go @@ -53,20 +53,30 @@ func TestAWSCredentialProviderCaching(t *testing.T) { t.Setenv(urienv, testEndpoint) testCases := []struct { - expiration time.Duration - reqCount uint32 + expiration time.Duration + reqCount uint32 + assertError func(t assert.TestingT, err error) bool }{ { expiration: 20 * time.Minute, reqCount: 1, + assertError: func(t assert.TestingT, err error) bool { + return assert.NoError(t, err, "error in GetCredentialsDoc") + }, }, { expiration: 5 * time.Minute, reqCount: 2, + assertError: func(t assert.TestingT, err error) bool { + return assert.ErrorContains(t, err, "credentials are invalid") + }, }, { expiration: -1 * time.Minute, reqCount: 2, + assertError: func(t assert.TestingT, err error) bool { + return assert.ErrorContains(t, err, "credentials are invalid") + }, }, } for _, tc := range testCases { @@ -108,9 +118,9 @@ func TestAWSCredentialProviderCaching(t *testing.T) { p := AWSCredentialProvider{credentials.NewChainCredentials([]credentials.Provider{env, ecs})} var err error _, err = p.GetCredentialsDoc(context.Background()) - assert.NoError(t, err, "error in GetCredentialsDoc") + tc.assertError(t, err) _, err = p.GetCredentialsDoc(context.Background()) - assert.NoError(t, err, "error in GetCredentialsDoc") + tc.assertError(t, err) assert.Equal(t, tc.reqCount, atomic.LoadUint32(&cnt), "expected and actual credentials retrieval count don't match") }) } diff --git a/x/mongo/driver/auth/mongodbaws.go b/x/mongo/driver/auth/mongodbaws.go index dd9661e1a9..7961b83828 100644 --- a/x/mongo/driver/auth/mongodbaws.go +++ b/x/mongo/driver/auth/mongodbaws.go @@ -12,6 +12,7 @@ import ( "net/http" "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" + v4signer "go.mongodb.org/mongo-driver/v2/internal/aws/signer/v4" "go.mongodb.org/mongo-driver/v2/internal/credproviders" "go.mongodb.org/mongo-driver/v2/x/mongo/driver" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/auth/creds" @@ -27,33 +28,36 @@ 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{ + + providers := []credentials.Provider{ + &credproviders.StaticProvider{ Value: credentials.Value{ AccessKeyID: cred.Username, SecretAccessKey: cred.Password, SessionToken: cred.Props["AWS_SESSION_TOKEN"], }, }, - httpClient: httpClient, + } + if cred.AWSCredentialsProvider != nil { + providers = append(providers, cred.AWSCredentialsProvider) + } + + return &MongoDBAWSAuthenticator{ + credentials: creds.NewAWSCredentialProvider(httpClient, providers...).Cred, }, nil } // MongoDBAWSAuthenticator uses AWS-IAM credentials over SASL to authenticate a connection. type MongoDBAWSAuthenticator struct { - credentials *credproviders.StaticProvider - httpClient *http.Client + credentials *credentials.Credentials } // Auth authenticates the connection. func (a *MongoDBAWSAuthenticator) Auth(ctx context.Context, cfg *driver.AuthConfig) error { - providers := creds.NewAWSCredentialProvider(a.httpClient, a.credentials) - adapter := &awsSaslAdapter{ - conversation: &awsConversation{ - credentials: providers.Cred, - }, + awsSasl := &awsSaslAdapter{ + signer: v4signer.NewSigner(a.credentials), } - err := ConductSaslConversation(ctx, cfg, sourceExternal, adapter) + err := ConductSaslConversation(ctx, cfg, sourceExternal, awsSasl) if err != nil { return newAuthError("sasl conversation error", err) } @@ -64,29 +68,3 @@ func (a *MongoDBAWSAuthenticator) Auth(ctx context.Context, cfg *driver.AuthConf func (a *MongoDBAWSAuthenticator) Reauth(_ context.Context, _ *driver.AuthConfig) error { return newAuthError("AWS authentication does not support reauthentication", nil) } - -type awsSaslAdapter struct { - conversation *awsConversation -} - -var _ SaslClient = (*awsSaslAdapter)(nil) - -func (a *awsSaslAdapter) Start() (string, []byte, error) { - step, err := a.conversation.Step(nil) - if err != nil { - return MongoDBAWS, nil, err - } - return MongoDBAWS, step, nil -} - -func (a *awsSaslAdapter) Next(_ context.Context, challenge []byte) ([]byte, error) { - step, err := a.conversation.Step(challenge) - if err != nil { - return nil, err - } - return step, nil -} - -func (a *awsSaslAdapter) Completed() bool { - return a.conversation.Done() -} diff --git a/x/mongo/driver/driver.go b/x/mongo/driver/driver.go index 49ce715caa..fa55dd6a72 100644 --- a/x/mongo/driver/driver.go +++ b/x/mongo/driver/driver.go @@ -17,6 +17,7 @@ import ( "context" "time" + "go.mongodb.org/mongo-driver/v2/internal/aws/credentials" "go.mongodb.org/mongo-driver/v2/internal/csot" "go.mongodb.org/mongo-driver/v2/mongo/address" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" @@ -73,13 +74,19 @@ type Authenticator interface { // Cred is a user's credential. type Cred struct { - Source string - Username string - Password string - PasswordSet bool - Props map[string]string - OIDCMachineCallback OIDCCallback - OIDCHumanCallback OIDCCallback + Source string + Username string + Password string + PasswordSet bool + Props map[string]string + OIDCMachineCallback OIDCCallback + OIDCHumanCallback OIDCCallback + AWSCredentialsProvider AWSCredentialsProvider +} + +// AWSCredentialsProvider is the interface used to retrieve AWS credentials. +type AWSCredentialsProvider interface { + Retrieve(ctx context.Context) (credentials.Value, error) } // Deployment is implemented by types that can select a server from a deployment.