diff --git a/cmd/auth/token.go b/cmd/auth/token.go index f0f2e0a765..de30fb2785 100644 --- a/cmd/auth/token.go +++ b/cmd/auth/token.go @@ -199,11 +199,27 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) { return nil, err } args.profileName = selected + existingProfile, err = loadProfileByName(ctx, selected, args.profiler) + if err != nil { + return nil, err + } } else if len(matchingProfiles) == 1 { args.profileName = matchingProfiles[0].Name + existingProfile = &matchingProfiles[0] } } + // Check if the resolved profile uses M2M authentication (client credentials). + // The auth token command only supports U2M OAuth tokens. + if existingProfile != nil && existingProfile.HasClientCredentials { + return nil, fmt.Errorf( + "profile %q 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", + args.profileName, + ) + } + args.authArguments.Profile = args.profileName ctx, cancel := context.WithTimeout(ctx, args.tokenTimeout) diff --git a/cmd/auth/token_test.go b/cmd/auth/token_test.go index 3ec29fd515..8becf5f43c 100644 --- a/cmd/auth/token_test.go +++ b/cmd/auth/token_test.go @@ -125,6 +125,11 @@ func TestToken_loadToken(t *testing.T) { Name: "legacy-ws", Host: "https://legacy-ws.cloud.databricks.com", }, + { + Name: "m2m-profile", + Host: "https://m2m.cloud.databricks.com", + HasClientCredentials: true, + }, }, } tokenCache := &inMemoryTokenCache{ @@ -526,6 +531,47 @@ func TestToken_loadToken(t *testing.T) { }, wantErr: "no profiles configured. Run 'databricks auth login' to create a profile", }, + { + name: "M2M profile returns clear error", + args: loadTokenArgs{ + authArguments: &auth.AuthArguments{}, + profileName: "m2m-profile", + args: []string{}, + tokenTimeout: 1 * time.Hour, + profiler: profiler, + }, + wantErr: `profile "m2m-profile" 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", + }, + { + name: "M2M profile detected via positional arg", + args: loadTokenArgs{ + authArguments: &auth.AuthArguments{}, + profileName: "", + args: []string{"m2m-profile"}, + tokenTimeout: 1 * time.Hour, + profiler: profiler, + }, + wantErr: `profile "m2m-profile" 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", + }, + { + name: "M2M profile detected via host resolution", + args: loadTokenArgs{ + authArguments: &auth.AuthArguments{ + Host: "https://m2m.cloud.databricks.com", + }, + profileName: "", + args: []string{}, + tokenTimeout: 1 * time.Hour, + profiler: profiler, + }, + wantErr: `profile "m2m-profile" 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", + }, { name: "no args, DATABRICKS_HOST env resolves", setupCtx: func(ctx context.Context) context.Context { diff --git a/libs/databrickscfg/profile/file.go b/libs/databrickscfg/profile/file.go index 7661baf956..b078e67d3a 100644 --- a/libs/databrickscfg/profile/file.go +++ b/libs/databrickscfg/profile/file.go @@ -79,13 +79,14 @@ func (f FileProfilerImpl) LoadProfiles(ctx context.Context, fn ProfileMatchFunct continue } profile := Profile{ - Name: v.Name(), - Host: host, - AccountID: all["account_id"], - WorkspaceID: all["workspace_id"], - IsUnifiedHost: all["experimental_is_unified_host"] == "true", - ClusterID: all["cluster_id"], - ServerlessComputeID: all["serverless_compute_id"], + Name: v.Name(), + Host: host, + AccountID: all["account_id"], + WorkspaceID: all["workspace_id"], + IsUnifiedHost: all["experimental_is_unified_host"] == "true", + ClusterID: all["cluster_id"], + ServerlessComputeID: all["serverless_compute_id"], + HasClientCredentials: all["client_id"] != "" && all["client_secret"] != "", } if fn(profile) { profiles = append(profiles, profile) diff --git a/libs/databrickscfg/profile/profile.go b/libs/databrickscfg/profile/profile.go index 0358b8f7ec..9cdf24f82b 100644 --- a/libs/databrickscfg/profile/profile.go +++ b/libs/databrickscfg/profile/profile.go @@ -10,13 +10,14 @@ import ( // It should only be used for prompting and filtering. // Use its name to construct a config.Config. type Profile struct { - Name string - Host string - AccountID string - WorkspaceID string - IsUnifiedHost bool - ClusterID string - ServerlessComputeID string + Name string + Host string + AccountID string + WorkspaceID string + IsUnifiedHost bool + ClusterID string + ServerlessComputeID string + HasClientCredentials bool } func (p Profile) Cloud() string {