From 57f8b471b0220beecc349c5668282a31473f1055 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 27 Oct 2026 13:40:46 +0000 Subject: [PATCH 01/29] Reject an empty host at the login host prompt Pressing Enter at the "Databricks host" prompt left the host empty, so login fell back to DATABRICKS_HOST or failed later with an unrelated error. The prompt now refuses an empty answer, which replaces the device code specific empty host checks. Co-authored-by: Isaac --- cmd/auth/auth.go | 6 ++++++ cmd/auth/auth_test.go | 12 ++++++++++++ cmd/auth/login.go | 16 ++-------------- cmd/auth/login_device_code_test.go | 8 ++++++-- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 6bbee3730b3..a8276b11fb4 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -53,6 +53,12 @@ func promptForHost(ctx context.Context) (string, error) { cmdio.LogString(ctx, "Example: https://.cloud.databricks.com") return cmdio.RunPrompt(ctx, cmdio.PromptOptions{ Label: "Databricks host", + Validate: func(input string) error { + if input == "" { + return errors.New("host is required") + } + return nil + }, }) } diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go index 9dbf04fc664..4bd8bb80f46 100644 --- a/cmd/auth/auth_test.go +++ b/cmd/auth/auth_test.go @@ -7,6 +7,7 @@ import ( "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/cli/libs/cmdio/cmdiotest/termtest" "github.com/databricks/cli/libs/databrickscfg/profile" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -141,3 +142,14 @@ func TestProfileHostCompatibleViaCobra(t *testing.T) { assert.NotContains(t, err.Error(), "conflicts with --host") } } + +func TestPromptForHostRejectsEmptyHost(t *testing.T) { + tm := termtest.Run(t, promptForHost) + tm.WaitFor("Databricks host") + tm.Type(termtest.KeyEnter) + tm.WaitFor(">> host is required") + tm.Type("https://workspace.test" + termtest.KeyEnter) + host, err := tm.Result() + require.NoError(t, err) + assert.Equal(t, "https://workspace.test", host) +} diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 7552c7defcc..dcd59c696fa 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -315,11 +315,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: // Reject the names of existing profiles, including those the @@ -417,15 +412,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 deviceCodeLogin. - 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) { + return errDeviceCodeWorkspaceOnly } err = setAccountID(ctx, existingProfile, authArguments) if err != nil { diff --git a/cmd/auth/login_device_code_test.go b/cmd/auth/login_device_code_test.go index 1a3a7c2f713..dc37385bef7 100644 --- a/cmd/auth/login_device_code_test.go +++ b/cmd/auth/login_device_code_test.go @@ -307,8 +307,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") } func TestLoginDeviceCodePickerEmptyHost(t *testing.T) { @@ -323,8 +325,10 @@ 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 TestLoginDeviceCodeCreateNewProfileRejectsHiddenProfileName(t *testing.T) { From 81bdb8f07117173baaa0d6dc05eee62c7d52ece1 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 27 Oct 2026 13:51:02 +0000 Subject: [PATCH 02/29] Add a changelog fragment for the empty host prompt fix Co-authored-by: Isaac --- .nextchanges/cli/login-reject-empty-host.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nextchanges/cli/login-reject-empty-host.md diff --git a/.nextchanges/cli/login-reject-empty-host.md b/.nextchanges/cli/login-reject-empty-host.md new file mode 100644 index 00000000000..e7f368760d4 --- /dev/null +++ b/.nextchanges/cli/login-reject-empty-host.md @@ -0,0 +1 @@ +* `databricks auth login` now rejects an empty host at the host prompt instead of falling back to `DATABRICKS_HOST` or failing later. ([#7195](https://github.com/databricks/cli/pull/7195)) From 9df79c6e17b7c90b50a0b45bdbc7b08fbc54aeb7 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 27 Oct 2026 13:58:31 +0000 Subject: [PATCH 03/29] Trim spaces around the host entered at the login host prompt A host pasted with surrounding spaces failed later with an unrelated URL error, and a host of only spaces got past the empty host check. Co-authored-by: Isaac --- cmd/auth/auth.go | 6 ++++-- cmd/auth/auth_test.go | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index a8276b11fb4..94f62d5edfe 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/cmdio" @@ -51,15 +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://.cloud.databricks.com") - return cmdio.RunPrompt(ctx, cmdio.PromptOptions{ + result, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ Label: "Databricks host", Validate: func(input string) error { - if input == "" { + if strings.TrimSpace(input) == "" { return errors.New("host is required") } return nil }, }) + return strings.TrimSpace(result), err } func promptForAccountID(ctx context.Context) (string, error) { diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go index 4bd8bb80f46..714d2ac4073 100644 --- a/cmd/auth/auth_test.go +++ b/cmd/auth/auth_test.go @@ -148,7 +148,9 @@ func TestPromptForHostRejectsEmptyHost(t *testing.T) { tm.WaitFor("Databricks host") tm.Type(termtest.KeyEnter) tm.WaitFor(">> host is required") - tm.Type("https://workspace.test" + termtest.KeyEnter) + tm.Type(" " + termtest.KeyEnter) + tm.WaitFor(">> host is required") + tm.Type("https://workspace.test " + termtest.KeyEnter) host, err := tm.Result() require.NoError(t, err) assert.Equal(t, "https://workspace.test", host) From 5a1041f409dc4a22319866f9131c6c76353fb798 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 27 Oct 2026 15:45:16 +0000 Subject: [PATCH 04/29] Reject a host that is empty after normalization setHost strips a trailing "/" and the query params, so "/" or "?o=123" became an empty host after the prompt check. Login and auth token then resolved the host from DATABRICKS_HOST or failed later. Checking in setHost covers the prompt, --host, the positional argument, and profile hosts, and keeps the hint from the removed errDeviceCodeNeedsHost. Co-authored-by: Isaac --- cmd/auth/login.go | 7 +++++++ cmd/auth/login_device_code_test.go | 10 ++++++++++ cmd/auth/login_test.go | 15 +++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index dcd59c696fa..0e02ec3dca9 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -617,6 +617,8 @@ func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile, return setAccountID(ctx, existingProfile, authArguments) } +var errHostRequired = errors.New("host is required. Pass it as an argument or with --host") + // 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. @@ -654,6 +656,11 @@ func setHost(ctx context.Context, existingProfile *profile.Profile, authArgument // URL params from explicit --host override stale profile values. params := auth.ExtractHostQueryParams(authArguments.Host) authArguments.Host = params.Host + // Checked after normalization because a host like "/" or "?o=123" becomes + // empty. Without a host, login would resolve one from DATABRICKS_HOST. + if authArguments.Host == "" { + return errHostRequired + } if authArguments.WorkspaceID == "" { authArguments.WorkspaceID = params.WorkspaceID } diff --git a/cmd/auth/login_device_code_test.go b/cmd/auth/login_device_code_test.go index dc37385bef7..e86985fc90c 100644 --- a/cmd/auth/login_device_code_test.go +++ b/cmd/auth/login_device_code_test.go @@ -354,6 +354,16 @@ account_id = acct-123 assert.EqualError(t, err, "^C") } +func TestLoginDeviceCodeHostEmptyAfterNormalization(t *testing.T) { + for _, host := range []string{"/", "?o=123"} { + t.Run(host, func(t *testing.T) { + tm := startDeviceCodeLogin(t, "", "--profile", "newprof", "--host", host) + _, err := tm.Result() + assert.ErrorIs(t, err, errHostRequired) + }) + } +} + func TestMatchDeviceCodeProfiles(t *testing.T) { tests := []struct { name string diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index 01f58c56cff..1d6ea3f56dd 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -206,6 +206,21 @@ func TestSetHost(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "https://www.host2.test", authArguments.Host) + // Test host that is empty after normalization from flag + authArguments.Host = "?o=123" + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) + assert.ErrorIs(t, err, errHostRequired) + + // Test host that is empty after normalization from argument + authArguments.Host = "" + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{"/"}) + assert.ErrorIs(t, err, errHostRequired) + + // Test host that is empty after normalization from profile + authArguments.Host = "" + err = setHostAndAccountId(ctx, &profile.Profile{Host: "/"}, &authArguments, []string{}) + assert.ErrorIs(t, err, errHostRequired) + // Test host is not set. Should prompt. authArguments.Host = "" err = setHostAndAccountId(ctx, nil, &authArguments, []string{}) From 00f08ce669a264a3ebd001e807664c2a435d7328 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 27 Oct 2026 15:45:17 +0000 Subject: [PATCH 05/29] Check the screen in the empty host prompt test WaitFor matched the error printed for the first empty answer, so the test passed even if a whitespace-only host was accepted. Golden steps compare the rendered screen after each Enter. Co-authored-by: Isaac --- cmd/auth/auth_test.go | 4 ++-- .../TestPromptForHostRejectsEmptyHost/01-empty.golden | 3 +++ .../TestPromptForHostRejectsEmptyHost/02-spaces.golden | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 cmd/auth/testdata/TestPromptForHostRejectsEmptyHost/01-empty.golden create mode 100644 cmd/auth/testdata/TestPromptForHostRejectsEmptyHost/02-spaces.golden diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go index 714d2ac4073..bfef5a34c4f 100644 --- a/cmd/auth/auth_test.go +++ b/cmd/auth/auth_test.go @@ -147,9 +147,9 @@ func TestPromptForHostRejectsEmptyHost(t *testing.T) { tm := termtest.Run(t, promptForHost) tm.WaitFor("Databricks host") tm.Type(termtest.KeyEnter) - tm.WaitFor(">> host is required") + tm.Golden("01-empty") tm.Type(" " + termtest.KeyEnter) - tm.WaitFor(">> host is required") + tm.Golden("02-spaces") tm.Type("https://workspace.test " + termtest.KeyEnter) host, err := tm.Result() require.NoError(t, err) diff --git a/cmd/auth/testdata/TestPromptForHostRejectsEmptyHost/01-empty.golden b/cmd/auth/testdata/TestPromptForHostRejectsEmptyHost/01-empty.golden new file mode 100644 index 00000000000..01918873e19 --- /dev/null +++ b/cmd/auth/testdata/TestPromptForHostRejectsEmptyHost/01-empty.golden @@ -0,0 +1,3 @@ +Example: https://.cloud.databricks.com +✗ Databricks host: █ +>> host is required diff --git a/cmd/auth/testdata/TestPromptForHostRejectsEmptyHost/02-spaces.golden b/cmd/auth/testdata/TestPromptForHostRejectsEmptyHost/02-spaces.golden new file mode 100644 index 00000000000..7c5f1e4da33 --- /dev/null +++ b/cmd/auth/testdata/TestPromptForHostRejectsEmptyHost/02-spaces.golden @@ -0,0 +1,3 @@ +Example: https://.cloud.databricks.com +✗ Databricks host: █ +>> host is required From 63017a440b8c5837c851ef7ce684e20b83461b28 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 27 Oct 2026 16:40:18 +0000 Subject: [PATCH 06/29] Show a blank prompt glyph until invalid input is edited The prompt glyph ran Validate on every render, so a prompt that rejects empty input, such as the login host prompt, opened with a red cross before anything was typed. The glyph is now blank until the first edit or Enter, then shows a cross while the input is invalid and a checkmark once it is valid. Co-authored-by: Isaac --- .../prompt_validate_baseline_test.go | 44 +++++++++++++++++++ .../01-empty.golden | 2 +- .../01-enter.golden | 2 + .../01-initial.golden | 1 + libs/cmdio/prompt.go | 21 ++++++--- 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden create mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden diff --git a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go index 5f703a5865d..68d507a57fb 100644 --- a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go +++ b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go @@ -45,3 +45,47 @@ func TestPromptBaseline_Validate(t *testing.T) { require.NoError(t, err, "raw output: %q", tm.Raw()) assert.Equal(t, "https://example.com", v, "snapshot:\n%s", tm.Snapshot()) } + +// TestPromptBaseline_ValidateEnterBeforeEdit pins that Enter on an untouched +// prompt flips the glyph to "✗" along with the error line. +func TestPromptBaseline_ValidateEnterBeforeEdit(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace host", + Validate: func(s string) error { + if s == "" { + return errors.New("host is required") + } + return nil + }, + }) + tm.WaitFor("Workspace host") + tm.Type(termtest.KeyEnter) + tm.Golden("01-enter") + + tm.Type(termtest.KeyCtrlC) + _, err := tm.Result() + assert.Error(t, err) +} + +// TestPromptBaseline_ValidateValidBeforeEdit pins that an untouched prompt +// whose input already passes Validate shows "✔". +func TestPromptBaseline_ValidateValidBeforeEdit(t *testing.T) { + t.Parallel() + tm := termtest.NewPrompt(t, cmdio.PromptOptions{ + Label: "Workspace host", + Validate: func(s string) error { + if strings.Contains(s, " ") { + return errors.New("must not contain spaces") + } + return nil + }, + }) + tm.WaitFor("Workspace host") + tm.Golden("01-initial") + + tm.Type(termtest.KeyEnter) + v, err := tm.Result() + require.NoError(t, err, "raw output: %q", tm.Raw()) + assert.Empty(t, v) +} diff --git a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden index aef99ba9f27..6da352062e8 100644 --- a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden +++ b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden @@ -1 +1 @@ -✗ Workspace host: █ + Workspace host: █ diff --git a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden new file mode 100644 index 00000000000..a001d0c4bfa --- /dev/null +++ b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden @@ -0,0 +1,2 @@ +✗ Workspace host: █ +>> host is required diff --git a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden new file mode 100644 index 00000000000..c2487520307 --- /dev/null +++ b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden @@ -0,0 +1 @@ +✔ Workspace host: █ diff --git a/libs/cmdio/prompt.go b/libs/cmdio/prompt.go index 8c8ca1a3a39..52870183e09 100644 --- a/libs/cmdio/prompt.go +++ b/libs/cmdio/prompt.go @@ -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 { + 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: From f6d384f742766c65276261f860b242b0c70267b9 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 27 Oct 2026 16:40:21 +0000 Subject: [PATCH 07/29] Update the changelog fragment for the review fixes Mention auth token, and that an empty host after "Enter a host" no longer starts discovery login. Co-authored-by: Isaac --- .nextchanges/cli/login-reject-empty-host.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nextchanges/cli/login-reject-empty-host.md b/.nextchanges/cli/login-reject-empty-host.md index e7f368760d4..ddb03d804a3 100644 --- a/.nextchanges/cli/login-reject-empty-host.md +++ b/.nextchanges/cli/login-reject-empty-host.md @@ -1 +1 @@ -* `databricks auth login` now rejects an empty host at the host prompt instead of falling back to `DATABRICKS_HOST` or failing later. ([#7195](https://github.com/databricks/cli/pull/7195)) +* `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)) From 954e9ac9ebc9e96ae549b3fcc39b3fe51710dc91 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 27 Oct 2026 17:45:52 +0000 Subject: [PATCH 08/29] Check for an empty host in one place, including --skip-workspace With --skip-workspace, login stripped the query params from the host before setHost ran, so a host like "?a=123" became empty and login went to discovery instead of failing. Both places now share normalizeHost, which does the empty host check. The error is now `invalid host ""`. The previous hint to pass the host as an argument or with --host did not help, because the error only happens when a host was given. Co-authored-by: Isaac --- cmd/auth/login.go | 53 ++++++++++++++++-------------- cmd/auth/login_device_code_test.go | 2 +- cmd/auth/login_test.go | 29 +++++++++++----- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 0e02ec3dca9..75304309714 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -334,13 +334,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 := normalizeHost(authArguments); err != nil { + return err } runHostDiscovery(ctx, authArguments) } @@ -617,7 +612,31 @@ func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile, return setAccountID(ctx, existingProfile, authArguments) } -var errHostRequired = errors.New("host is required. Pass it as an argument or with --host") +var errInvalidHost = errors.New("invalid host") + +// normalizeHost strips the trailing slash and the query params from the host +// and moves ?o= and ?a= into WorkspaceID and AccountID unless they are set. +// 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 normalizeHost(authArguments *auth.AuthArguments) error { + 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) + } + 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: @@ -650,22 +669,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 - // Checked after normalization because a host like "/" or "?o=123" becomes - // empty. Without a host, login would resolve one from DATABRICKS_HOST. - if authArguments.Host == "" { - return errHostRequired - } - if authArguments.WorkspaceID == "" { - authArguments.WorkspaceID = params.WorkspaceID - } - if authArguments.AccountID == "" { - authArguments.AccountID = params.AccountID + if err := normalizeHost(authArguments); err != nil { + return err } // Inherit workspace_id from the existing profile AFTER URL param extraction. diff --git a/cmd/auth/login_device_code_test.go b/cmd/auth/login_device_code_test.go index e86985fc90c..334ea24a4c3 100644 --- a/cmd/auth/login_device_code_test.go +++ b/cmd/auth/login_device_code_test.go @@ -359,7 +359,7 @@ func TestLoginDeviceCodeHostEmptyAfterNormalization(t *testing.T) { t.Run(host, func(t *testing.T) { tm := startDeviceCodeLogin(t, "", "--profile", "newprof", "--host", host) _, err := tm.Result() - assert.ErrorIs(t, err, errHostRequired) + assert.ErrorIs(t, err, errInvalidHost) }) } } diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index 1d6ea3f56dd..ae2d20ffe09 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -209,17 +209,17 @@ func TestSetHost(t *testing.T) { // Test host that is empty after normalization from flag authArguments.Host = "?o=123" err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) - assert.ErrorIs(t, err, errHostRequired) + assert.ErrorIs(t, err, errInvalidHost) // Test host that is empty after normalization from argument authArguments.Host = "" err = setHostAndAccountId(ctx, profile1, &authArguments, []string{"/"}) - assert.ErrorIs(t, err, errHostRequired) + assert.ErrorIs(t, err, errInvalidHost) // Test host that is empty after normalization from profile authArguments.Host = "" err = setHostAndAccountId(ctx, &profile.Profile{Host: "/"}, &authArguments, []string{}) - assert.ErrorIs(t, err, errHostRequired) + assert.ErrorIs(t, err, errInvalidHost) // Test host is not set. Should prompt. authArguments.Host = "" @@ -806,11 +806,7 @@ func TestSkipWorkspaceProfileNameUsesDiscoveredAccountID(t *testing.T) { args := &auth.AuthArguments{Host: server.URL} // Mirrors the pre-naming block in newLoginCommand's RunE for --skip-workspace. - params := auth.ExtractHostQueryParams(args.Host) - args.Host = params.Host - if args.AccountID == "" { - args.AccountID = params.AccountID - } + require.NoError(t, normalizeHost(args)) runHostDiscovery(ctx, args) assert.Equal(t, "abc-123", args.AccountID) @@ -1467,6 +1463,23 @@ func TestLoginRejectsPositionalArgWithHostFlag(t *testing.T) { assert.ErrorContains(t, err, `argument "myprofile" cannot be combined with --host or --profile`) } +func TestLoginSkipWorkspaceHostEmptyAfterNormalization(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + t.Setenv("DATABRICKS_CONFIG_FILE", writeTempConfig(t, "")) + t.Setenv("DATABRICKS_AUTH_STORAGE", "plaintext") + + ctx := cmdio.MockDiscard(t.Context()) + authArgs := &auth.AuthArguments{Host: "?a=123"} + cmd := newLoginCommand(authArgs) + cmd.Flags().String("profile", "", "") + cmd.SetContext(ctx) + cmd.SetArgs([]string{"--skip-workspace"}) + err := cmd.Execute() + assert.EqualError(t, err, `invalid host "?a=123"`) +} + func TestLoginRejectsPositionalArgWithProfileFlag(t *testing.T) { ctx := cmdio.MockDiscard(t.Context()) authArgs := &auth.AuthArguments{} From 5604deb6cbe92dc1333ed69fd4cb00d66ef1aab0 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 27 Oct 2026 17:49:27 +0000 Subject: [PATCH 09/29] Rename normalizeHost to normalizeLoginHost cmd/configure and libs/databrickscfg already have a normalizeHost. Co-authored-by: Isaac --- cmd/auth/login.go | 10 +++++----- cmd/auth/login_test.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 75304309714..fd88b325209 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -334,7 +334,7 @@ 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 == "" { - if err := normalizeHost(authArguments); err != nil { + if err := normalizeLoginHost(authArguments); err != nil { return err } runHostDiscovery(ctx, authArguments) @@ -614,11 +614,11 @@ func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile, var errInvalidHost = errors.New("invalid host") -// normalizeHost strips the trailing slash and the query params from the host -// and moves ?o= and ?a= into WorkspaceID and AccountID unless they are set. +// 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. // 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 normalizeHost(authArguments *auth.AuthArguments) error { +func normalizeLoginHost(authArguments *auth.AuthArguments) error { rawHost := authArguments.Host authArguments.Host = strings.TrimSuffix(authArguments.Host, "/") @@ -669,7 +669,7 @@ func setHost(ctx context.Context, existingProfile *profile.Profile, authArgument } } - if err := normalizeHost(authArguments); err != nil { + if err := normalizeLoginHost(authArguments); err != nil { return err } diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index ae2d20ffe09..c4b8132ac38 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -806,7 +806,7 @@ func TestSkipWorkspaceProfileNameUsesDiscoveredAccountID(t *testing.T) { args := &auth.AuthArguments{Host: server.URL} // Mirrors the pre-naming block in newLoginCommand's RunE for --skip-workspace. - require.NoError(t, normalizeHost(args)) + require.NoError(t, normalizeLoginHost(args)) runHostDiscovery(ctx, args) assert.Equal(t, "abc-123", args.AccountID) From fe365cbe4b92a08ff7c2bbeed898855f035042f8 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Fri, 30 Oct 2026 16:55:24 +0000 Subject: [PATCH 10/29] Revert the blank prompt glyph The glyph change affects every prompt that uses Validate, including the databricks configure host prompt, so it does not belong in this PR. The baseline tests added with it pass with the old glyph and stay. Co-authored-by: Isaac --- .../prompt_validate_baseline_test.go | 2 +- .../01-empty.golden | 2 +- libs/cmdio/prompt.go | 21 +++++-------------- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go index 68d507a57fb..3d934a188db 100644 --- a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go +++ b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go @@ -47,7 +47,7 @@ func TestPromptBaseline_Validate(t *testing.T) { } // TestPromptBaseline_ValidateEnterBeforeEdit pins that Enter on an untouched -// prompt flips the glyph to "✗" along with the error line. +// prompt shows the error line. func TestPromptBaseline_ValidateEnterBeforeEdit(t *testing.T) { t.Parallel() tm := termtest.NewPrompt(t, cmdio.PromptOptions{ diff --git a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden index 6da352062e8..aef99ba9f27 100644 --- a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden +++ b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_Validate/01-empty.golden @@ -1 +1 @@ - Workspace host: █ +✗ Workspace host: █ diff --git a/libs/cmdio/prompt.go b/libs/cmdio/prompt.go index 52870183e09..8c8ca1a3a39 100644 --- a/libs/cmdio/prompt.go +++ b/libs/cmdio/prompt.go @@ -33,9 +33,7 @@ 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. Until the first edit or Enter, - // an invalid input shows a blank glyph instead, so an empty prompt does - // not open as an error. + // below the prompt until the next edit. Validate func(input string) error } @@ -75,9 +73,6 @@ 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 @@ -97,16 +92,12 @@ func (m *promptModel) value() string { } // glyph returns the leading status indicator: bold-green ✔ when valid, -// bold-red ✗ when validate rejects the buffer after an edit or Enter, and a -// blank before that. +// bold-red ✗ when validate rejects the buffer. func (m *promptModel) glyph() string { - if m.validate == nil || m.validate(m.value()) == nil { - return ansiBold + ansiGreen + glyphValid + ansiReset - } - if !m.edited && m.submitErr == nil { - return " " + if m.validate != nil && m.validate(m.value()) != nil { + return ansiBold + ansiRed + glyphInvalid + ansiReset } - return ansiBold + ansiRed + glyphInvalid + ansiReset + return ansiBold + ansiGreen + glyphValid + ansiReset } func (m *promptModel) Init() tea.Cmd { return nil } @@ -163,7 +154,6 @@ 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: @@ -182,7 +172,6 @@ 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: From ab595b8158ea608113c9231383f6557d8b6c66f9 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Fri, 30 Oct 2026 16:55:48 +0000 Subject: [PATCH 11/29] List all query params in the normalizeLoginHost comment ExtractHostQueryParams also handles ?w=, ?workspace_id= and ?account_id=, not just ?o= and ?a=. Co-authored-by: Isaac --- cmd/auth/login.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index fd88b325209..2fb1ffff096 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -615,7 +615,8 @@ func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile, 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. +// host and moves the workspace and account ID query params into WorkspaceID and +// AccountID unless they are set. // 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 { From 6317d0a8069dcf6187ff640b22521ca06be67ef1 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Fri, 30 Oct 2026 16:56:25 +0000 Subject: [PATCH 12/29] Name the empty host test cases and keep paired tests together A "/" host made the subtest name end in "//", which is awkward to target from the command line. TestLoginSkipWorkspaceHostEmptyAfterNormalization had landed between the two positional argument tests. Co-authored-by: Isaac --- cmd/auth/login_device_code_test.go | 13 ++++++++++--- cmd/auth/login_test.go | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/cmd/auth/login_device_code_test.go b/cmd/auth/login_device_code_test.go index 334ea24a4c3..01cae5fc2d4 100644 --- a/cmd/auth/login_device_code_test.go +++ b/cmd/auth/login_device_code_test.go @@ -355,9 +355,16 @@ account_id = acct-123 } func TestLoginDeviceCodeHostEmptyAfterNormalization(t *testing.T) { - for _, host := range []string{"/", "?o=123"} { - t.Run(host, func(t *testing.T) { - tm := startDeviceCodeLogin(t, "", "--profile", "newprof", "--host", host) + tests := []struct { + name string + host string + }{ + {name: "slash", host: "/"}, + {name: "query params only", host: "?o=123"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tm := startDeviceCodeLogin(t, "", "--profile", "newprof", "--host", tt.host) _, err := tm.Result() assert.ErrorIs(t, err, errInvalidHost) }) diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index c4b8132ac38..7fb7cc3c65d 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -1463,6 +1463,17 @@ func TestLoginRejectsPositionalArgWithHostFlag(t *testing.T) { assert.ErrorContains(t, err, `argument "myprofile" cannot be combined with --host or --profile`) } +func TestLoginRejectsPositionalArgWithProfileFlag(t *testing.T) { + ctx := cmdio.MockDiscard(t.Context()) + authArgs := &auth.AuthArguments{} + cmd := newLoginCommand(authArgs) + cmd.Flags().String("profile", "", "") + cmd.SetContext(ctx) + cmd.SetArgs([]string{"--profile", "myprofile", "https://example.com"}) + err := cmd.Execute() + assert.ErrorContains(t, err, `argument "https://example.com" cannot be combined with --host or --profile`) +} + func TestLoginSkipWorkspaceHostEmptyAfterNormalization(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) @@ -1480,17 +1491,6 @@ func TestLoginSkipWorkspaceHostEmptyAfterNormalization(t *testing.T) { assert.EqualError(t, err, `invalid host "?a=123"`) } -func TestLoginRejectsPositionalArgWithProfileFlag(t *testing.T) { - ctx := cmdio.MockDiscard(t.Context()) - authArgs := &auth.AuthArguments{} - cmd := newLoginCommand(authArgs) - cmd.Flags().String("profile", "", "") - cmd.SetContext(ctx) - cmd.SetArgs([]string{"--profile", "myprofile", "https://example.com"}) - err := cmd.Execute() - assert.ErrorContains(t, err, `argument "https://example.com" cannot be combined with --host or --profile`) -} - func TestPromptForProfileRejectsTakenName(t *testing.T) { tm := termtest.Run(t, func(ctx context.Context) (string, error) { return promptForProfile(ctx, "DEFAULT", []string{"DEFAULT", "taken"}) From f206240552c4f568dd6d677f5e43114ed72206f3 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Fri, 30 Oct 2026 16:57:58 +0000 Subject: [PATCH 13/29] Trim spaces around the host in normalizeLoginHost Only the host prompt trimmed spaces, so --host " ", a positional argument or a profile host of only spaces passed the empty host check and failed later with "host must start with 'https://'". A leading space also made url.Parse fail, so ?o= never ended up in WorkspaceID. promptForHost still trims its result because the profile picker passes the host to getProfileName before it is normalized. Co-authored-by: Isaac --- cmd/auth/login.go | 2 +- cmd/auth/login_test.go | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 2fb1ffff096..87c39149ef6 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -621,7 +621,7 @@ var errInvalidHost = errors.New("invalid host") // with an empty host login would use discovery or DATABRICKS_HOST instead. func normalizeLoginHost(authArguments *auth.AuthArguments) error { rawHost := authArguments.Host - authArguments.Host = strings.TrimSuffix(authArguments.Host, "/") + authArguments.Host = strings.TrimSuffix(strings.TrimSpace(authArguments.Host), "/") // Extract query parameters from the host URL (?o=workspace_id, ?a=account_id). // URL params from explicit --host override stale profile values. diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index 7fb7cc3c65d..caf11f7cd90 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -221,6 +221,18 @@ func TestSetHost(t *testing.T) { err = setHostAndAccountId(ctx, &profile.Profile{Host: "/"}, &authArguments, []string{}) assert.ErrorIs(t, err, errInvalidHost) + // Test host of only spaces from flag + authArguments.Host = " " + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) + assert.EqualError(t, err, `invalid host " "`) + + // Test spaces around the host are trimmed before the query params are extracted + authArguments = auth.AuthArguments{Host: " https://www.host1.test/?o=123 "} + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) + assert.NoError(t, err) + assert.Equal(t, "https://www.host1.test", authArguments.Host) + assert.Equal(t, "123", authArguments.WorkspaceID) + // Test host is not set. Should prompt. authArguments.Host = "" err = setHostAndAccountId(ctx, nil, &authArguments, []string{}) From d091842210b3847205037ef87640b23dfe44636b Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Fri, 30 Oct 2026 16:59:03 +0000 Subject: [PATCH 14/29] Validate the normalized host at the login host prompt The prompt only rejected blank input, so "/" or "?o=123" passed and then failed in setHost. In auth login, the "Enter a host" picker path first asked for a profile name and then failed, and in auth token the command exited instead of asking again. Running normalizeLoginHost in Validate keeps the prompt open with the error under it. Co-authored-by: Isaac --- cmd/auth/auth.go | 4 +++- cmd/auth/auth_test.go | 12 ++++++++++++ .../01-query-params.golden | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 cmd/auth/testdata/TestPromptForHostRejectsHostEmptyAfterNormalization/01-query-params.golden diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 94f62d5edfe..32e6e3300e8 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -58,7 +58,9 @@ func promptForHost(ctx context.Context) (string, error) { if strings.TrimSpace(input) == "" { return errors.New("host is required") } - return nil + // Reject hosts that setHost would reject after the prompt, + // e.g. "/" or "?o=123", while the user can still edit them. + return normalizeLoginHost(&auth.AuthArguments{Host: input}) }, }) return strings.TrimSpace(result), err diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go index bfef5a34c4f..10cb67409b9 100644 --- a/cmd/auth/auth_test.go +++ b/cmd/auth/auth_test.go @@ -3,6 +3,7 @@ package auth import ( "os" "path/filepath" + "strings" "testing" "github.com/databricks/cli/cmd/root" @@ -155,3 +156,14 @@ func TestPromptForHostRejectsEmptyHost(t *testing.T) { require.NoError(t, err) assert.Equal(t, "https://workspace.test", host) } + +func TestPromptForHostRejectsHostEmptyAfterNormalization(t *testing.T) { + tm := termtest.Run(t, promptForHost) + tm.WaitFor("Databricks host") + tm.Type("?o=123" + termtest.KeyEnter) + tm.Golden("01-query-params") + tm.Type(strings.Repeat(termtest.KeyBackspace, len("?o=123")) + "https://workspace.test/?o=123" + termtest.KeyEnter) + host, err := tm.Result() + require.NoError(t, err) + assert.Equal(t, "https://workspace.test/?o=123", host) +} diff --git a/cmd/auth/testdata/TestPromptForHostRejectsHostEmptyAfterNormalization/01-query-params.golden b/cmd/auth/testdata/TestPromptForHostRejectsHostEmptyAfterNormalization/01-query-params.golden new file mode 100644 index 00000000000..be92e1393e7 --- /dev/null +++ b/cmd/auth/testdata/TestPromptForHostRejectsHostEmptyAfterNormalization/01-query-params.golden @@ -0,0 +1,3 @@ +Example: https://.cloud.databricks.com +✗ Databricks host: ?o=123█ +>> invalid host "?o=123" From 1f1a485dbfafa1e5941975a67183db0597166757 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Fri, 30 Oct 2026 17:00:20 +0000 Subject: [PATCH 15/29] Add acceptance tests for an invalid host in auth login and auth token The invalid host error is user-visible output of both commands, and the auth token path was not exercised end to end. Co-authored-by: Isaac --- acceptance/cmd/auth/login/invalid-host/out.test.toml | 1 + acceptance/cmd/auth/login/invalid-host/output.txt | 3 +++ acceptance/cmd/auth/login/invalid-host/script | 5 +++++ acceptance/cmd/auth/login/invalid-host/test.toml | 3 +++ acceptance/cmd/auth/token/invalid-host/out.test.toml | 1 + acceptance/cmd/auth/token/invalid-host/output.txt | 3 +++ acceptance/cmd/auth/token/invalid-host/script | 9 +++++++++ acceptance/cmd/auth/token/invalid-host/test.toml | 3 +++ 8 files changed, 28 insertions(+) create mode 100644 acceptance/cmd/auth/login/invalid-host/out.test.toml create mode 100644 acceptance/cmd/auth/login/invalid-host/output.txt create mode 100644 acceptance/cmd/auth/login/invalid-host/script create mode 100644 acceptance/cmd/auth/login/invalid-host/test.toml create mode 100644 acceptance/cmd/auth/token/invalid-host/out.test.toml create mode 100644 acceptance/cmd/auth/token/invalid-host/output.txt create mode 100644 acceptance/cmd/auth/token/invalid-host/script create mode 100644 acceptance/cmd/auth/token/invalid-host/test.toml diff --git a/acceptance/cmd/auth/login/invalid-host/out.test.toml b/acceptance/cmd/auth/login/invalid-host/out.test.toml new file mode 100644 index 00000000000..18b1a88417e --- /dev/null +++ b/acceptance/cmd/auth/login/invalid-host/out.test.toml @@ -0,0 +1 @@ +Cloud = false diff --git a/acceptance/cmd/auth/login/invalid-host/output.txt b/acceptance/cmd/auth/login/invalid-host/output.txt new file mode 100644 index 00000000000..d63e4c8e680 --- /dev/null +++ b/acceptance/cmd/auth/login/invalid-host/output.txt @@ -0,0 +1,3 @@ + +>>> musterr [CLI] auth login --host ?o=123 --profile test +Error: invalid host "?o=123" diff --git a/acceptance/cmd/auth/login/invalid-host/script b/acceptance/cmd/auth/login/invalid-host/script new file mode 100644 index 00000000000..55eaf5e314d --- /dev/null +++ b/acceptance/cmd/auth/login/invalid-host/script @@ -0,0 +1,5 @@ +sethome "./home" + +# A host that is empty without its query params is rejected before login +# can fall back to discovery or DATABRICKS_HOST. +trace musterr $CLI auth login --host '?o=123' --profile test diff --git a/acceptance/cmd/auth/login/invalid-host/test.toml b/acceptance/cmd/auth/login/invalid-host/test.toml new file mode 100644 index 00000000000..36c0e7e237b --- /dev/null +++ b/acceptance/cmd/auth/login/invalid-host/test.toml @@ -0,0 +1,3 @@ +Ignore = [ + "home" +] diff --git a/acceptance/cmd/auth/token/invalid-host/out.test.toml b/acceptance/cmd/auth/token/invalid-host/out.test.toml new file mode 100644 index 00000000000..18b1a88417e --- /dev/null +++ b/acceptance/cmd/auth/token/invalid-host/out.test.toml @@ -0,0 +1 @@ +Cloud = false diff --git a/acceptance/cmd/auth/token/invalid-host/output.txt b/acceptance/cmd/auth/token/invalid-host/output.txt new file mode 100644 index 00000000000..c6f176d19a5 --- /dev/null +++ b/acceptance/cmd/auth/token/invalid-host/output.txt @@ -0,0 +1,3 @@ + +>>> musterr [CLI] auth token --host ?o=123 +Error: invalid host "?o=123" diff --git a/acceptance/cmd/auth/token/invalid-host/script b/acceptance/cmd/auth/token/invalid-host/script new file mode 100644 index 00000000000..5130f9ee6f6 --- /dev/null +++ b/acceptance/cmd/auth/token/invalid-host/script @@ -0,0 +1,9 @@ +sethome "./home" + +unset DATABRICKS_HOST +unset DATABRICKS_TOKEN +unset DATABRICKS_CONFIG_PROFILE + +# A host that is empty without its query params is rejected instead of +# falling back to DATABRICKS_HOST. +trace musterr $CLI auth token --host '?o=123' diff --git a/acceptance/cmd/auth/token/invalid-host/test.toml b/acceptance/cmd/auth/token/invalid-host/test.toml new file mode 100644 index 00000000000..36c0e7e237b --- /dev/null +++ b/acceptance/cmd/auth/token/invalid-host/test.toml @@ -0,0 +1,3 @@ +Ignore = [ + "home" +] From 4e0b26198342f59bc567475cda12652ef9f13ba4 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 09:01:24 +0000 Subject: [PATCH 16/29] Drop the prompt baseline tests added for the reverted glyph change TestPromptBaseline_ValidateEnterBeforeEdit and TestPromptBaseline_ValidateValidBeforeEdit were written for the blank prompt glyph, which was reverted. They only pin unchanged libs/cmdio behavior, and the cmd/auth goldens cover the host prompt. Co-authored-by: Isaac --- .../prompt_validate_baseline_test.go | 44 ------------------- .../01-enter.golden | 2 - .../01-initial.golden | 1 - 3 files changed, 47 deletions(-) delete mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden delete mode 100644 libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden diff --git a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go index 3d934a188db..5f703a5865d 100644 --- a/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go +++ b/libs/cmdio/cmdiotest/prompt_validate_baseline_test.go @@ -45,47 +45,3 @@ func TestPromptBaseline_Validate(t *testing.T) { require.NoError(t, err, "raw output: %q", tm.Raw()) assert.Equal(t, "https://example.com", v, "snapshot:\n%s", tm.Snapshot()) } - -// TestPromptBaseline_ValidateEnterBeforeEdit pins that Enter on an untouched -// prompt shows the error line. -func TestPromptBaseline_ValidateEnterBeforeEdit(t *testing.T) { - t.Parallel() - tm := termtest.NewPrompt(t, cmdio.PromptOptions{ - Label: "Workspace host", - Validate: func(s string) error { - if s == "" { - return errors.New("host is required") - } - return nil - }, - }) - tm.WaitFor("Workspace host") - tm.Type(termtest.KeyEnter) - tm.Golden("01-enter") - - tm.Type(termtest.KeyCtrlC) - _, err := tm.Result() - assert.Error(t, err) -} - -// TestPromptBaseline_ValidateValidBeforeEdit pins that an untouched prompt -// whose input already passes Validate shows "✔". -func TestPromptBaseline_ValidateValidBeforeEdit(t *testing.T) { - t.Parallel() - tm := termtest.NewPrompt(t, cmdio.PromptOptions{ - Label: "Workspace host", - Validate: func(s string) error { - if strings.Contains(s, " ") { - return errors.New("must not contain spaces") - } - return nil - }, - }) - tm.WaitFor("Workspace host") - tm.Golden("01-initial") - - tm.Type(termtest.KeyEnter) - v, err := tm.Result() - require.NoError(t, err, "raw output: %q", tm.Raw()) - assert.Empty(t, v) -} diff --git a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden deleted file mode 100644 index a001d0c4bfa..00000000000 --- a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateEnterBeforeEdit/01-enter.golden +++ /dev/null @@ -1,2 +0,0 @@ -✗ Workspace host: █ ->> host is required diff --git a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden b/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden deleted file mode 100644 index c2487520307..00000000000 --- a/libs/cmdio/cmdiotest/testdata/TestPromptBaseline_ValidateValidBeforeEdit/01-initial.golden +++ /dev/null @@ -1 +0,0 @@ -✔ Workspace host: █ From da27a3aab0dade59c000965f512b2e1530cb4408 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 09:01:58 +0000 Subject: [PATCH 17/29] Reject a login host without a hostname normalizeLoginHost only rejected a host that was empty after removing the trailing slash and the query params, so "https://" became "https:/" and "https://?o=123" became "https:". The SDK reads those as a host named "https", so login tried to connect to it and auth token reported missing credentials. The check now parses the host, adding https:// to a host without an http(s) scheme as the SDK does, and fails if there is no hostname. A host that does not parse, e.g. with spaces, still fails later as before. Co-authored-by: Isaac --- .nextchanges/cli/login-reject-empty-host.md | 2 +- .../cmd/auth/login/invalid-host/output.txt | 3 +++ acceptance/cmd/auth/login/invalid-host/script | 4 ++++ .../cmd/auth/token/invalid-host/output.txt | 3 +++ acceptance/cmd/auth/token/invalid-host/script | 4 ++++ cmd/auth/login.go | 23 ++++++++++++++++--- cmd/auth/login_test.go | 16 +++++++++++++ 7 files changed, 51 insertions(+), 4 deletions(-) diff --git a/.nextchanges/cli/login-reject-empty-host.md b/.nextchanges/cli/login-reject-empty-host.md index ddb03d804a3..39f71892ba0 100644 --- a/.nextchanges/cli/login-reject-empty-host.md +++ b/.nextchanges/cli/login-reject-empty-host.md @@ -1 +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)) +* `databricks auth login` and `databricks auth token` now reject a host without a hostname, such as `/`, `?o=123` or `https://`, instead of falling back to `DATABRICKS_HOST` or failing later. 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)) diff --git a/acceptance/cmd/auth/login/invalid-host/output.txt b/acceptance/cmd/auth/login/invalid-host/output.txt index d63e4c8e680..7288b68fc0f 100644 --- a/acceptance/cmd/auth/login/invalid-host/output.txt +++ b/acceptance/cmd/auth/login/invalid-host/output.txt @@ -1,3 +1,6 @@ >>> musterr [CLI] auth login --host ?o=123 --profile test Error: invalid host "?o=123" + +>>> musterr [CLI] auth login --host https:// --profile test +Error: invalid host "https://" diff --git a/acceptance/cmd/auth/login/invalid-host/script b/acceptance/cmd/auth/login/invalid-host/script index 55eaf5e314d..c6a8ce4da47 100644 --- a/acceptance/cmd/auth/login/invalid-host/script +++ b/acceptance/cmd/auth/login/invalid-host/script @@ -3,3 +3,7 @@ sethome "./home" # A host that is empty without its query params is rejected before login # can fall back to discovery or DATABRICKS_HOST. trace musterr $CLI auth login --host '?o=123' --profile test + +# A host without a hostname is rejected instead of being read as a host +# named "https". +trace musterr $CLI auth login --host 'https://' --profile test diff --git a/acceptance/cmd/auth/token/invalid-host/output.txt b/acceptance/cmd/auth/token/invalid-host/output.txt index c6f176d19a5..d0d54632d4b 100644 --- a/acceptance/cmd/auth/token/invalid-host/output.txt +++ b/acceptance/cmd/auth/token/invalid-host/output.txt @@ -1,3 +1,6 @@ >>> musterr [CLI] auth token --host ?o=123 Error: invalid host "?o=123" + +>>> musterr [CLI] auth token --host https:// +Error: invalid host "https://" diff --git a/acceptance/cmd/auth/token/invalid-host/script b/acceptance/cmd/auth/token/invalid-host/script index 5130f9ee6f6..1a3c46bd114 100644 --- a/acceptance/cmd/auth/token/invalid-host/script +++ b/acceptance/cmd/auth/token/invalid-host/script @@ -7,3 +7,7 @@ unset DATABRICKS_CONFIG_PROFILE # A host that is empty without its query params is rejected instead of # falling back to DATABRICKS_HOST. trace musterr $CLI auth token --host '?o=123' + +# A host without a hostname is rejected instead of being read as a host +# named "https". +trace musterr $CLI auth token --host 'https://' diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 87c39149ef6..6ab0e61b6e6 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/url" "runtime" "slices" "strconv" @@ -617,8 +618,9 @@ var errInvalidHost = errors.New("invalid host") // normalizeLoginHost strips the trailing slash and the query params from the // host and moves the workspace and account ID query params into WorkspaceID and // AccountID unless they are set. -// 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. +// It fails if the host has no hostname, e.g. for "/", "?o=123" or "https://", +// because login would then use discovery or DATABRICKS_HOST, or connect to a +// host named "https". func normalizeLoginHost(authArguments *auth.AuthArguments) error { rawHost := authArguments.Host authArguments.Host = strings.TrimSuffix(strings.TrimSpace(authArguments.Host), "/") @@ -627,7 +629,7 @@ func normalizeLoginHost(authArguments *auth.AuthArguments) error { // URL params from explicit --host override stale profile values. params := auth.ExtractHostQueryParams(authArguments.Host) authArguments.Host = params.Host - if authArguments.Host == "" { + if lacksHostname(authArguments.Host) { return fmt.Errorf("%w %q", errInvalidHost, rawHost) } if authArguments.WorkspaceID == "" { @@ -639,6 +641,21 @@ func normalizeLoginHost(authArguments *auth.AuthArguments) error { return nil } +// lacksHostname reports whether host, with or without a scheme, is a URL +// without a hostname. A host that is not a URL, e.g. with spaces, is not +// rejected here and fails later as before. +func lacksHostname(host string) bool { + u, err := url.Parse(host) + // Like the SDK, treat a host without an http(s) scheme as an https host. + // url.Parse reads "localhost:8080" with "localhost" as the scheme and + // fails on "127.0.0.1:8080". Unlike the SDK, keep an explicit "https:" + // as the scheme, so "https:" is not read as a host named "https". + if err != nil || (u.Scheme != "https" && u.Scheme != "http") { + u, err = url.Parse("https://" + host) + } + return err == nil && u.Hostname() == "" +} + // 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. diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index caf11f7cd90..658d488fea8 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -226,6 +226,22 @@ func TestSetHost(t *testing.T) { err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) assert.EqualError(t, err, `invalid host " "`) + // Test host with a scheme but no hostname from flag + authArguments.Host = "https://" + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) + assert.EqualError(t, err, `invalid host "https://"`) + + // Test host with a scheme and query params but no hostname from flag + authArguments.Host = "https://?o=123" + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) + assert.EqualError(t, err, `invalid host "https://?o=123"`) + + // Test host:port without a scheme from flag + authArguments = auth.AuthArguments{Host: "www.host1.test:8080"} + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) + assert.NoError(t, err) + assert.Equal(t, "www.host1.test:8080", authArguments.Host) + // Test spaces around the host are trimmed before the query params are extracted authArguments = auth.AuthArguments{Host: " https://www.host1.test/?o=123 "} err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) From 7d6471df7f39a09084bb3dc3f3c4d1730ab49c26 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 09:02:15 +0000 Subject: [PATCH 18/29] Reject an invalid login host before the profile name prompt setHost normalizes the host after the keyring probe and the profile name prompt. With --host / and no --profile, login first asked for a profile name and only then failed with invalid host "/". Checking a host from --host or the positional argument right after the positional argument is resolved fails before both. Co-authored-by: Isaac --- cmd/auth/auth.go | 2 +- cmd/auth/login.go | 13 +++++++++++++ cmd/auth/login_test.go | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 32e6e3300e8..0bcfb9635c8 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -60,7 +60,7 @@ func promptForHost(ctx context.Context) (string, error) { } // Reject hosts that setHost would reject after the prompt, // e.g. "/" or "?o=123", while the user can still edit them. - return normalizeLoginHost(&auth.AuthArguments{Host: input}) + return validateLoginHost(input) }, }) return strings.TrimSpace(result), err diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 6ab0e61b6e6..072544e2f39 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -247,6 +247,14 @@ a new profile is created. } } + // setHost normalizes the host only after the keyring probe and the + // profile name prompt, so reject an invalid host before them. + if authArguments.Host != "" { + if err := validateLoginHost(authArguments.Host); err != nil { + return err + } + } + if err := validateDeviceCodeCompletionFlags(cmd); err != nil { return err } @@ -641,6 +649,11 @@ func normalizeLoginHost(authArguments *auth.AuthArguments) error { return nil } +// validateLoginHost returns the error normalizeLoginHost returns for host. +func validateLoginHost(host string) error { + return normalizeLoginHost(&auth.AuthArguments{Host: host}) +} + // lacksHostname reports whether host, with or without a scheme, is a URL // without a hostname. A host that is not a URL, e.g. with spaces, is not // rejected here and fails later as before. diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index 658d488fea8..76900356446 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -1519,6 +1519,25 @@ func TestLoginSkipWorkspaceHostEmptyAfterNormalization(t *testing.T) { assert.EqualError(t, err, `invalid host "?a=123"`) } +func TestLoginRejectsInvalidHostBeforeProfileNamePrompt(t *testing.T) { + tests := []struct { + name string + args []string + }{ + {name: "slash", args: []string{"--host", "/"}}, + {name: "no hostname", args: []string{"--host", "https://"}}, + {name: "positional arg without hostname", args: []string{"https://?o=123"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Without --profile, login would otherwise wait at the profile name prompt. + tm := startLogin(t, "", tt.args...) + _, err := tm.Result() + assert.ErrorIs(t, err, errInvalidHost) + }) + } +} + func TestPromptForProfileRejectsTakenName(t *testing.T) { tm := termtest.Run(t, func(ctx context.Context) (string, error) { return promptForProfile(ctx, "DEFAULT", []string{"DEFAULT", "taken"}) From 02ec972268a10d6aac29965582d66b06396755a4 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 09:13:51 +0000 Subject: [PATCH 19/29] Reject a login host that is not a URL A host that url.Parse rejects, e.g. "my workspace.test", passed the hostname check and failed later with an SDK parse error. It now fails with invalid host, like a host without a hostname. TestSetHost used "val from --host" and "val from [HOST]" as hosts, which are not URLs, so they are now real test hosts. Co-authored-by: Isaac --- .nextchanges/cli/login-reject-empty-host.md | 2 +- cmd/auth/login.go | 17 ++++++++--------- cmd/auth/login_test.go | 17 +++++++++++------ 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.nextchanges/cli/login-reject-empty-host.md b/.nextchanges/cli/login-reject-empty-host.md index 39f71892ba0..f8d36c6a325 100644 --- a/.nextchanges/cli/login-reject-empty-host.md +++ b/.nextchanges/cli/login-reject-empty-host.md @@ -1 +1 @@ -* `databricks auth login` and `databricks auth token` now reject a host without a hostname, such as `/`, `?o=123` or `https://`, instead of falling back to `DATABRICKS_HOST` or failing later. 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)) +* `databricks auth login` and `databricks auth token` now reject a host that is not a URL with a hostname, such as `/`, `?o=123` or `https://`, instead of falling back to `DATABRICKS_HOST` or failing later. 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)) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 072544e2f39..d4cf93aaa51 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -626,9 +626,9 @@ var errInvalidHost = errors.New("invalid host") // normalizeLoginHost strips the trailing slash and the query params from the // host and moves the workspace and account ID query params into WorkspaceID and // AccountID unless they are set. -// It fails if the host has no hostname, e.g. for "/", "?o=123" or "https://", -// because login would then use discovery or DATABRICKS_HOST, or connect to a -// host named "https". +// It fails if the host is not a URL with a hostname, e.g. for "/", "?o=123", +// "https://" or "my workspace.test", because login would then use discovery +// or DATABRICKS_HOST, connect to a host named "https", or fail later. func normalizeLoginHost(authArguments *auth.AuthArguments) error { rawHost := authArguments.Host authArguments.Host = strings.TrimSuffix(strings.TrimSpace(authArguments.Host), "/") @@ -637,7 +637,7 @@ func normalizeLoginHost(authArguments *auth.AuthArguments) error { // URL params from explicit --host override stale profile values. params := auth.ExtractHostQueryParams(authArguments.Host) authArguments.Host = params.Host - if lacksHostname(authArguments.Host) { + if !hasHostname(authArguments.Host) { return fmt.Errorf("%w %q", errInvalidHost, rawHost) } if authArguments.WorkspaceID == "" { @@ -654,10 +654,9 @@ func validateLoginHost(host string) error { return normalizeLoginHost(&auth.AuthArguments{Host: host}) } -// lacksHostname reports whether host, with or without a scheme, is a URL -// without a hostname. A host that is not a URL, e.g. with spaces, is not -// rejected here and fails later as before. -func lacksHostname(host string) bool { +// hasHostname reports whether host, with or without a scheme, is a URL with a +// hostname. +func hasHostname(host string) bool { u, err := url.Parse(host) // Like the SDK, treat a host without an http(s) scheme as an https host. // url.Parse reads "localhost:8080" with "localhost" as the scheme and @@ -666,7 +665,7 @@ func lacksHostname(host string) bool { if err != nil || (u.Scheme != "https" && u.Scheme != "http") { u, err = url.Parse("https://" + host) } - return err == nil && u.Hostname() == "" + return err == nil && u.Hostname() != "" } // Sets the host in the persistentAuth object based on the provided arguments and flags. diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index 76900356446..d18cdcf3a9d 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -166,15 +166,15 @@ func TestSetHost(t *testing.T) { profile2 := loadTestProfile(t, ctx, "profile-2") // Test error when both flag and argument are provided - authArguments.Host = "val from --host" - err := setHostAndAccountId(ctx, profile1, &authArguments, []string{"val from [HOST]"}) + authArguments.Host = "https://www.host-from-flag.test" + err := setHostAndAccountId(ctx, profile1, &authArguments, []string{"https://www.host-from-arg.test"}) assert.EqualError(t, err, "please only provide a host as an argument or a flag, not both") // Test setting host from flag - authArguments.Host = "val from --host" + authArguments.Host = "https://www.host-from-flag.test" err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) assert.NoError(t, err) - assert.Equal(t, "val from --host", authArguments.Host) + assert.Equal(t, "https://www.host-from-flag.test", authArguments.Host) // Test setting host from flag with trailing slash is stripped authArguments.Host = "https://www.host1.test/" @@ -184,9 +184,9 @@ func TestSetHost(t *testing.T) { // Test setting host from argument authArguments.Host = "" - err = setHostAndAccountId(ctx, profile1, &authArguments, []string{"val from [HOST]"}) + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{"https://www.host-from-arg.test"}) assert.NoError(t, err) - assert.Equal(t, "val from [HOST]", authArguments.Host) + assert.Equal(t, "https://www.host-from-arg.test", authArguments.Host) // Test setting host from argument with trailing slash is stripped authArguments.Host = "" @@ -236,6 +236,11 @@ func TestSetHost(t *testing.T) { err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) assert.EqualError(t, err, `invalid host "https://?o=123"`) + // Test host that is not a URL from flag + authArguments.Host = "my workspace.test" + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) + assert.EqualError(t, err, `invalid host "my workspace.test"`) + // Test host:port without a scheme from flag authArguments = auth.AuthArguments{Host: "www.host1.test:8080"} err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) From df05549adf57d72f657d7e8c57cafc4a5a39bd7a Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 09:42:57 +0000 Subject: [PATCH 20/29] Reject a login host with a scheme that is not a URL hasHostname retried any host that failed to parse with "https://" in front, so "https://my workspace.test" or "https://x.test:abc" was read as a host named "https" and passed the check. A host with "://" is now not retried. Co-authored-by: Isaac --- cmd/auth/login.go | 13 +++++++------ cmd/auth/login_test.go | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index d4cf93aaa51..6e85336c25b 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -627,7 +627,7 @@ var errInvalidHost = errors.New("invalid host") // host and moves the workspace and account ID query params into WorkspaceID and // AccountID unless they are set. // It fails if the host is not a URL with a hostname, e.g. for "/", "?o=123", -// "https://" or "my workspace.test", because login would then use discovery +// "https://" or "https://my workspace.test", because login would then use discovery // or DATABRICKS_HOST, connect to a host named "https", or fail later. func normalizeLoginHost(authArguments *auth.AuthArguments) error { rawHost := authArguments.Host @@ -658,11 +658,12 @@ func validateLoginHost(host string) error { // hostname. func hasHostname(host string) bool { u, err := url.Parse(host) - // Like the SDK, treat a host without an http(s) scheme as an https host. - // url.Parse reads "localhost:8080" with "localhost" as the scheme and - // fails on "127.0.0.1:8080". Unlike the SDK, keep an explicit "https:" - // as the scheme, so "https:" is not read as a host named "https". - if err != nil || (u.Scheme != "https" && u.Scheme != "http") { + // Treat a host without an http(s) scheme as an https host. url.Parse + // reads "localhost:8080" with "localhost" as the scheme and fails on + // "127.0.0.1:8080". Keep an explicit "https:" as the scheme, and don't + // retry a host with "://" that fails to parse, so that neither "https:" + // nor "https://my workspace.test" is read as a host named "https". + if !strings.Contains(host, "://") && (err != nil || (u.Scheme != "https" && u.Scheme != "http")) { u, err = url.Parse("https://" + host) } return err == nil && u.Hostname() != "" diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index d18cdcf3a9d..e0a86c2a46d 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -241,6 +241,11 @@ func TestSetHost(t *testing.T) { err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) assert.EqualError(t, err, `invalid host "my workspace.test"`) + // Test host with a scheme that is not a URL from flag + authArguments.Host = "https://my workspace.test" + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) + assert.EqualError(t, err, `invalid host "https://my workspace.test"`) + // Test host:port without a scheme from flag authArguments = auth.AuthArguments{Host: "www.host1.test:8080"} err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) From 7d24efd4641c0d20325cc5a19dde69f758d2d4cd Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 09:42:59 +0000 Subject: [PATCH 21/29] Rename the skip workspace invalid host test The early host check in auth login rejects the host before the --skip-workspace block normalizes it, so the test checks that an invalid host is rejected, not that it is empty after normalization. Co-authored-by: Isaac --- cmd/auth/login_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index e0a86c2a46d..7105b063cba 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -1512,7 +1512,7 @@ func TestLoginRejectsPositionalArgWithProfileFlag(t *testing.T) { assert.ErrorContains(t, err, `argument "https://example.com" cannot be combined with --host or --profile`) } -func TestLoginSkipWorkspaceHostEmptyAfterNormalization(t *testing.T) { +func TestLoginSkipWorkspaceRejectsInvalidHost(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("USERPROFILE", home) From 35c5fa91443112500785b833f7dd17d0a0adfa68 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 2 Nov 2026 09:56:00 +0000 Subject: [PATCH 22/29] Simplify the login host hostname check Check for an http(s) scheme prefix instead of parsing the host twice. Co-authored-by: Isaac --- cmd/auth/login.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 6e85336c25b..c80ba98ec43 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -657,15 +657,13 @@ func validateLoginHost(host string) error { // hasHostname reports whether host, with or without a scheme, is a URL with a // hostname. func hasHostname(host string) bool { - u, err := url.Parse(host) - // Treat a host without an http(s) scheme as an https host. url.Parse - // reads "localhost:8080" with "localhost" as the scheme and fails on - // "127.0.0.1:8080". Keep an explicit "https:" as the scheme, and don't - // retry a host with "://" that fails to parse, so that neither "https:" - // nor "https://my workspace.test" is read as a host named "https". - if !strings.Contains(host, "://") && (err != nil || (u.Scheme != "https" && u.Scheme != "http")) { - u, err = url.Parse("https://" + host) + // url.Parse can't read a host without a scheme, e.g. "localhost:8080", + // so check it as an https host. + lower := strings.ToLower(host) + if !strings.HasPrefix(lower, "https:") && !strings.HasPrefix(lower, "http:") { + host = "https://" + host } + u, err := url.Parse(host) return err == nil && u.Hostname() != "" } From de8d0272423db58374b6aff4321e07db7b19f2dc Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 15:15:36 +0000 Subject: [PATCH 23/29] Require an http or https scheme in the login host A host like "https//x.test" or "ftp://x.test" got "https://" prepended and was read as a host named "https" or "ftp". Co-authored-by: Isaac --- cmd/auth/login.go | 22 +++++++++++++--------- cmd/auth/login_test.go | 13 +++++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index c80ba98ec43..857fcdc06c3 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -654,17 +654,21 @@ func validateLoginHost(host string) error { return normalizeLoginHost(&auth.AuthArguments{Host: host}) } -// hasHostname reports whether host, with or without a scheme, is a URL with a -// hostname. +// hasHostname reports whether host, with or without a scheme, is an http or +// https URL with a hostname. func hasHostname(host string) bool { - // url.Parse can't read a host without a scheme, e.g. "localhost:8080", - // so check it as an https host. - lower := strings.ToLower(host) - if !strings.HasPrefix(lower, "https:") && !strings.HasPrefix(lower, "http:") { - host = "https://" + host - } u, err := url.Parse(host) - return err == nil && u.Hostname() != "" + // url.Parse can't read a host without a scheme, e.g. "localhost:8080", so + // check it as an https host. A host with "//" has a scheme, possibly a + // malformed one like in "https//x.test", so it is checked as is. + if (err != nil || !isHTTPScheme(u.Scheme)) && !strings.Contains(host, "//") { + u, err = url.Parse("https://" + host) + } + return err == nil && isHTTPScheme(u.Scheme) && u.Hostname() != "" +} + +func isHTTPScheme(scheme string) bool { + return scheme == "http" || scheme == "https" } // Sets the host in the persistentAuth object based on the provided arguments and flags. diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index 7105b063cba..a4bbca2fb1e 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -246,6 +246,19 @@ func TestSetHost(t *testing.T) { err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) assert.EqualError(t, err, `invalid host "https://my workspace.test"`) + // Test hosts with a malformed or unsupported scheme from flag + for _, host := range []string{"https//www.host1.test", "https:www.host1.test", "//www.host1.test", "ftp://www.host1.test"} { + authArguments.Host = host + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) + assert.ErrorIs(t, err, errInvalidHost, host) + } + + // Test host with an uppercase scheme from flag + authArguments = auth.AuthArguments{Host: "HTTPS://WWW.HOST1.TEST"} + err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) + assert.NoError(t, err) + assert.Equal(t, "HTTPS://WWW.HOST1.TEST", authArguments.Host) + // Test host:port without a scheme from flag authArguments = auth.AuthArguments{Host: "www.host1.test:8080"} err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) From 0810d37b697bc6f36574f4d66fe9614b3ab35597 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 15:17:03 +0000 Subject: [PATCH 24/29] Normalize the login host before the profile name prompt The host from --host or the positional argument was only validated before the profile name prompt, so the prompt suggested a name from the untrimmed host and ignored a ?a= account ID without --skip-workspace. Co-authored-by: Isaac --- cmd/auth/login.go | 7 ++++--- cmd/auth/login_test.go | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 857fcdc06c3..52f18326723 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -247,10 +247,11 @@ a new profile is created. } } - // setHost normalizes the host only after the keyring probe and the - // profile name prompt, so reject an invalid host before them. + // Normalize the host before the keyring probe and the profile name + // prompt, so an invalid host fails before them and the prompt suggests + // a name based on the normalized host and account ID. if authArguments.Host != "" { - if err := validateLoginHost(authArguments.Host); err != nil { + if err := normalizeLoginHost(authArguments); err != nil { return err } } diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index a4bbca2fb1e..99dbe2c2022 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -1561,6 +1561,26 @@ func TestLoginRejectsInvalidHostBeforeProfileNamePrompt(t *testing.T) { } } +func TestLoginProfileNamePromptSuggestsNameFromNormalizedHost(t *testing.T) { + tests := []struct { + name string + host string + want string + }{ + {name: "spaces", host: " https://myworkspace.test ", want: "[myworkspace]"}, + {name: "account ID", host: "https://myworkspace.test/?a=123", want: "[ACCOUNT-123]"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tm := startLogin(t, "", "--host", tt.host) + tm.WaitFor("Databricks profile name " + tt.want) + tm.Type(termtest.KeyCtrlC) + _, err := tm.Result() + assert.EqualError(t, err, "^C") + }) + } +} + func TestPromptForProfileRejectsTakenName(t *testing.T) { tm := termtest.Run(t, func(ctx context.Context) (string, error) { return promptForProfile(ctx, "DEFAULT", []string{"DEFAULT", "taken"}) From b4fe7aecb6a74a7b3f5d6571004126f4622af79c Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 15:17:44 +0000 Subject: [PATCH 25/29] Keep the login host unchanged when it is rejected Co-authored-by: Isaac --- cmd/auth/login.go | 11 +++++------ cmd/auth/login_test.go | 7 +++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 52f18326723..ce5ed2f6ad7 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -631,16 +631,15 @@ var errInvalidHost = errors.New("invalid host") // "https://" or "https://my workspace.test", because login would then use discovery // or DATABRICKS_HOST, connect to a host named "https", or fail later. func normalizeLoginHost(authArguments *auth.AuthArguments) error { - rawHost := authArguments.Host - authArguments.Host = strings.TrimSuffix(strings.TrimSpace(authArguments.Host), "/") + host := strings.TrimSuffix(strings.TrimSpace(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 !hasHostname(authArguments.Host) { - return fmt.Errorf("%w %q", errInvalidHost, rawHost) + params := auth.ExtractHostQueryParams(host) + if !hasHostname(params.Host) { + return fmt.Errorf("%w %q", errInvalidHost, authArguments.Host) } + authArguments.Host = params.Host if authArguments.WorkspaceID == "" { authArguments.WorkspaceID = params.WorkspaceID } diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index 99dbe2c2022..323a247ea50 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -843,6 +843,13 @@ func TestGetProfileName(t *testing.T) { } } +func TestNormalizeLoginHostKeepsArgumentsOnError(t *testing.T) { + args := &auth.AuthArguments{Host: "https://?o=123", WorkspaceID: "456"} + err := normalizeLoginHost(args) + assert.ErrorIs(t, err, errInvalidHost) + assert.Equal(t, &auth.AuthArguments{Host: "https://?o=123", WorkspaceID: "456"}, args) +} + // TestSkipWorkspaceProfileNameUsesDiscoveredAccountID verifies that the // pre-naming discovery block populates AccountID from .well-known so the // profile-name prompt suggests ACCOUNT- instead of the host-based From 46a6421b485a841d2debbe58dd311f0250cc85c4 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 15:19:18 +0000 Subject: [PATCH 26/29] Show an example host in the invalid login host error A host from a profile or DATABRICKS_HOST failed with only the host in the error. The host prompt keeps the short error, because it already prints the example above the input. Co-authored-by: Isaac --- .../cmd/auth/login/invalid-host/output.txt | 4 +-- .../cmd/auth/token/invalid-host/output.txt | 4 +-- cmd/auth/auth.go | 2 +- cmd/auth/login.go | 32 +++++++++++++------ cmd/auth/login_test.go | 12 +++---- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/acceptance/cmd/auth/login/invalid-host/output.txt b/acceptance/cmd/auth/login/invalid-host/output.txt index 7288b68fc0f..cdad0c705ef 100644 --- a/acceptance/cmd/auth/login/invalid-host/output.txt +++ b/acceptance/cmd/auth/login/invalid-host/output.txt @@ -1,6 +1,6 @@ >>> musterr [CLI] auth login --host ?o=123 --profile test -Error: invalid host "?o=123" +Error: invalid host "?o=123". Expected a URL like https://.cloud.databricks.com >>> musterr [CLI] auth login --host https:// --profile test -Error: invalid host "https://" +Error: invalid host "https://". Expected a URL like https://.cloud.databricks.com diff --git a/acceptance/cmd/auth/token/invalid-host/output.txt b/acceptance/cmd/auth/token/invalid-host/output.txt index d0d54632d4b..eeaec081e27 100644 --- a/acceptance/cmd/auth/token/invalid-host/output.txt +++ b/acceptance/cmd/auth/token/invalid-host/output.txt @@ -1,6 +1,6 @@ >>> musterr [CLI] auth token --host ?o=123 -Error: invalid host "?o=123" +Error: invalid host "?o=123". Expected a URL like https://.cloud.databricks.com >>> musterr [CLI] auth token --host https:// -Error: invalid host "https://" +Error: invalid host "https://". Expected a URL like https://.cloud.databricks.com diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 0bcfb9635c8..6bd71a7e43d 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -51,7 +51,7 @@ func promptForHost(ctx context.Context) (string, error) { // promptui's screenbuf does not account for terminal line wrapping, and a // 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://.cloud.databricks.com") + cmdio.LogString(ctx, "Example: "+exampleHost) result, err := cmdio.RunPrompt(ctx, cmdio.PromptOptions{ Label: "Databricks host", Validate: func(input string) error { diff --git a/cmd/auth/login.go b/cmd/auth/login.go index ce5ed2f6ad7..90611c19470 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -624,6 +624,8 @@ func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile, var errInvalidHost = errors.New("invalid host") +const exampleHost = "https://.cloud.databricks.com" + // normalizeLoginHost strips the trailing slash and the query params from the // host and moves the workspace and account ID query params into WorkspaceID and // AccountID unless they are set. @@ -631,13 +633,11 @@ var errInvalidHost = errors.New("invalid host") // "https://" or "https://my workspace.test", because login would then use discovery // or DATABRICKS_HOST, connect to a host named "https", or fail later. func normalizeLoginHost(authArguments *auth.AuthArguments) error { - host := strings.TrimSuffix(strings.TrimSpace(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(host) - if !hasHostname(params.Host) { - return fmt.Errorf("%w %q", errInvalidHost, authArguments.Host) + params, err := parseLoginHost(authArguments.Host) + if err != nil { + // The host may come from a profile or DATABRICKS_HOST, so say what + // shape is expected. + return fmt.Errorf("%w. Expected a URL like %s", err, exampleHost) } authArguments.Host = params.Host if authArguments.WorkspaceID == "" { @@ -649,9 +649,23 @@ func normalizeLoginHost(authArguments *auth.AuthArguments) error { return nil } -// validateLoginHost returns the error normalizeLoginHost returns for host. +// parseLoginHost trims host and extracts its query params. It returns +// errInvalidHost if host is not a URL with a hostname. +func parseLoginHost(host string) (auth.HostParams, error) { + // 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(strings.TrimSuffix(strings.TrimSpace(host), "/")) + if !hasHostname(params.Host) { + return auth.HostParams{}, fmt.Errorf("%w %q", errInvalidHost, host) + } + return params, nil +} + +// validateLoginHost returns the error normalizeLoginHost returns for host, +// without the example host that the host prompt prints above the input. func validateLoginHost(host string) error { - return normalizeLoginHost(&auth.AuthArguments{Host: host}) + _, err := parseLoginHost(host) + return err } // hasHostname reports whether host, with or without a scheme, is an http or diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index 323a247ea50..e5c705db5c0 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -224,27 +224,27 @@ func TestSetHost(t *testing.T) { // Test host of only spaces from flag authArguments.Host = " " err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) - assert.EqualError(t, err, `invalid host " "`) + assert.EqualError(t, err, `invalid host " ". Expected a URL like https://.cloud.databricks.com`) // Test host with a scheme but no hostname from flag authArguments.Host = "https://" err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) - assert.EqualError(t, err, `invalid host "https://"`) + assert.EqualError(t, err, `invalid host "https://". Expected a URL like https://.cloud.databricks.com`) // Test host with a scheme and query params but no hostname from flag authArguments.Host = "https://?o=123" err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) - assert.EqualError(t, err, `invalid host "https://?o=123"`) + assert.EqualError(t, err, `invalid host "https://?o=123". Expected a URL like https://.cloud.databricks.com`) // Test host that is not a URL from flag authArguments.Host = "my workspace.test" err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) - assert.EqualError(t, err, `invalid host "my workspace.test"`) + assert.EqualError(t, err, `invalid host "my workspace.test". Expected a URL like https://.cloud.databricks.com`) // Test host with a scheme that is not a URL from flag authArguments.Host = "https://my workspace.test" err = setHostAndAccountId(ctx, profile1, &authArguments, []string{}) - assert.EqualError(t, err, `invalid host "https://my workspace.test"`) + assert.EqualError(t, err, `invalid host "https://my workspace.test". Expected a URL like https://.cloud.databricks.com`) // Test hosts with a malformed or unsupported scheme from flag for _, host := range []string{"https//www.host1.test", "https:www.host1.test", "//www.host1.test", "ftp://www.host1.test"} { @@ -1546,7 +1546,7 @@ func TestLoginSkipWorkspaceRejectsInvalidHost(t *testing.T) { cmd.SetContext(ctx) cmd.SetArgs([]string{"--skip-workspace"}) err := cmd.Execute() - assert.EqualError(t, err, `invalid host "?a=123"`) + assert.EqualError(t, err, `invalid host "?a=123". Expected a URL like https://.cloud.databricks.com`) } func TestLoginRejectsInvalidHostBeforeProfileNamePrompt(t *testing.T) { From e0402018e8d5836d237cb3f2a25c5fab3d706ccc Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 15:19:34 +0000 Subject: [PATCH 27/29] Use the picker label in the changelog fragment Co-authored-by: Isaac --- .nextchanges/cli/login-reject-empty-host.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nextchanges/cli/login-reject-empty-host.md b/.nextchanges/cli/login-reject-empty-host.md index f8d36c6a325..0f0e18203b9 100644 --- a/.nextchanges/cli/login-reject-empty-host.md +++ b/.nextchanges/cli/login-reject-empty-host.md @@ -1 +1 @@ -* `databricks auth login` and `databricks auth token` now reject a host that is not a URL with a hostname, such as `/`, `?o=123` or `https://`, instead of falling back to `DATABRICKS_HOST` or failing later. 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)) +* `databricks auth login` and `databricks auth token` now reject a host that is not a URL with a hostname, such as `/`, `?o=123` or `https://`, instead of falling back to `DATABRICKS_HOST` or failing later. An empty host after "Enter a host URL manually" in the login profile picker no longer starts discovery login. ([#7195](https://github.com/databricks/cli/pull/7195)) From b5013362a59e014cfcf5b99022c13c83ae7a0f33 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 16:04:10 +0000 Subject: [PATCH 28/29] Only trim the login host before the profile name prompt Extracting the query params before the prompt made a workspace login with ?w=&account_id=, the example in the help text, suggest ACCOUNT- as the profile name. Accepting it could log in to an existing account profile and replace its host. The early step now only validates and trims the host, and setHost extracts the query params as before. Co-authored-by: Isaac --- cmd/auth/login.go | 10 ++++++---- cmd/auth/login_test.go | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 90611c19470..2679d67c598 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -247,13 +247,15 @@ a new profile is created. } } - // Normalize the host before the keyring probe and the profile name - // prompt, so an invalid host fails before them and the prompt suggests - // a name based on the normalized host and account ID. + // setHost normalizes the host only after the keyring probe and the + // profile name prompt, so reject an invalid host before them. Only trim + // it here: an account ID from the query params would make the prompt + // suggest ACCOUNT- for a workspace login. if authArguments.Host != "" { - if err := normalizeLoginHost(authArguments); err != nil { + if err := normalizeLoginHost(&auth.AuthArguments{Host: authArguments.Host}); err != nil { return err } + authArguments.Host = strings.TrimSpace(authArguments.Host) } if err := validateDeviceCodeCompletionFlags(cmd); err != nil { diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index e5c705db5c0..97b8d82cc9f 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -1568,14 +1568,16 @@ func TestLoginRejectsInvalidHostBeforeProfileNamePrompt(t *testing.T) { } } -func TestLoginProfileNamePromptSuggestsNameFromNormalizedHost(t *testing.T) { +func TestLoginProfileNamePromptSuggestsNameFromHost(t *testing.T) { tests := []struct { name string host string want string }{ {name: "spaces", host: " https://myworkspace.test ", want: "[myworkspace]"}, - {name: "account ID", host: "https://myworkspace.test/?a=123", want: "[ACCOUNT-123]"}, + // Without --skip-workspace this is a workspace login, so the account + // ID must not turn the suggestion into ACCOUNT-. + {name: "workspace and account ID", host: "https://myworkspace.test/?w=456&account_id=123", want: "[myworkspace]"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 27671c6ca934f802cfc4c8fe7cd050af2fa16ce3 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 3 Nov 2026 16:09:31 +0000 Subject: [PATCH 29/29] refactor --- cmd/auth/auth.go | 3 ++- cmd/auth/login.go | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 6bd71a7e43d..bfba43c7c76 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -60,7 +60,8 @@ func promptForHost(ctx context.Context) (string, error) { } // Reject hosts that setHost would reject after the prompt, // e.g. "/" or "?o=123", while the user can still edit them. - return validateLoginHost(input) + _, err := parseLoginHost(input) + return err }, }) return strings.TrimSpace(result), err diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 2679d67c598..067d9e6f7be 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -663,13 +663,6 @@ func parseLoginHost(host string) (auth.HostParams, error) { return params, nil } -// validateLoginHost returns the error normalizeLoginHost returns for host, -// without the example host that the host prompt prints above the input. -func validateLoginHost(host string) error { - _, err := parseLoginHost(host) - return err -} - // hasHostname reports whether host, with or without a scheme, is an http or // https URL with a hostname. func hasHostname(host string) bool {