Skip to content

CLOUDP-330561: Moves unauth error check to common errors #4043

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: SA_refactor_feature_branch
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/atlas/atlas.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"strings"

"github.com/AlecAivazis/survey/v2/core"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli/commonerrors"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli/root"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/config"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/telemetry"
Expand All @@ -36,6 +37,10 @@ func execute(rootCmd *cobra.Command) {

To learn more, see our documentation: https://www.mongodb.com/docs/atlas/cli/stable/connect-atlas-cli/`
if cmd, err := rootCmd.ExecuteContextC(ctx); err != nil {
errMsg := commonerrors.GetHumanFriendlyErrorMessage(err)
if errMsg != nil {
fmt.Fprintln(os.Stderr, errMsg)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we did not have this print before, how was cobra printing it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think errors might be printing twice, there is a SilenceErrors property in cobra, double check if you need it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cobra prints to stderr but only prints response error. errMsg only prints custom error (ie. "this action requires authentication ...").

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since errMsg has different content than err printed by cobra, we don't get duplication.

}
if !telemetry.StartedTrackingCommand() {
telemetry.StartTrackingCommand(cmd, os.Args[1:])
}
Expand Down
50 changes: 46 additions & 4 deletions internal/cli/commonerrors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@ package commonerrors

import (
"errors"
"net/http"

"go.mongodb.org/atlas-sdk/v20250312005/admin"
atlasClustersPinned "go.mongodb.org/atlas-sdk/v20240530005/admin"
atlasv2 "go.mongodb.org/atlas-sdk/v20250312005/admin"
atlas "go.mongodb.org/atlas/mongodbatlas"
)

var (
errClusterUnsupported = errors.New("atlas supports this command only for M10+ clusters. You can upgrade your cluster by running the 'atlas cluster upgrade' command")
errOutsideVPN = errors.New("forbidden action outside access allow list, if you are a MongoDB employee double check your VPN connection")
errAsymmetricShardUnsupported = errors.New("trying to run a cluster wide scaling command on an independent shard scaling cluster. Use --autoScalingMode 'independentShardScaling' instead")
ErrUnauthorized = errors.New(`this action requires authentication

To log in using your Atlas username and password, run: atlas auth login
To set credentials using API keys, run: atlas config init`)
)

const (
Expand All @@ -35,7 +42,7 @@ func Check(err error) error {
return nil
}

apiError, ok := admin.AsError(err)
apiError, ok := atlasv2.AsError(err)
if ok {
switch apiError.GetErrorCode() {
case "TENANT_CLUSTER_UPDATE_UNSUPPORTED":
Expand All @@ -49,16 +56,51 @@ func Check(err error) error {
return err
}

// GetHumanFriendErrorMessage returns a human-friendly error message if error is status code 401.
func GetHumanFriendlyErrorMessage(err error) error {
if err == nil {
return nil
}

statusCode := GetErrorStatusCode(err)
if statusCode == http.StatusUnauthorized {
return ErrUnauthorized
}
return nil
}

// GetErrorStatusCode returns the HTTP status code from the error.
// It checks for v2 SDK, the pinned clusters SDK and the old SDK errors.
// If the error is not an API error or is nil, it returns 0.
func GetErrorStatusCode(err error) int {
if err == nil {
return 0
}
apiError, ok := atlasv2.AsError(err)
if ok {
return apiError.GetError()
}
apiPinnedError, ok := atlasClustersPinned.AsError(err)
if ok {
return apiPinnedError.GetError()
}
var atlasErr *atlas.ErrorResponse
if errors.As(err, &atlasErr) {
return atlasErr.HTTPCode
}
return 0
}

func IsAsymmetricShardUnsupported(err error) bool {
apiError, ok := admin.AsError(err)
apiError, ok := atlasv2.AsError(err)
if !ok {
return false
}
return apiError.GetErrorCode() == asymmetricShardUnsupportedErrorCode
}

func IsCannotUseFlexWithClusterApis(err error) bool {
apiError, ok := admin.AsError(err)
apiError, ok := atlasv2.AsError(err)
if !ok {
return false
}
Expand Down
92 changes: 87 additions & 5 deletions internal/cli/commonerrors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@ import (
"errors"
"testing"

"go.mongodb.org/atlas-sdk/v20250312005/admin"
atlasClustersPinned "go.mongodb.org/atlas-sdk/v20240530005/admin"
atlasv2 "go.mongodb.org/atlas-sdk/v20250312005/admin"
atlas "go.mongodb.org/atlas/mongodbatlas"
)

func TestCheck(t *testing.T) {
dummyErr := errors.New("dummy error")

skderr := &admin.GenericOpenAPIError{}
skderr.SetModel(admin.ApiError{ErrorCode: "TENANT_CLUSTER_UPDATE_UNSUPPORTED"})
skderr := &atlasv2.GenericOpenAPIError{}
skderr.SetModel(atlasv2.ApiError{ErrorCode: "TENANT_CLUSTER_UPDATE_UNSUPPORTED"})

asymmetricShardErr := &admin.GenericOpenAPIError{}
asymmetricShardErr.SetModel(admin.ApiError{ErrorCode: asymmetricShardUnsupportedErrorCode})
asymmetricShardErr := &atlasv2.GenericOpenAPIError{}
asymmetricShardErr.SetModel(atlasv2.ApiError{ErrorCode: asymmetricShardUnsupportedErrorCode})

testCases := []struct {
name string
Expand Down Expand Up @@ -67,3 +69,83 @@ func TestCheck(t *testing.T) {
})
}
}

func TestGetHumanFriendlyErrorMessage(t *testing.T) {
dummyErr := errors.New("dummy error")
testErr := &atlasv2.GenericOpenAPIError{}
testErr.SetModel(atlasv2.ApiError{Error: 401})

testCases := []struct {
name string
err error
want error
}{
{
name: "unauthorized error",
err: testErr,
want: ErrUnauthorized,
},
{
name: "arbitrary error",
err: dummyErr,
want: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := GetHumanFriendlyErrorMessage(tc.err); !errors.Is(got, tc.want) {
t.Errorf("GetHumanFriendlyErrorMessage(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}

func TestGetErrorStatusCode(t *testing.T) {
dummyErr := errors.New("dummy error")

unauthorizedCode := 401
forbiddenCode := 403
notFoundCode := 404

atlasErr := &atlas.ErrorResponse{HTTPCode: unauthorizedCode}
atlasv2Err := &atlasv2.GenericOpenAPIError{}
atlasv2Err.SetModel(atlasv2.ApiError{Error: forbiddenCode})
atlasClustersPinnedErr := &atlasClustersPinned.GenericOpenAPIError{}
atlasClustersPinnedErr.SetModel(atlasClustersPinned.ApiError{Error: &notFoundCode})

testCases := []struct {
name string
err error
want int
}{
{
name: "atlas unauthorized error",
err: atlasErr,
want: unauthorizedCode,
},
{
name: "atlasv2 forbidden error",
err: atlasv2Err,
want: forbiddenCode,
},
{
name: "atlasClusterPinned not found error",
err: atlasClustersPinnedErr,
want: notFoundCode,
},
{
name: "arbitrary error",
err: dummyErr,
want: 0,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := GetErrorStatusCode(tc.err); got != tc.want {
t.Errorf("GetErrorStatusCode(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}
3 changes: 1 addition & 2 deletions internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ func (s *Store) httpClient(httpTransport http.RoundTripper) (*http.Client, error

return &http.Client{Transport: tr}, nil
default:
tr := &transport.AuthRequiredRoundTripper{Base: httpTransport}
return &http.Client{Transport: tr}, nil
return &http.Client{Transport: httpTransport}, nil
}
}

Expand Down
23 changes: 0 additions & 23 deletions internal/transport/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@
package transport

import (
"fmt"
"net"
"net/http"
"time"

"github.com/mongodb-forks/digest"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/config"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/oauth"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/validate"
atlasauth "go.mongodb.org/atlas/auth"
)

Expand Down Expand Up @@ -112,24 +110,3 @@ func (tr *tokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {

return tr.base.RoundTrip(req)
}

type AuthRequiredRoundTripper struct {
Base http.RoundTripper
}

Comment on lines -116 to -119
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transport wrapper no longer needed since its primary purpose, to check for unauth, is now done after the command executes

func (tr *AuthRequiredRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := tr.Base.RoundTrip(req)
if resp != nil && resp.StatusCode == http.StatusUnauthorized {
return nil, fmt.Errorf(
`%w

To log in using your Atlas username and password, run: atlas auth login
To set credentials using API keys, run: atlas config init`,
validate.ErrMissingCredentials,
)
}
if err != nil {
return nil, err
}
return resp, nil
}
11 changes: 2 additions & 9 deletions internal/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"slices"
"strings"

"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli/commonerrors"
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/config"
)

Expand Down Expand Up @@ -95,8 +96,6 @@ func ObjectID(s string) error {
return nil
}

var ErrMissingCredentials = errors.New("this action requires authentication")

// Credentials validates public and private API keys have been set.
func Credentials() error {
if t, err := config.Token(); t != nil {
Expand All @@ -106,13 +105,7 @@ func Credentials() error {
return nil
}

return fmt.Errorf(
`%w

To log in using your Atlas username and password, run: atlas auth login
To set credentials using API keys, run: atlas config init`,
ErrMissingCredentials,
)
return commonerrors.ErrUnauthorized
}

var ErrAlreadyAuthenticatedAPIKeys = errors.New("already authenticated with an API key")
Expand Down
2 changes: 1 addition & 1 deletion internal/validate/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func TestCredentials(t *testing.T) {
})
}

func TestNoAPIKeyss(t *testing.T) {
func TestNoAPIKeys(t *testing.T) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by fix

t.Run("no credentials", func(t *testing.T) {
if err := NoAPIKeys(); err != nil {
t.Fatalf("NoAPIKeys() unexpected error %v\n", err)
Expand Down
Loading