Skip to content

Commit 278e906

Browse files
authored
Return clear error when auth token is used with M2M profile (#4594)
## Summary Fixes #1939 When `databricks auth token --profile <m2m-profile>` is used with a profile that has `client_id`/`client_secret` configured (M2M / service principal auth), the command currently either: 1. **Silently returns the wrong token** — a cached U2M token for the same host, identifying the user instead of the service principal 2. **Triggers an interactive browser login** — pushing the user into U2M auth with no explanation This PR adds early detection of M2M profiles and returns a clear error: ``` Error: profile "my-sp" uses M2M authentication (client_id/client_secret). `databricks auth token` only supports U2M (user-to-machine) authentication tokens. To authenticate as a service principal, use the Databricks SDK directly ``` ### Changes - Added `ClientID` field to `profile.Profile` struct (populated from `.databrickscfg`) - Added check in `loadToken` that detects M2M profiles before attempting U2M token lookup - After host-based profile resolution, `existingProfile` is reloaded so the M2M guard also covers `--host` invocations that resolve to a single M2M profile - Works for all three profile resolution paths: `--profile` flag, positional argument, and host-based matching ## Test plan - [x] `M2M profile returns clear error` — explicit `--profile` targeting M2M profile - [x] `M2M profile detected via positional arg` — positional arg resolves to M2M profile - [x] `M2M profile detected via host resolution` — `--host` uniquely matches an M2M profile - [x] All existing `TestToken_loadToken` cases pass (no regressions) - [x] `make checks` passes - [x] `make lintfull` passes
1 parent 79d8237 commit 278e906

File tree

4 files changed

+78
-14
lines changed

4 files changed

+78
-14
lines changed

cmd/auth/token.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,27 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) {
205205
return nil, err
206206
}
207207
args.profileName = selected
208+
existingProfile, err = loadProfileByName(ctx, selected, args.profiler)
209+
if err != nil {
210+
return nil, err
211+
}
208212
} else if len(matchingProfiles) == 1 {
209213
args.profileName = matchingProfiles[0].Name
214+
existingProfile = &matchingProfiles[0]
210215
}
211216
}
212217

218+
// Check if the resolved profile uses M2M authentication (client credentials).
219+
// The auth token command only supports U2M OAuth tokens.
220+
if existingProfile != nil && existingProfile.HasClientCredentials {
221+
return nil, fmt.Errorf(
222+
"profile %q uses M2M authentication (client_id/client_secret). "+
223+
"`databricks auth token` only supports U2M (user-to-machine) authentication tokens. "+
224+
"To authenticate as a service principal, use the Databricks SDK directly",
225+
args.profileName,
226+
)
227+
}
228+
213229
args.authArguments.Profile = args.profileName
214230

215231
ctx, cancel := context.WithTimeout(ctx, args.tokenTimeout)

cmd/auth/token_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ func TestToken_loadToken(t *testing.T) {
125125
Name: "legacy-ws",
126126
Host: "https://legacy-ws.cloud.databricks.com",
127127
},
128+
{
129+
Name: "m2m-profile",
130+
Host: "https://m2m.cloud.databricks.com",
131+
HasClientCredentials: true,
132+
},
128133
},
129134
}
130135
tokenCache := &inMemoryTokenCache{
@@ -526,6 +531,47 @@ func TestToken_loadToken(t *testing.T) {
526531
},
527532
wantErr: "no profiles configured. Run 'databricks auth login' to create a profile",
528533
},
534+
{
535+
name: "M2M profile returns clear error",
536+
args: loadTokenArgs{
537+
authArguments: &auth.AuthArguments{},
538+
profileName: "m2m-profile",
539+
args: []string{},
540+
tokenTimeout: 1 * time.Hour,
541+
profiler: profiler,
542+
},
543+
wantErr: `profile "m2m-profile" uses M2M authentication (client_id/client_secret). ` +
544+
"`databricks auth token` only supports U2M (user-to-machine) authentication tokens. " +
545+
"To authenticate as a service principal, use the Databricks SDK directly",
546+
},
547+
{
548+
name: "M2M profile detected via positional arg",
549+
args: loadTokenArgs{
550+
authArguments: &auth.AuthArguments{},
551+
profileName: "",
552+
args: []string{"m2m-profile"},
553+
tokenTimeout: 1 * time.Hour,
554+
profiler: profiler,
555+
},
556+
wantErr: `profile "m2m-profile" uses M2M authentication (client_id/client_secret). ` +
557+
"`databricks auth token` only supports U2M (user-to-machine) authentication tokens. " +
558+
"To authenticate as a service principal, use the Databricks SDK directly",
559+
},
560+
{
561+
name: "M2M profile detected via host resolution",
562+
args: loadTokenArgs{
563+
authArguments: &auth.AuthArguments{
564+
Host: "https://m2m.cloud.databricks.com",
565+
},
566+
profileName: "",
567+
args: []string{},
568+
tokenTimeout: 1 * time.Hour,
569+
profiler: profiler,
570+
},
571+
wantErr: `profile "m2m-profile" uses M2M authentication (client_id/client_secret). ` +
572+
"`databricks auth token` only supports U2M (user-to-machine) authentication tokens. " +
573+
"To authenticate as a service principal, use the Databricks SDK directly",
574+
},
529575
{
530576
name: "no args, DATABRICKS_HOST env resolves",
531577
setupCtx: func(ctx context.Context) context.Context {

libs/databrickscfg/profile/file.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,14 @@ func (f FileProfilerImpl) LoadProfiles(ctx context.Context, fn ProfileMatchFunct
7979
continue
8080
}
8181
profile := Profile{
82-
Name: v.Name(),
83-
Host: host,
84-
AccountID: all["account_id"],
85-
WorkspaceID: all["workspace_id"],
86-
IsUnifiedHost: all["experimental_is_unified_host"] == "true",
87-
ClusterID: all["cluster_id"],
88-
ServerlessComputeID: all["serverless_compute_id"],
82+
Name: v.Name(),
83+
Host: host,
84+
AccountID: all["account_id"],
85+
WorkspaceID: all["workspace_id"],
86+
IsUnifiedHost: all["experimental_is_unified_host"] == "true",
87+
ClusterID: all["cluster_id"],
88+
ServerlessComputeID: all["serverless_compute_id"],
89+
HasClientCredentials: all["client_id"] != "" && all["client_secret"] != "",
8990
}
9091
if fn(profile) {
9192
profiles = append(profiles, profile)

libs/databrickscfg/profile/profile.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import (
1010
// It should only be used for prompting and filtering.
1111
// Use its name to construct a config.Config.
1212
type Profile struct {
13-
Name string
14-
Host string
15-
AccountID string
16-
WorkspaceID string
17-
IsUnifiedHost bool
18-
ClusterID string
19-
ServerlessComputeID string
13+
Name string
14+
Host string
15+
AccountID string
16+
WorkspaceID string
17+
IsUnifiedHost bool
18+
ClusterID string
19+
ServerlessComputeID string
20+
HasClientCredentials bool
2021
}
2122

2223
func (p Profile) Cloud() string {

0 commit comments

Comments
 (0)