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

Conversation

cveticm
Copy link
Collaborator

@cveticm cveticm commented Jul 16, 2025

Proposed changes

Issues

Initial work to move unauth check from builder.go to transport.go (#4034) neglected digest and access token transport clients. This meant that if either of these transport methods received a 401 response, the error would not be wrapped with any additional messaging. For example, when a user's access token expires.

Solution

To fix this, I have moved the unauth check to atlas.go.
The flow now is:

  • a command finishes executing
  • if err
    • check if 401
      • if 401, print custom error message to stdErr

Note that the raw transport error message will be printed to terminal before the command is fully complete. In practice, on terminal this looks much the same as the previous implementation:

Error: https://cloud-dev.mongodb.com/api/atlas/v2/groups/670cd17af33cea212ea1ed61/clusters GET: HTTP 401 Unauthorized (Error code: "") Detail: You are not authorized for this resource. Reason: Unauthorized. Params: []
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

Testing

Unit tests have been added.

As oauth authentication (Access Tokens) requires a browser-based flow, e2e testing could not be created to verify that access tokens are refreshed on expiry.
Manual testing was done instead. Access and Refresh Tokens are successfully refreshed when they expire.
Future work: E2E testing will be created once Service Accounts are implemented (CLOUDP-329808). An e2e test will be created to verify tokens are refreshed on expiry.

Jira ticket: CLOUDP-330561

Checklist

  • I have signed the MongoDB CLA
  • I have added tests that prove my fix is effective or that my feature works
  • I have added any necessary documentation in document requirements section listed in CONTRIBUTING.md (if appropriate)
  • I have addressed the @mongodb/docs-cloud-team comments (if appropriate)
  • I have updated test/README.md (if an e2e test has been added)
  • I have run make fmt and formatted my code

@cveticm cveticm requested a review from a team as a code owner July 16, 2025 10:44
Comment on lines -116 to -119
type AuthRequiredRoundTripper struct {
Base http.RoundTripper
}

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

@@ -49,6 +54,13 @@ func Check(err error) error {
return err
}

func CheckHTTPErrors(err error) error {
if strings.Contains(err.Error(), "401") && strings.Contains(err.Error(), "Unauthorized") {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this is the proper way to check the error code coming from the API.

I think you'll have to use:

admin.AsError(err)

I was first going to recommend using the errorcode: UNAUTHORIZED, but it looks like we have so many variations.

It's better to check the HTTP status code instead (through the error).

Unfortunately I don't fully remember how to get the status code from the SDK, let me know if you can't find it and want to look into it together

Copy link
Collaborator

@andreaangiolillo andreaangiolillo Jul 17, 2025

Choose a reason for hiding this comment

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

Unfortunately I don't fully remember how to get the status code from the SDK, let me know if you can't find it and want to look into it together

AsError returns the ApiError struct which contains the error code as param
https://github.com/mongodb/atlas-sdk-go/blob/main/admin/model_api_error.go#L6

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes i tried this initially, but these unauth errors aren't GenericOpenAPIError types so admin.AsError(err) returns !ok.

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 we need a custom error, perhaps should we introduce error handling on transport and create an ErrUnauthorized? at least a generic http error that has code as a property

Comment on lines 41 to 43
if errMsg != nil {
fmt.Println(errMsg)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

shouldn't we print this to stderr?

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 this prints it twice because cobra prints it

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we are moving the error check here we should remove each individual usage of it and move it here

@cveticm cveticm force-pushed the CLOUDP-330561_commonerror_unauth_check branch from f83fb70 to 6bc5ad3 Compare July 18, 2025 10:27
Copy link
Contributor

Coverage Report 📈

Branch Commit Coverage
SA_refactor_feature_branch 5fbb105 25.7%
CLOUDP-330561_commonerror_unauth_check 1f2a84a 25.8%
Difference .1%

@@ -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

@cveticm
Copy link
Collaborator Author

cveticm commented Jul 18, 2025

e2e failure is plugin e2e tests due to GitHub rate limiting. These tests pass locally and should not be affected by the changes in this PR. Will rerun next week once rate limiting resets.

@@ -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.

Copy link
Collaborator

@fmenezes fmenezes Jul 21, 2025

Choose a reason for hiding this comment

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

running this code this is what I get after my browser login is expired

Error: Get "https://cloud-dev.mongodb.com/api/atlas/v2/groups/686f96db2f08ee49a6da7ca8/clusters?includeCount=true&includeDeletedWithRetainedBackups=false&itemsPerPage=100&pageNum=1": POST https://cloud-dev.mongodb.com/api/private/unauth/account/device/token: 400 (request "INVALID_REFRESH_TOKEN") The refresh token is invalid or expired.
exit status 1

on master I get

Error: session expired

Please note that your session expires periodically. 
If you use Atlas CLI for automation, see https://www.mongodb.com/docs/atlas/cli/stable/atlas-cli-automate/ for best practices.
To login, run: atlas auth login
exit status 1

I prefer master's experience, can we keep it?

tested by running atlas clusters list

Copy link
Collaborator

@fmenezes fmenezes Jul 21, 2025

Choose a reason for hiding this comment

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

I know why that is, the error of not being able to refresh token is a 400 not a 401. I think this is a sort of chicken and egg problem.

I've managed to go back a to a good UX by:

  • updated internal/cli/commonerrors/errors.go
func Check(err error) error {
	if err == nil {
		return nil
	}
+
+	apiErrorCode := "UNKNOWN"
+
+	var sdkError *atlas.ErrorResponse
+	if ok := errors.As(err, &sdkError); ok {
+		apiErrorCode = sdkError.ErrorCode
+	}

	apiError, ok := atlasv2.AsError(err)
	if ok {
-		switch apiError.GetErrorCode() {
+		apiErrorCode = apiError.GetErrorCode()
+	}
+
+	apiPinnedError, ok := atlasClustersPinned.AsError(err)
+	if ok {
+		apiErrorCode = apiPinnedError.GetErrorCode()
+	}
+
+	switch apiErrorCode {
	case "TENANT_CLUSTER_UPDATE_UNSUPPORTED":
		return errClusterUnsupported
	case "GLOBAL_USER_OUTSIDE_SUBNET":
		return errOutsideVPN
	case asymmetricShardUnsupportedErrorCode:
		return errAsymmetricShardUnsupported
+	case "INVALID_REFRESH_TOKEN":
+		return ErrUnauthorized
-		}
	}
+
	return err
}
  • updated cmd/atlas/atlas.go
	if cmd, err := rootCmd.ExecuteContextC(ctx); err != nil {
-		errMsg := commonerrors.GetHumanFriendlyErrorMessage(err)
+		errMsg := commonerrors.Check(err)
		if errMsg != nil {
  • updated internal/cli/root/builder.go
		Example: `  # Display the help menu for the config command:
  atlas config --help
`,
		SilenceUsage:  true,
+		SilenceErrors: true,
		Annotations: map[string]string{
			"toc": "true",
		},

Copy link
Collaborator

@fmenezes fmenezes left a comment

Choose a reason for hiding this comment

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

check my comment on the thread unfortunately the UX here needs some more work, good news is I've managed the find the way I think we should move forward, take a look at my comments and let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants