Skip to content

Commit 49cc72b

Browse files
authored
Auth: error when --profile and --host conflict (#4841)
## Why Users can pass both `--profile logfood` and `--host https://other-host.com` on auth commands with no validation. The CLI silently picks one based on internal priority, which is confusing. ## Changes Before: `databricks auth login --profile logfood --host https://other.com` silently uses one of the two with no warning. Now: produces an error explaining the conflict and suggesting to use `--profile` alone. Adds a `PersistentPreRunE` on the `auth` parent command that validates when both `--profile` and `--host` are explicitly set. If the profile's host matches the `--host` value (after canonicalization), the command proceeds silently. If they conflict, it returns a clear error. ## Test plan - [x] New unit tests for `validateProfileHostConflict` (5 table-driven cases: matching hosts, trailing slash, conflict, profile not found, no host) - [x] `make checks` passes - [x] `make lintfull` passes - [x] `go test ./cmd/auth/` passes
1 parent da3daf5 commit 49cc72b

File tree

8 files changed

+197
-41
lines changed

8 files changed

+197
-41
lines changed

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Notable Changes
66

77
### CLI
8+
* Auth commands now error when --profile and --host conflict ([#4841](https://github.com/databricks/cli/pull/4841))
89
* Add `--force-refresh` flag to `databricks auth token` to force a token refresh even when the cached token is still valid ([#4767](https://github.com/databricks/cli/pull/4767)).
910

1011
### Bundles

acceptance/cmd/auth/login/custom-config-file/out.databrickscfg

Lines changed: 0 additions & 7 deletions
This file was deleted.

acceptance/cmd/auth/login/custom-config-file/output.txt

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,8 @@ host = https://old-host.cloud.databricks.com
55
auth_type = pat
66
token = old-token-123
77

8-
=== Login with new host (should override old host)
8+
=== Login with conflicting host (should error)
99
>>> [CLI] auth login --host [DATABRICKS_URL] --profile custom-test
10-
Profile custom-test was successfully saved
10+
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
1111

12-
=== Default config file should NOT exist
13-
OK: Default .databrickscfg does not exist
14-
15-
=== Custom config file after login (old host should be replaced)
16-
; The profile defined in the DEFAULT section is to be used as a fallback when no profile is explicitly specified.
17-
[DEFAULT]
18-
19-
[custom-test]
20-
host = [DATABRICKS_URL]
21-
auth_type = databricks-cli
22-
workspace_id = [NUMID]
12+
Exit code: 1

acceptance/cmd/auth/login/custom-config-file/script

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ export BROWSER="browser.py"
88
export DATABRICKS_CONFIG_FILE="./home/custom.databrickscfg"
99

1010
# Create an existing custom config file with a DIFFERENT host.
11-
# The login command should use the host from --host argument, NOT from this file.
12-
# If the wrong host (from the config file) were used, the OAuth flow would fail
13-
# because the mock server only responds for $DATABRICKS_HOST.
11+
# Since --profile and --host conflict, the CLI should error.
1412
cat > "./home/custom.databrickscfg" <<EOF
1513
[custom-test]
1614
host = https://old-host.cloud.databricks.com
@@ -21,19 +19,5 @@ EOF
2119
title "Initial custom config file (with old host)\n"
2220
cat "./home/custom.databrickscfg"
2321

24-
title "Login with new host (should override old host)"
22+
title "Login with conflicting host (should error)"
2523
trace $CLI auth login --host $DATABRICKS_HOST --profile custom-test
26-
27-
title "Default config file should NOT exist\n"
28-
if [ -f "./home/.databrickscfg" ]; then
29-
echo "ERROR: Default .databrickscfg exists but should not"
30-
cat "./home/.databrickscfg"
31-
else
32-
echo "OK: Default .databrickscfg does not exist"
33-
fi
34-
35-
title "Custom config file after login (old host should be replaced)\n"
36-
cat "./home/custom.databrickscfg"
37-
38-
# Track the custom config file to surface changes
39-
mv "./home/custom.databrickscfg" "./out.databrickscfg"

cmd/auth/auth.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package auth
33
import (
44
"context"
55
"errors"
6+
"fmt"
67

78
"github.com/databricks/cli/libs/auth"
89
"github.com/databricks/cli/libs/cmdio"
10+
"github.com/databricks/cli/libs/databrickscfg/profile"
11+
"github.com/databricks/databricks-sdk-go/config"
912
"github.com/spf13/cobra"
1013
)
1114

@@ -59,3 +62,50 @@ func promptForAccountID(ctx context.Context) (string, error) {
5962
prompt.AllowEdit = true
6063
return prompt.Run()
6164
}
65+
66+
// validateProfileHostConflict checks that --profile and --host don't conflict.
67+
// If the profile's host matches the provided host (after canonicalization),
68+
// the flags are considered compatible. If the profile is not found or has no
69+
// host, the check is skipped (let the downstream command handle it).
70+
func validateProfileHostConflict(ctx context.Context, profileName, host string, profiler profile.Profiler) error {
71+
p, err := loadProfileByName(ctx, profileName, profiler)
72+
if err != nil {
73+
return err
74+
}
75+
if p == nil || p.Host == "" {
76+
return nil
77+
}
78+
79+
profileHost := (&config.Config{Host: p.Host}).CanonicalHostName()
80+
flagHost := (&config.Config{Host: host}).CanonicalHostName()
81+
82+
if profileHost != flagHost {
83+
return fmt.Errorf(
84+
"--profile %q has host %q, which conflicts with --host %q. Use --profile only to select a profile",
85+
profileName, p.Host, host,
86+
)
87+
}
88+
return nil
89+
}
90+
91+
// profileHostConflictCheck is a PreRunE function that validates
92+
// --profile and --host don't conflict.
93+
func profileHostConflictCheck(cmd *cobra.Command, args []string) error {
94+
profileFlag := cmd.Flag("profile")
95+
hostFlag := cmd.Flag("host")
96+
97+
// Only validate when both flags are explicitly set by the user.
98+
if profileFlag == nil || hostFlag == nil {
99+
return nil
100+
}
101+
if !profileFlag.Changed || !hostFlag.Changed {
102+
return nil
103+
}
104+
105+
return validateProfileHostConflict(
106+
cmd.Context(),
107+
profileFlag.Value.String(),
108+
hostFlag.Value.String(),
109+
profile.DefaultProfiler,
110+
)
111+
}

cmd/auth/auth_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package auth
2+
3+
import (
4+
"testing"
5+
6+
"github.com/databricks/cli/cmd/root"
7+
"github.com/databricks/cli/libs/cmdctx"
8+
"github.com/databricks/cli/libs/databrickscfg/profile"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestValidateProfileHostConflict(t *testing.T) {
14+
profiler := profile.InMemoryProfiler{
15+
Profiles: profile.Profiles{
16+
{Name: "logfood", Host: "https://logfood.cloud.databricks.com"},
17+
{Name: "staging", Host: "https://staging.cloud.databricks.com"},
18+
{Name: "no-host", Host: ""},
19+
},
20+
}
21+
22+
cases := []struct {
23+
name string
24+
profileName string
25+
host string
26+
wantErr string
27+
}{
28+
{
29+
name: "matching hosts are allowed",
30+
profileName: "logfood",
31+
host: "https://logfood.cloud.databricks.com",
32+
},
33+
{
34+
name: "matching hosts with trailing slash",
35+
profileName: "logfood",
36+
host: "https://logfood.cloud.databricks.com/",
37+
},
38+
{
39+
name: "conflicting hosts produce error",
40+
profileName: "logfood",
41+
host: "https://other.cloud.databricks.com",
42+
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`,
43+
},
44+
{
45+
name: "profile not found skips check",
46+
profileName: "nonexistent",
47+
host: "https://any.cloud.databricks.com",
48+
},
49+
{
50+
name: "profile with no host skips check",
51+
profileName: "no-host",
52+
host: "https://any.cloud.databricks.com",
53+
},
54+
}
55+
56+
for _, tc := range cases {
57+
t.Run(tc.name, func(t *testing.T) {
58+
ctx := t.Context()
59+
err := validateProfileHostConflict(ctx, tc.profileName, tc.host, profiler)
60+
if tc.wantErr != "" {
61+
assert.ErrorContains(t, err, tc.wantErr)
62+
} else {
63+
require.NoError(t, err)
64+
}
65+
})
66+
}
67+
}
68+
69+
// TestProfileHostConflictViaCobra verifies that the conflict check runs
70+
// through Cobra's lifecycle (PreRunE on login) and that the root command's
71+
// PersistentPreRunE is NOT shadowed (it initializes logging, IO, user agent).
72+
func TestProfileHostConflictViaCobra(t *testing.T) {
73+
// Point at a config file that has "profile-1" with host https://www.host1.com.
74+
t.Setenv("DATABRICKS_CONFIG_FILE", "./testdata/.databrickscfg")
75+
76+
ctx := cmdctx.GenerateExecId(t.Context())
77+
cli := root.New(ctx)
78+
cli.AddCommand(New())
79+
80+
// Set args: auth login --profile profile-1 --host https://other.host.com
81+
cli.SetArgs([]string{
82+
"auth", "login",
83+
"--profile", "profile-1",
84+
"--host", "https://other.host.com",
85+
})
86+
87+
_, err := cli.ExecuteContextC(ctx)
88+
require.Error(t, err)
89+
assert.Contains(t, err.Error(), `--profile "profile-1" has host "https://www.host1.com", which conflicts with --host "https://other.host.com"`)
90+
assert.Contains(t, err.Error(), "Use --profile only to select a profile")
91+
}
92+
93+
// TestProfileHostConflictTokenViaCobra verifies the conflict check on the token subcommand.
94+
func TestProfileHostConflictTokenViaCobra(t *testing.T) {
95+
t.Setenv("DATABRICKS_CONFIG_FILE", "./testdata/.databrickscfg")
96+
97+
ctx := cmdctx.GenerateExecId(t.Context())
98+
cli := root.New(ctx)
99+
cli.AddCommand(New())
100+
101+
cli.SetArgs([]string{
102+
"auth", "token",
103+
"--profile", "profile-1",
104+
"--host", "https://other.host.com",
105+
})
106+
107+
_, err := cli.ExecuteContextC(ctx)
108+
require.Error(t, err)
109+
assert.Contains(t, err.Error(), `--profile "profile-1" has host "https://www.host1.com", which conflicts with --host "https://other.host.com"`)
110+
}
111+
112+
// TestProfileHostCompatibleViaCobra verifies that matching --profile and --host
113+
// pass the conflict check (the command will fail later for other reasons, but
114+
// NOT with a conflict error).
115+
func TestProfileHostCompatibleViaCobra(t *testing.T) {
116+
t.Setenv("DATABRICKS_CONFIG_FILE", "./testdata/.databrickscfg")
117+
118+
ctx := cmdctx.GenerateExecId(t.Context())
119+
cli := root.New(ctx)
120+
cli.AddCommand(New())
121+
122+
cli.SetArgs([]string{
123+
"auth", "login",
124+
"--profile", "profile-1",
125+
"--host", "https://www.host1.com",
126+
})
127+
128+
_, err := cli.ExecuteContextC(ctx)
129+
// The command may fail for other reasons (no browser, non-interactive, etc.)
130+
// but it should NOT fail with a conflict error.
131+
if err != nil {
132+
assert.NotContains(t, err.Error(), "conflicts with --host")
133+
}
134+
}

cmd/auth/login.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ depends on the existing profiles you have set in your configuration file
131131
not the case before.
132132
133133
3. If a profile with the specified name exists and specifies a host, but you
134-
specify a host using --host (or as the [HOST] positional arg), the profile will
135-
be updated to use the newly specified host. The auth type will be updated to
136-
"databricks-cli" if that was not the case before.
134+
specify a different host using --host (or as the [HOST] positional arg), the
135+
command returns an error. Use --profile alone to log in with the profile's
136+
configured host, or omit --profile to log in with the specified host.
137137
138138
4. If a profile with the specified name does not exist, a new profile will be
139139
created with the specified host. The auth type will be set to "databricks-cli".
@@ -156,6 +156,8 @@ depends on the existing profiles you have set in your configuration file
156156
cmd.Flags().StringVar(&scopes, "scopes", "",
157157
"Comma-separated list of OAuth scopes to request (defaults to 'all-apis')")
158158

159+
cmd.PreRunE = profileHostConflictCheck
160+
159161
cmd.RunE = func(cmd *cobra.Command, args []string) error {
160162
ctx := cmd.Context()
161163
profileName := cmd.Flag("profile").Value.String()

cmd/auth/token.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ and secret is not supported.`,
7171
cmd.Flags().BoolVar(&forceRefresh, "force-refresh", false,
7272
"Force a token refresh even if the cached token is still valid.")
7373

74+
cmd.PreRunE = profileHostConflictCheck
75+
7476
cmd.RunE = func(cmd *cobra.Command, args []string) error {
7577
ctx := cmd.Context()
7678
profileName := ""

0 commit comments

Comments
 (0)