From 1834d3312c748fe8f0bd4082f9787ed04f9e633f Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 25 Mar 2026 22:33:48 +0100 Subject: [PATCH 1/6] Add validateProfileHostConflict for auth commands Co-authored-by: Isaac --- cmd/auth/auth.go | 41 +++++++++++++++++++++++++++ cmd/auth/auth_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 cmd/auth/auth_test.go diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 9e632604d3..45a8214346 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -3,9 +3,12 @@ package auth import ( "context" "errors" + "fmt" "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/databrickscfg/profile" + "github.com/databricks/databricks-sdk-go/config" "github.com/spf13/cobra" ) @@ -59,3 +62,41 @@ func promptForAccountID(ctx context.Context) (string, error) { prompt.AllowEdit = true return prompt.Run() } + +func promptForWorkspaceID(ctx context.Context) (string, error) { + if !cmdio.IsPromptSupported(ctx) { + // Workspace ID is optional for unified hosts, so return empty string in non-interactive mode + return "", nil + } + + prompt := cmdio.Prompt(ctx) + prompt.Label = "Databricks workspace ID (optional - provide only if using this profile for workspace operations, leave empty for account operations)" + prompt.Default = "" + prompt.AllowEdit = true + return prompt.Run() +} + +// validateProfileHostConflict checks that --profile and --host don't conflict. +// If the profile's host matches the provided host (after canonicalization), +// the flags are considered compatible. If the profile is not found or has no +// host, the check is skipped (let the downstream command handle it). +func validateProfileHostConflict(ctx context.Context, profileName, host string, profiler profile.Profiler) error { + p, err := loadProfileByName(ctx, profileName, profiler) + if err != nil { + return err + } + if p == nil || p.Host == "" { + return nil + } + + profileHost := (&config.Config{Host: p.Host}).CanonicalHostName() + flagHost := (&config.Config{Host: host}).CanonicalHostName() + + if profileHost != flagHost { + return fmt.Errorf( + "--profile %q has host %q, which conflicts with --host %q. Use --profile alone to select a profile", + profileName, p.Host, host, + ) + } + return nil +} diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go new file mode 100644 index 0000000000..e8ca5baccd --- /dev/null +++ b/cmd/auth/auth_test.go @@ -0,0 +1,65 @@ +package auth + +import ( + "testing" + + "github.com/databricks/cli/libs/databrickscfg/profile" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateProfileHostConflict(t *testing.T) { + profiler := profile.InMemoryProfiler{ + Profiles: profile.Profiles{ + {Name: "logfood", Host: "https://logfood.cloud.databricks.com"}, + {Name: "staging", Host: "https://staging.cloud.databricks.com"}, + {Name: "no-host", Host: ""}, + }, + } + + cases := []struct { + name string + profileName string + host string + wantErr string + }{ + { + name: "matching hosts are allowed", + profileName: "logfood", + host: "https://logfood.cloud.databricks.com", + }, + { + name: "matching hosts with trailing slash", + profileName: "logfood", + host: "https://logfood.cloud.databricks.com/", + }, + { + name: "conflicting hosts produce error", + profileName: "logfood", + host: "https://other.cloud.databricks.com", + wantErr: `--profile "logfood" has host "https://logfood.cloud.databricks.com", which conflicts with --host "https://other.cloud.databricks.com". Use --profile alone to select a profile`, + }, + { + name: "profile not found skips check", + profileName: "nonexistent", + host: "https://any.cloud.databricks.com", + }, + { + name: "profile with no host skips check", + profileName: "no-host", + host: "https://any.cloud.databricks.com", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx := t.Context() + err := validateProfileHostConflict(ctx, tc.profileName, tc.host, profiler) + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} From ecb437907b159203e00ba46ba81fb9f09b909f02 Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 25 Mar 2026 22:38:08 +0100 Subject: [PATCH 2/6] auth: error when --profile and --host conflict Co-authored-by: Isaac --- cmd/auth/auth.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 45a8214346..f0778d80b4 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -25,6 +25,26 @@ Azure: https://learn.microsoft.com/azure/databricks/dev-tools/auth GCP: https://docs.gcp.databricks.com/dev-tools/auth/index.html`, } + cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + profileFlag := cmd.Flag("profile") + hostFlag := cmd.Flag("host") + + // Only validate when both flags are explicitly set by the user. + if profileFlag == nil || hostFlag == nil { + return nil + } + if !profileFlag.Changed || !hostFlag.Changed { + return nil + } + + return validateProfileHostConflict( + cmd.Context(), + profileFlag.Value.String(), + hostFlag.Value.String(), + profile.DefaultProfiler, + ) + } + var authArguments auth.AuthArguments cmd.PersistentFlags().StringVar(&authArguments.Host, "host", "", "Databricks Host") cmd.PersistentFlags().StringVar(&authArguments.AccountID, "account-id", "", "Databricks Account ID") From 9344eae18a3e80fcdee6af12a7c533b4d76b5fab Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 25 Mar 2026 23:45:55 +0100 Subject: [PATCH 3/6] fix: move profile-host conflict check to PreRunE on login/token Co-authored-by: Isaac --- cmd/auth/auth.go | 44 ++++++++++++++------------- cmd/auth/auth_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++- cmd/auth/login.go | 2 ++ cmd/auth/token.go | 2 ++ 4 files changed, 97 insertions(+), 22 deletions(-) diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index f0778d80b4..e102236fb3 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -25,26 +25,6 @@ Azure: https://learn.microsoft.com/azure/databricks/dev-tools/auth GCP: https://docs.gcp.databricks.com/dev-tools/auth/index.html`, } - cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { - profileFlag := cmd.Flag("profile") - hostFlag := cmd.Flag("host") - - // Only validate when both flags are explicitly set by the user. - if profileFlag == nil || hostFlag == nil { - return nil - } - if !profileFlag.Changed || !hostFlag.Changed { - return nil - } - - return validateProfileHostConflict( - cmd.Context(), - profileFlag.Value.String(), - hostFlag.Value.String(), - profile.DefaultProfiler, - ) - } - var authArguments auth.AuthArguments cmd.PersistentFlags().StringVar(&authArguments.Host, "host", "", "Databricks Host") cmd.PersistentFlags().StringVar(&authArguments.AccountID, "account-id", "", "Databricks Account ID") @@ -114,9 +94,31 @@ func validateProfileHostConflict(ctx context.Context, profileName, host string, if profileHost != flagHost { return fmt.Errorf( - "--profile %q has host %q, which conflicts with --host %q. Use --profile alone to select a profile", + "--profile %q has host %q, which conflicts with --host %q. Use --profile alone or --host alone, not both", profileName, p.Host, host, ) } return nil } + +// profileHostConflictCheck is a PreRunE function that validates +// --profile and --host don't conflict. +func profileHostConflictCheck(cmd *cobra.Command, args []string) error { + profileFlag := cmd.Flag("profile") + hostFlag := cmd.Flag("host") + + // Only validate when both flags are explicitly set by the user. + if profileFlag == nil || hostFlag == nil { + return nil + } + if !profileFlag.Changed || !hostFlag.Changed { + return nil + } + + return validateProfileHostConflict( + cmd.Context(), + profileFlag.Value.String(), + hostFlag.Value.String(), + profile.DefaultProfiler, + ) +} diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go index e8ca5baccd..59d86317c7 100644 --- a/cmd/auth/auth_test.go +++ b/cmd/auth/auth_test.go @@ -3,6 +3,8 @@ package auth import ( "testing" + "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/cmdctx" "github.com/databricks/cli/libs/databrickscfg/profile" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -37,7 +39,7 @@ func TestValidateProfileHostConflict(t *testing.T) { name: "conflicting hosts produce error", profileName: "logfood", host: "https://other.cloud.databricks.com", - wantErr: `--profile "logfood" has host "https://logfood.cloud.databricks.com", which conflicts with --host "https://other.cloud.databricks.com". Use --profile alone to select a profile`, + wantErr: `--profile "logfood" has host "https://logfood.cloud.databricks.com", which conflicts with --host "https://other.cloud.databricks.com". Use --profile alone or --host alone, not both`, }, { name: "profile not found skips check", @@ -63,3 +65,70 @@ func TestValidateProfileHostConflict(t *testing.T) { }) } } + +// TestProfileHostConflictViaCobra verifies that the conflict check runs +// through Cobra's lifecycle (PreRunE on login) and that the root command's +// PersistentPreRunE is NOT shadowed (it initializes logging, IO, user agent). +func TestProfileHostConflictViaCobra(t *testing.T) { + // Point at a config file that has "profile-1" with host https://www.host1.com. + t.Setenv("DATABRICKS_CONFIG_FILE", "./testdata/.databrickscfg") + + ctx := cmdctx.GenerateExecId(t.Context()) + cli := root.New(ctx) + cli.AddCommand(New()) + + // Set args: auth login --profile profile-1 --host https://other.host.com + cli.SetArgs([]string{ + "auth", "login", + "--profile", "profile-1", + "--host", "https://other.host.com", + }) + + _, err := cli.ExecuteContextC(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), `--profile "profile-1" has host "https://www.host1.com", which conflicts with --host "https://other.host.com"`) + assert.Contains(t, err.Error(), "Use --profile alone or --host alone, not both") +} + +// TestProfileHostConflictTokenViaCobra verifies the conflict check on the token subcommand. +func TestProfileHostConflictTokenViaCobra(t *testing.T) { + t.Setenv("DATABRICKS_CONFIG_FILE", "./testdata/.databrickscfg") + + ctx := cmdctx.GenerateExecId(t.Context()) + cli := root.New(ctx) + cli.AddCommand(New()) + + cli.SetArgs([]string{ + "auth", "token", + "--profile", "profile-1", + "--host", "https://other.host.com", + }) + + _, err := cli.ExecuteContextC(ctx) + require.Error(t, err) + assert.Contains(t, err.Error(), `--profile "profile-1" has host "https://www.host1.com", which conflicts with --host "https://other.host.com"`) +} + +// TestProfileHostCompatibleViaCobra verifies that matching --profile and --host +// pass the conflict check (the command will fail later for other reasons, but +// NOT with a conflict error). +func TestProfileHostCompatibleViaCobra(t *testing.T) { + t.Setenv("DATABRICKS_CONFIG_FILE", "./testdata/.databrickscfg") + + ctx := cmdctx.GenerateExecId(t.Context()) + cli := root.New(ctx) + cli.AddCommand(New()) + + cli.SetArgs([]string{ + "auth", "login", + "--profile", "profile-1", + "--host", "https://www.host1.com", + }) + + _, err := cli.ExecuteContextC(ctx) + // The command may fail for other reasons (no browser, non-interactive, etc.) + // but it should NOT fail with a conflict error. + if err != nil { + assert.NotContains(t, err.Error(), "conflicts with --host") + } +} diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 29732066ce..7c0a70686c 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -156,6 +156,8 @@ depends on the existing profiles you have set in your configuration file cmd.Flags().StringVar(&scopes, "scopes", "", "Comma-separated list of OAuth scopes to request (defaults to 'all-apis')") + cmd.PreRunE = profileHostConflictCheck + cmd.RunE = func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() profileName := cmd.Flag("profile").Value.String() diff --git a/cmd/auth/token.go b/cmd/auth/token.go index 83e9d9fc0a..8f8c7e3569 100644 --- a/cmd/auth/token.go +++ b/cmd/auth/token.go @@ -63,6 +63,8 @@ using a client ID and secret is not supported.`, cmd.Flags().DurationVar(&tokenTimeout, "timeout", defaultTimeout, "Timeout for acquiring a token.") + cmd.PreRunE = profileHostConflictCheck + cmd.RunE = func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() profileName := "" From f015e7f3c79ee2cea180bbcfeeea67507aa557ad Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 25 Mar 2026 23:52:55 +0100 Subject: [PATCH 4/6] update error message to nudge users toward --profile only --- cmd/auth/auth.go | 2 +- cmd/auth/auth_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index e102236fb3..3626738b6e 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -94,7 +94,7 @@ func validateProfileHostConflict(ctx context.Context, profileName, host string, if profileHost != flagHost { return fmt.Errorf( - "--profile %q has host %q, which conflicts with --host %q. Use --profile alone or --host alone, not both", + "--profile %q has host %q, which conflicts with --host %q. Use --profile only to select a profile", profileName, p.Host, host, ) } diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go index 59d86317c7..c54d550f01 100644 --- a/cmd/auth/auth_test.go +++ b/cmd/auth/auth_test.go @@ -39,7 +39,7 @@ func TestValidateProfileHostConflict(t *testing.T) { name: "conflicting hosts produce error", profileName: "logfood", host: "https://other.cloud.databricks.com", - wantErr: `--profile "logfood" has host "https://logfood.cloud.databricks.com", which conflicts with --host "https://other.cloud.databricks.com". Use --profile alone or --host alone, not both`, + wantErr: `--profile "logfood" has host "https://logfood.cloud.databricks.com", which conflicts with --host "https://other.cloud.databricks.com". Use --profile only to select a profile`, }, { name: "profile not found skips check", @@ -87,7 +87,7 @@ func TestProfileHostConflictViaCobra(t *testing.T) { _, err := cli.ExecuteContextC(ctx) require.Error(t, err) assert.Contains(t, err.Error(), `--profile "profile-1" has host "https://www.host1.com", which conflicts with --host "https://other.host.com"`) - assert.Contains(t, err.Error(), "Use --profile alone or --host alone, not both") + assert.Contains(t, err.Error(), "Use --profile only to select a profile") } // TestProfileHostConflictTokenViaCobra verifies the conflict check on the token subcommand. From c758131de3c234322c4a9fed2215dcb23c4fb64a Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 26 Mar 2026 00:05:05 +0100 Subject: [PATCH 5/6] Add NEXT_CHANGELOG entry for auth profile/host conflict detection Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index b406a874e8..ff3d36343a 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -5,6 +5,7 @@ ### Notable Changes ### CLI +* Auth commands now error when --profile and --host conflict ([#4841](https://github.com/databricks/cli/pull/4841)) ### Bundles * engine/direct: Fix drift in grants resource due to privilege reordering ([#4794](https://github.com/databricks/cli/pull/4794)) From 70e8eabbfb83f73cd12963f375db4dca1a8da8dc Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 26 Mar 2026 23:36:38 +0100 Subject: [PATCH 6/6] fix: remove stale promptForWorkspaceID, update custom-config-file test Remove promptForWorkspaceID that was deleted on main but accidentally reintroduced during rebase. Update the custom-config-file acceptance test to expect the new profile/host conflict error. Co-authored-by: Isaac --- .../custom-config-file/out.databrickscfg | 7 ------- .../auth/login/custom-config-file/output.txt | 16 +++------------ .../cmd/auth/login/custom-config-file/script | 20 ++----------------- cmd/auth/auth.go | 13 ------------ 4 files changed, 5 insertions(+), 51 deletions(-) delete mode 100644 acceptance/cmd/auth/login/custom-config-file/out.databrickscfg diff --git a/acceptance/cmd/auth/login/custom-config-file/out.databrickscfg b/acceptance/cmd/auth/login/custom-config-file/out.databrickscfg deleted file mode 100644 index 2097f1a344..0000000000 --- a/acceptance/cmd/auth/login/custom-config-file/out.databrickscfg +++ /dev/null @@ -1,7 +0,0 @@ -; The profile defined in the DEFAULT section is to be used as a fallback when no profile is explicitly specified. -[DEFAULT] - -[custom-test] -host = [DATABRICKS_URL] -auth_type = databricks-cli -workspace_id = [NUMID] diff --git a/acceptance/cmd/auth/login/custom-config-file/output.txt b/acceptance/cmd/auth/login/custom-config-file/output.txt index f6657d7c6f..c586e020df 100644 --- a/acceptance/cmd/auth/login/custom-config-file/output.txt +++ b/acceptance/cmd/auth/login/custom-config-file/output.txt @@ -5,18 +5,8 @@ host = https://old-host.cloud.databricks.com auth_type = pat token = old-token-123 -=== Login with new host (should override old host) +=== Login with conflicting host (should error) >>> [CLI] auth login --host [DATABRICKS_URL] --profile custom-test -Profile custom-test was successfully saved +Error: --profile "custom-test" has host "https://old-host.cloud.databricks.com", which conflicts with --host "[DATABRICKS_URL]". Use --profile only to select a profile -=== Default config file should NOT exist -OK: Default .databrickscfg does not exist - -=== Custom config file after login (old host should be replaced) -; The profile defined in the DEFAULT section is to be used as a fallback when no profile is explicitly specified. -[DEFAULT] - -[custom-test] -host = [DATABRICKS_URL] -auth_type = databricks-cli -workspace_id = [NUMID] +Exit code: 1 diff --git a/acceptance/cmd/auth/login/custom-config-file/script b/acceptance/cmd/auth/login/custom-config-file/script index 8be849974e..6e7aeb6c6a 100644 --- a/acceptance/cmd/auth/login/custom-config-file/script +++ b/acceptance/cmd/auth/login/custom-config-file/script @@ -8,9 +8,7 @@ export BROWSER="browser.py" export DATABRICKS_CONFIG_FILE="./home/custom.databrickscfg" # Create an existing custom config file with a DIFFERENT host. -# The login command should use the host from --host argument, NOT from this file. -# If the wrong host (from the config file) were used, the OAuth flow would fail -# because the mock server only responds for $DATABRICKS_HOST. +# Since --profile and --host conflict, the CLI should error. cat > "./home/custom.databrickscfg" <