Skip to content

Commit 5fbb105

Browse files
authored
CLOUDP-330675: Reworks atlas auth login flow (#4038)
1 parent e82d6b3 commit 5fbb105

File tree

6 files changed

+291
-15
lines changed

6 files changed

+291
-15
lines changed

docs/command/atlas-auth-login.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Options
5353
* - --noBrowser
5454
-
5555
- false
56-
- Don't try to open a browser session.
56+
- Don't automatically open a browser session.
5757

5858
Inherited Options
5959
-----------------

internal/cli/auth/login.go

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ import (
2727
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/flag"
2828
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/log"
2929
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/prerun"
30+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/prompt"
3031
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/telemetry"
32+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/usage"
3133
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/validate"
3234
"github.com/pkg/browser"
3335
"github.com/spf13/cobra"
3436
"go.mongodb.org/atlas/auth"
3537
)
3638

37-
//go:generate go tool go.uber.org/mock/mockgen -typed -destination=login_mock_test.go -package=auth . LoginConfig
39+
//go:generate go tool go.uber.org/mock/mockgen -typed -destination=login_mock_test.go -package=auth . LoginConfig,TrackAsker
3840

3941
type SetSaver interface {
4042
Set(string, any)
@@ -49,20 +51,119 @@ type LoginConfig interface {
4951
ProjectID() string
5052
}
5153

54+
type TrackAsker interface {
55+
TrackAsk([]*survey.Question, any, ...survey.AskOpt) error
56+
TrackAskOne(survey.Prompt, any, ...survey.AskOpt) error
57+
}
58+
59+
const (
60+
userAccountAuth = "UserAccount"
61+
apiKeysAuth = "APIKeys"
62+
atlasName = "atlas"
63+
)
64+
5265
var (
5366
ErrProjectIDNotFound = errors.New("project is inaccessible. You either don't have access to this project or the project doesn't exist")
5467
ErrOrgIDNotFound = errors.New("organization is inaccessible. You don't have access to this organization or the organization doesn't exist")
68+
authTypeOptions = []string{userAccountAuth, apiKeysAuth}
69+
authTypeDescription = map[string]string{
70+
userAccountAuth: "(best for getting started)",
71+
apiKeysAuth: "(for existing automations)",
72+
}
5573
)
5674

5775
type LoginOpts struct {
5876
cli.DefaultSetterOpts
5977
cli.RefresherOpts
78+
cli.DigestConfigOpts
6079
AccessToken string
6180
RefreshToken string
6281
IsGov bool
6382
NoBrowser bool
83+
authType string
84+
force bool
6485
SkipConfig bool
6586
config LoginConfig
87+
Asker TrackAsker
88+
}
89+
90+
func (opts *LoginOpts) promptAuthType() error {
91+
if opts.force {
92+
opts.authType = userAccountAuth
93+
return nil
94+
}
95+
authTypePrompt := &survey.Select{
96+
Message: "Select authentication type:",
97+
Options: authTypeOptions,
98+
Default: userAccountAuth,
99+
Description: func(value string, _ int) string {
100+
return authTypeDescription[value]
101+
},
102+
}
103+
return opts.Asker.TrackAskOne(authTypePrompt, &opts.authType)
104+
}
105+
106+
func (opts *LoginOpts) SetUpAccess() {
107+
switch {
108+
case opts.IsGov:
109+
opts.Service = config.CloudGovService
110+
default:
111+
opts.Service = config.CloudService
112+
}
113+
114+
opts.SetUpServiceAndKeys()
115+
}
116+
117+
func (opts *LoginOpts) runAPIKeysLogin(ctx context.Context) error {
118+
_, _ = fmt.Fprintf(opts.OutWriter, `You are configuring a profile for %s.
119+
120+
All values are optional and you can use environment variables (MONGODB_ATLAS_*) instead.
121+
122+
Enter [?] on any option to get help.
123+
124+
`, atlasName)
125+
126+
q := prompt.AccessQuestions()
127+
if err := opts.Asker.TrackAsk(q, opts); err != nil {
128+
return err
129+
}
130+
opts.SetUpAccess()
131+
132+
if err := opts.InitStore(ctx); err != nil {
133+
return err
134+
}
135+
136+
if config.IsAccessSet() {
137+
if err := opts.AskOrg(); err != nil {
138+
return err
139+
}
140+
if err := opts.AskProject(); err != nil {
141+
return err
142+
}
143+
} else {
144+
q := prompt.TenantQuestions()
145+
if err := opts.Asker.TrackAsk(q, opts); err != nil {
146+
return err
147+
}
148+
}
149+
opts.SetUpProject()
150+
opts.SetUpOrg()
151+
152+
if err := opts.Asker.TrackAsk(opts.DefaultQuestions(), opts); err != nil {
153+
return err
154+
}
155+
opts.SetUpOutput()
156+
157+
if err := opts.config.Save(); err != nil {
158+
return err
159+
}
160+
161+
_, _ = fmt.Fprintf(opts.OutWriter, "\nYour profile is now configured.\n")
162+
if config.Name() != config.DefaultProfile {
163+
_, _ = fmt.Fprintf(opts.OutWriter, "To use this profile, you must set the flag [-%s %s] for every command.\n", flag.ProfileShort, config.Name())
164+
}
165+
_, _ = fmt.Fprintf(opts.OutWriter, "You can use [%s config set] to change these settings at a later time.\n", atlasName)
166+
return nil
66167
}
67168

68169
// SyncWithOAuthAccessProfile returns a function that is synchronizing the oauth settings
@@ -102,7 +203,7 @@ func (opts *LoginOpts) SyncWithOAuthAccessProfile(c LoginConfig) func() error {
102203
}
103204
}
104205

105-
func (opts *LoginOpts) LoginRun(ctx context.Context) error {
206+
func (opts *LoginOpts) runUserAccountLogin(ctx context.Context) error {
106207
if err := opts.oauthFlow(ctx); err != nil {
107208
return err
108209
}
@@ -141,6 +242,18 @@ func (opts *LoginOpts) LoginRun(ctx context.Context) error {
141242
return nil
142243
}
143244

245+
func (opts *LoginOpts) LoginRun(ctx context.Context) error {
246+
if err := opts.promptAuthType(); err != nil {
247+
return fmt.Errorf("failed to select authentication type: %w", err)
248+
}
249+
250+
if opts.authType == apiKeysAuth {
251+
return opts.runAPIKeysLogin(ctx)
252+
}
253+
254+
return opts.runUserAccountLogin(ctx)
255+
}
256+
144257
func (opts *LoginOpts) checkProfile(ctx context.Context) error {
145258
if err := opts.InitStore(ctx); err != nil {
146259
return err
@@ -223,6 +336,10 @@ func (opts *LoginOpts) handleBrowser(uri string) {
223336
return
224337
}
225338

339+
if !opts.force {
340+
_, _ = fmt.Fprintf(opts.OutWriter, "\nPress Enter to open the browser to complete authentication...")
341+
_, _ = fmt.Scanln()
342+
}
226343
if errBrowser := browser.OpenURL(uri); errBrowser != nil {
227344
_, _ = log.Warningln("There was an issue opening your browser")
228345
}
@@ -243,7 +360,7 @@ func (opts *LoginOpts) oauthFlow(ctx context.Context) error {
243360
}
244361

245362
accessToken, _, err := opts.PollToken(ctx, code)
246-
if retry, errRetry := shouldRetryAuthenticate(err, newRegenerationPrompt()); errRetry != nil {
363+
if retry, errRetry := opts.shouldRetryAuthenticate(err, newRegenerationPrompt()); errRetry != nil {
247364
return errRetry
248365
} else if retry {
249366
continue
@@ -258,11 +375,11 @@ func (opts *LoginOpts) oauthFlow(ctx context.Context) error {
258375
}
259376
}
260377

261-
func shouldRetryAuthenticate(err error, p survey.Prompt) (retry bool, errSurvey error) {
378+
func (opts *LoginOpts) shouldRetryAuthenticate(err error, p survey.Prompt) (retry bool, errSurvey error) {
262379
if err == nil || !auth.IsTimeoutErr(err) {
263380
return false, nil
264381
}
265-
err = telemetry.TrackAskOne(p, &retry)
382+
err = opts.Asker.TrackAskOne(p, &retry)
266383
return retry, err
267384
}
268385

@@ -290,7 +407,9 @@ func (opts *LoginOpts) LoginPreRun(ctx context.Context) func() error {
290407
}
291408

292409
func LoginBuilder() *cobra.Command {
293-
opts := &LoginOpts{}
410+
opts := &LoginOpts{
411+
Asker: &telemetry.Ask{},
412+
}
294413

295414
cmd := &cobra.Command{
296415
Use: "login",
@@ -316,8 +435,10 @@ func LoginBuilder() *cobra.Command {
316435
}
317436

318437
cmd.Flags().BoolVar(&opts.IsGov, "gov", false, "Log in to Atlas for Government.")
319-
cmd.Flags().BoolVar(&opts.NoBrowser, "noBrowser", false, "Don't try to open a browser session.")
438+
cmd.Flags().BoolVar(&opts.NoBrowser, "noBrowser", false, "Don't automatically open a browser session.")
320439
cmd.Flags().BoolVar(&opts.SkipConfig, "skipConfig", false, "Skip profile configuration.")
321440
_ = cmd.Flags().MarkDeprecated("skipConfig", "if you configured a profile, the command skips the config step by default.")
441+
cmd.Flags().BoolVar(&opts.force, flag.Force, false, usage.Force)
442+
_ = cmd.Flags().MarkHidden(flag.Force)
322443
return cmd
323444
}

internal/cli/auth/login_mock_test.go

Lines changed: 113 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)