Repository navigation
Reject an empty host at the login host prompt #7195
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
base: main
Are you sure you want to change the base?
Changes from all commits
1a8288d
765a41f
01186a1
3e96944
ecd576f
69e2b68
f80c59f
2c68da8
50e1992
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| * `databricks auth login` and `databricks auth token` now reject an empty host instead of falling back to `DATABRICKS_HOST`. An empty host after "Enter a host" in the login profile picker no longer starts discovery login. ([#7195](https://github.com/databricks/cli/pull/7195)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/databricks/cli/libs/auth" | ||
| "github.com/databricks/cli/libs/cmdio" | ||
|
|
@@ -51,9 +52,16 @@ func promptForHost(ctx context.Context) (string, error) { | |
| // long "label + value" line that wraps causes each keystroke to leave a | ||
| // stale render on screen instead of overwriting the previous one. | ||
| cmdio.LogString(ctx, "Example: https://<databricks-instance>.cloud.databricks.com") | ||
| return cmdio.RunPrompt(ctx, cmdio.PromptOptions{ | ||
| result, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ | ||
| Label: "Databricks host", | ||
| Validate: func(input string) error { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nice to have] I think running the same normalization in return normalizeLoginHost(&auth.AuthArguments{Host: strings.TrimSpace(input)})Checking for a missing hostname (like |
||
| if strings.TrimSpace(input) == "" { | ||
| return errors.New("host is required") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two small nits:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| return nil | ||
| }, | ||
| }) | ||
| return strings.TrimSpace(result), err | ||
| } | ||
|
|
||
| func promptForAccountID(ctx context.Context) (string, error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -258,11 +258,6 @@ a new profile is created. | |
| if err != nil { | ||
| return err | ||
| } | ||
| // Fail here rather than prompting for the host again after | ||
| // the profile name. | ||
| if useDeviceCode && host == "" { | ||
| return errDeviceCodeNeedsHost | ||
| } | ||
| authArguments.Host = host | ||
| case profilePickerCreateNew: | ||
| // Fall through to the profile name prompt below. | ||
|
|
@@ -275,13 +270,8 @@ a new profile is created. | |
| // only populated by setHostAndAccountId further down, which runs after | ||
| // the profile-name prompt. | ||
| if skipWorkspace && profileName == "" && authArguments.Host != "" && authArguments.AccountID == "" { | ||
| params := auth.ExtractHostQueryParams(authArguments.Host) | ||
| authArguments.Host = params.Host | ||
| if authArguments.AccountID == "" { | ||
| authArguments.AccountID = params.AccountID | ||
| } | ||
| if authArguments.WorkspaceID == "" { | ||
| authArguments.WorkspaceID = params.WorkspaceID | ||
| if err := normalizeLoginHost(authArguments); err != nil { | ||
| return err | ||
| } | ||
| runHostDiscovery(ctx, authArguments) | ||
| } | ||
|
|
@@ -344,15 +334,8 @@ a new profile is created. | |
| // Check the host once it is resolved, before setAccountID prompts for | ||
| // an account ID for account and unified hosts. A workspace without | ||
| // device code login enabled is rejected later by DeviceCodeChallenge. | ||
| if useDeviceCode { | ||
| // The host prompt accepts an empty answer. Without this check, | ||
| // login would resolve the host from DATABRICKS_HOST instead. | ||
| if authArguments.Host == "" { | ||
| return errDeviceCodeNeedsHost | ||
| } | ||
| if !isDeviceCodeSupportedForHost(authArguments.Host, authArguments.DiscoveryURL) { | ||
| return errDeviceCodeWorkspaceOnly | ||
| } | ||
| if useDeviceCode && !isDeviceCodeSupportedForHost(authArguments.Host, authArguments.DiscoveryURL) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This drops the With an empty host, Repro: I think we should keep an empty check after
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added a check inside setHost |
||
| return errDeviceCodeWorkspaceOnly | ||
| } | ||
| err = setAccountID(ctx, existingProfile, authArguments) | ||
| if err != nil { | ||
|
|
@@ -542,6 +525,32 @@ func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile, | |
| return setAccountID(ctx, existingProfile, authArguments) | ||
| } | ||
|
|
||
| var errInvalidHost = errors.New("invalid host") | ||
|
|
||
| // normalizeLoginHost strips the trailing slash and the query params from the | ||
| // host and moves ?o= and ?a= into WorkspaceID and AccountID unless they are set. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] The comment says it moves |
||
| // It fails if nothing is left of the host, e.g. for "/" or "?o=123", because | ||
| // with an empty host login would use discovery or DATABRICKS_HOST instead. | ||
| func normalizeLoginHost(authArguments *auth.AuthArguments) error { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nice to have] Only the prompt trims spaces. I think trimming once at the top of |
||
| rawHost := authArguments.Host | ||
| authArguments.Host = strings.TrimSuffix(authArguments.Host, "/") | ||
|
|
||
| // Extract query parameters from the host URL (?o=workspace_id, ?a=account_id). | ||
| // URL params from explicit --host override stale profile values. | ||
| params := auth.ExtractHostQueryParams(authArguments.Host) | ||
| authArguments.Host = params.Host | ||
| if authArguments.Host == "" { | ||
| return fmt.Errorf("%w %q", errInvalidHost, rawHost) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nice to have] |
||
| } | ||
| if authArguments.WorkspaceID == "" { | ||
| authArguments.WorkspaceID = params.WorkspaceID | ||
| } | ||
| if authArguments.AccountID == "" { | ||
| authArguments.AccountID = params.AccountID | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // Sets the host in the persistentAuth object based on the provided arguments and flags. | ||
| // Follows the following precedence: | ||
| // 1. [HOST] (first positional argument) or --host flag. Error if both are specified. | ||
|
|
@@ -573,17 +582,8 @@ func setHost(ctx context.Context, existingProfile *profile.Profile, authArgument | |
| } | ||
| } | ||
|
|
||
| authArguments.Host = strings.TrimSuffix(authArguments.Host, "/") | ||
|
|
||
| // Extract query parameters from the host URL (?o=workspace_id, ?a=account_id). | ||
| // URL params from explicit --host override stale profile values. | ||
| params := auth.ExtractHostQueryParams(authArguments.Host) | ||
| authArguments.Host = params.Host | ||
| if authArguments.WorkspaceID == "" { | ||
| authArguments.WorkspaceID = params.WorkspaceID | ||
| } | ||
| if authArguments.AccountID == "" { | ||
| authArguments.AccountID = params.AccountID | ||
| if err := normalizeLoginHost(authArguments); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Inherit workspace_id from the existing profile AFTER URL param extraction. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -219,8 +219,10 @@ func startDeviceCodeLogin(t *testing.T, config string, args ...string) *termtest | |
| t.Setenv(deviceCodeEnvVar, "true") | ||
|
|
||
| return termtest.Run(t, func(ctx context.Context) (struct{}, error) { | ||
| cmd := newLoginCommand(&auth.AuthArguments{}) | ||
| authArguments := &auth.AuthArguments{} | ||
| cmd := newLoginCommand(authArguments) | ||
| cmd.Flags().String("profile", "", "") | ||
| cmd.Flags().StringVar(&authArguments.Host, "host", "", "") | ||
| cmd.SetContext(ctx) | ||
| cmd.SetArgs(append([]string{"--use-device-code"}, args...)) | ||
| cmd.SetOut(io.Discard) | ||
|
|
@@ -233,8 +235,10 @@ func TestLoginDeviceCodeNewProfilePromptsForHost(t *testing.T) { | |
| tm := startDeviceCodeLogin(t, "", "--profile", "newprof") | ||
| tm.WaitFor("Databricks host") | ||
| tm.Type(termtest.KeyEnter) | ||
| tm.WaitFor(">> host is required") | ||
| tm.Type(termtest.KeyCtrlC) | ||
| _, err := tm.Result() | ||
| assert.ErrorIs(t, err, errDeviceCodeNeedsHost) | ||
| assert.EqualError(t, err, "^C") | ||
|
maksim-zuev-db marked this conversation as resolved.
|
||
| } | ||
|
|
||
| func TestLoginDeviceCodePickerEmptyHost(t *testing.T) { | ||
|
|
@@ -249,8 +253,20 @@ func TestLoginDeviceCodePickerEmptyHost(t *testing.T) { | |
| tm.Type(termtest.KeyDown + termtest.KeyEnter) | ||
| tm.WaitFor("Databricks host") | ||
| tm.Type(termtest.KeyEnter) | ||
| tm.WaitFor(">> host is required") | ||
| tm.Type(termtest.KeyCtrlC) | ||
| _, err := tm.Result() | ||
| assert.ErrorIs(t, err, errDeviceCodeNeedsHost) | ||
| assert.EqualError(t, err, "^C") | ||
| } | ||
|
|
||
| func TestLoginDeviceCodeHostEmptyAfterNormalization(t *testing.T) { | ||
| for _, host := range []string{"/", "?o=123"} { | ||
| t.Run(host, func(t *testing.T) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] With Small thing: |
||
| tm := startDeviceCodeLogin(t, "", "--profile", "newprof", "--host", host) | ||
| _, err := tm.Result() | ||
| assert.ErrorIs(t, err, errInvalidHost) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestMatchDeviceCodeProfiles(t *testing.T) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Example: https://<databricks-instance>.cloud.databricks.com | ||
| ✗ Databricks host: █ | ||
| >> host is required |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Example: https://<databricks-instance>.cloud.databricks.com | ||
| ✗ Databricks host: █ | ||
| >> host is required |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| ✗ Workspace host: █ | ||
| Workspace host: █ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| ✗ Workspace host: █ | ||
| >> host is required |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ✔ Workspace host: █ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,9 @@ type PromptOptions struct { | |
| // Validate, when set, is called on every keystroke. While it returns a | ||
| // non-nil error the leading glyph flips from "✔" to "✗" and Enter is | ||
| // inert; pressing Enter while invalid surfaces the returned error | ||
| // below the prompt until the next edit. | ||
| // below the prompt until the next edit. Until the first edit or Enter, | ||
| // an invalid input shows a blank glyph instead, so an empty prompt does | ||
| // not open as an error. | ||
| Validate func(input string) error | ||
| } | ||
|
|
||
|
|
@@ -73,6 +75,9 @@ type promptModel struct { | |
| deleted bool | ||
| submitted bool | ||
|
|
||
| // edited is set by the first change to runes. | ||
| edited bool | ||
|
|
||
| // submitErr is the error from the last failed Enter attempt. Rendered | ||
| // below the prompt and cleared on the next edit. | ||
| submitErr error | ||
|
|
@@ -92,12 +97,16 @@ func (m *promptModel) value() string { | |
| } | ||
|
|
||
| // glyph returns the leading status indicator: bold-green ✔ when valid, | ||
| // bold-red ✗ when validate rejects the buffer. | ||
| // bold-red ✗ when validate rejects the buffer after an edit or Enter, and a | ||
| // blank before that. | ||
| func (m *promptModel) glyph() string { | ||
| if m.validate != nil && m.validate(m.value()) != nil { | ||
| return ansiBold + ansiRed + glyphInvalid + ansiReset | ||
| if m.validate == nil || m.validate(m.value()) == nil { | ||
| return ansiBold + ansiGreen + glyphValid + ansiReset | ||
| } | ||
| if !m.edited && m.submitErr == nil { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coming back on my earlier comment about the red ✗ on the first frame, my bad. This changes every prompt that uses |
||
| return " " | ||
| } | ||
| return ansiBold + ansiGreen + glyphValid + ansiReset | ||
| return ansiBold + ansiRed + glyphInvalid + ansiReset | ||
| } | ||
|
|
||
| func (m *promptModel) Init() tea.Cmd { return nil } | ||
|
|
@@ -154,6 +163,7 @@ func (m *promptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |
| } | ||
| m.runes = append(m.runes[:m.cursor-1], m.runes[m.cursor:]...) | ||
| m.cursor-- | ||
| m.edited = true | ||
| m.submitErr = nil | ||
|
|
||
| case tea.KeyRunes, tea.KeySpace: | ||
|
|
@@ -172,6 +182,7 @@ func (m *promptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |
| m.runes = append(m.runes[:m.cursor], typed...) | ||
| m.runes = append(m.runes, tail...) | ||
| m.cursor += len(typed) | ||
| m.edited = true | ||
| m.submitErr = nil | ||
|
|
||
| default: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also changes the browser flow. On main, picking "Enter a host" in the profile picker and pressing Enter on an empty prompt left
Host == "", soshouldUseDiscoverykicked in and login went through login.databricks.com. Now the prompt loops on "host is required" and Ctrl+C is the only way out (no way back to "Create a new profile").Was that intended? If so, I think the PR description and changelog should mention it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I intended this change. I suppose discovery fallback is unexpected here, as it is a part of 'Enter host manually' flow