Skip to content

Commit 3418b86

Browse files
committed
Add host discovery to login.databricks.com flow
The discovery login flow via login.databricks.com now calls runHostDiscovery() on the discovered host to populate account_id, workspace_id, and DiscoveryURL from .well-known/databricks-config. This ensures profiles created via login.databricks.com have the same SPOG metadata as profiles created via the regular --host login path. Previously, the discovery flow relied solely on token introspection for workspace_id and deliberately skipped saving account_id. With the SPOG discovery changes now in place, account_id can be safely saved to profiles. Token introspection remains as a fallback when host metadata discovery is unavailable (e.g. classic workspace hosts). Co-authored-by: Isaac
1 parent bf6dc75 commit 3418b86

File tree

2 files changed

+102
-13
lines changed

2 files changed

+102
-13
lines changed

cmd/auth/login.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -630,24 +630,33 @@ func discoveryLogin(ctx context.Context, dc discoveryClient, profileName string,
630630
return discoveryErr("login succeeded but no workspace host was discovered", nil)
631631
}
632632

633-
// Get the token for introspection
633+
// Run host metadata discovery on the discovered host to detect SPOG hosts
634+
// and populate account_id/workspace_id. This ensures profiles created via
635+
// login.databricks.com have the same metadata as profiles created via the
636+
// regular --host login path.
637+
hostArgs := &auth.AuthArguments{Host: discoveredHost}
638+
runHostDiscovery(ctx, hostArgs)
639+
accountID := hostArgs.AccountID
640+
workspaceID := hostArgs.WorkspaceID
641+
642+
// Best-effort introspection as a fallback for workspace_id when host
643+
// metadata discovery didn't return it (e.g. classic workspace hosts).
634644
tok, err := persistentAuth.Token()
635645
if err != nil {
636646
return fmt.Errorf("retrieving token after login: %w", err)
637647
}
638648

639-
// Best-effort introspection for metadata.
640-
var workspaceID string
641649
introspection, err := dc.IntrospectToken(ctx, discoveredHost, tok.AccessToken)
642650
if err != nil {
643651
log.Debugf(ctx, "token introspection failed (non-fatal): %v", err)
644652
} else {
645-
// TODO: Save introspection.AccountID once the SDKs are ready to use
646-
// account_id as part of the profile/cache key. Adding it now would break
647-
// existing auth flows that don't expect account_id on workspace profiles.
648-
workspaceID = introspection.WorkspaceID
653+
if workspaceID == "" {
654+
workspaceID = introspection.WorkspaceID
655+
}
656+
if accountID == "" {
657+
accountID = introspection.AccountID
658+
}
649659

650-
// Warn if the detected account_id differs from what's already saved in the profile.
651660
if existingProfile != nil && existingProfile.AccountID != "" && introspection.AccountID != "" &&
652661
existingProfile.AccountID != introspection.AccountID {
653662
log.Warnf(ctx, "detected account ID %q differs from existing profile account ID %q",
@@ -658,10 +667,10 @@ func discoveryLogin(ctx context.Context, dc discoveryClient, profileName string,
658667
configFile := env.Get(ctx, "DATABRICKS_CONFIG_FILE")
659668
clearKeys := oauthLoginClearKeys()
660669
// Discovery login always produces a workspace-level profile pointing at the
661-
// discovered host. Any previous routing metadata (account_id, workspace_id,
662-
// is_unified_host, cluster_id, serverless_compute_id) from a prior login to
663-
// a different host type must be cleared so they don't leak into the new
664-
// profile. workspace_id is re-added only when introspection succeeds.
670+
// discovered host. Any previous routing metadata (is_unified_host,
671+
// cluster_id, serverless_compute_id) from a prior login to a different host
672+
// type must be cleared so they don't leak into the new profile. account_id
673+
// and workspace_id are re-added from discovery/introspection results.
665674
clearKeys = append(clearKeys,
666675
"account_id",
667676
"workspace_id",
@@ -673,6 +682,7 @@ func discoveryLogin(ctx context.Context, dc discoveryClient, profileName string,
673682
Profile: profileName,
674683
Host: discoveredHost,
675684
AuthType: authTypeDatabricksCLI,
685+
AccountID: accountID,
676686
WorkspaceID: workspaceID,
677687
Scopes: scopesList,
678688
ConfigFile: configFile,

cmd/auth/login_test.go

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,11 +688,12 @@ func TestDiscoveryLogin_AccountIDMismatchWarning(t *testing.T) {
688688
assert.Contains(t, logBuf.String(), "new-account-id")
689689
assert.Contains(t, logBuf.String(), "old-account-id")
690690

691-
// Verify the profile was saved without account_id (not overwritten).
691+
// Account ID from introspection is now saved to the profile.
692692
savedProfile, err := loadProfileByName(ctx, "DISCOVERY", profile.DefaultProfiler)
693693
require.NoError(t, err)
694694
require.NotNil(t, savedProfile)
695695
assert.Equal(t, "https://workspace.example.com", savedProfile.Host)
696+
assert.Equal(t, "new-account-id", savedProfile.AccountID)
696697
assert.Equal(t, "12345", savedProfile.WorkspaceID)
697698
}
698699

@@ -826,6 +827,83 @@ func TestDiscoveryLogin_ExplicitScopesOverrideExistingProfile(t *testing.T) {
826827
assert.Equal(t, "all-apis", savedProfile.Scopes)
827828
}
828829

830+
func TestDiscoveryLogin_SPOGHostPopulatesAccountIDFromDiscovery(t *testing.T) {
831+
// Start a mock server that returns SPOG discovery metadata.
832+
server := newDiscoveryServer(t, map[string]any{
833+
"account_id": "discovered-account",
834+
"workspace_id": "discovered-ws",
835+
"oidc_endpoint": "https://spog.example.com/oidc/accounts/discovered-account",
836+
})
837+
838+
tmpDir := t.TempDir()
839+
configPath := filepath.Join(tmpDir, ".databrickscfg")
840+
err := os.WriteFile(configPath, []byte(""), 0o600)
841+
require.NoError(t, err)
842+
t.Setenv("DATABRICKS_CONFIG_FILE", configPath)
843+
844+
oauthArg, err := u2m.NewBasicDiscoveryOAuthArgument("DISCOVERY")
845+
require.NoError(t, err)
846+
oauthArg.SetDiscoveredHost(server.URL)
847+
848+
dc := &fakeDiscoveryClient{
849+
oauthArg: oauthArg,
850+
persistentAuth: &fakeDiscoveryPersistentAuth{
851+
token: &oauth2.Token{AccessToken: "test-token"},
852+
},
853+
// Introspection returns different values to verify discovery takes precedence.
854+
introspection: &auth.IntrospectionResult{
855+
AccountID: "introspection-account",
856+
WorkspaceID: "introspection-ws",
857+
},
858+
}
859+
860+
ctx, _ := cmdio.NewTestContextWithStdout(t.Context())
861+
err = discoveryLogin(ctx, dc, "DISCOVERY", time.Second, "", nil, func(string) error { return nil })
862+
require.NoError(t, err)
863+
864+
savedProfile, err := loadProfileByName(ctx, "DISCOVERY", profile.DefaultProfiler)
865+
require.NoError(t, err)
866+
require.NotNil(t, savedProfile)
867+
assert.Equal(t, server.URL, savedProfile.Host)
868+
assert.Equal(t, "discovered-account", savedProfile.AccountID, "account_id should come from host discovery")
869+
assert.Equal(t, "discovered-ws", savedProfile.WorkspaceID, "workspace_id should come from host discovery")
870+
}
871+
872+
func TestDiscoveryLogin_IntrospectionFallsBackWhenDiscoveryFails(t *testing.T) {
873+
tmpDir := t.TempDir()
874+
configPath := filepath.Join(tmpDir, ".databrickscfg")
875+
err := os.WriteFile(configPath, []byte(""), 0o600)
876+
require.NoError(t, err)
877+
t.Setenv("DATABRICKS_CONFIG_FILE", configPath)
878+
879+
// Use a host that won't respond to .well-known/databricks-config.
880+
oauthArg, err := u2m.NewBasicDiscoveryOAuthArgument("DISCOVERY")
881+
require.NoError(t, err)
882+
oauthArg.SetDiscoveredHost("https://workspace.example.com")
883+
884+
dc := &fakeDiscoveryClient{
885+
oauthArg: oauthArg,
886+
persistentAuth: &fakeDiscoveryPersistentAuth{
887+
token: &oauth2.Token{AccessToken: "test-token"},
888+
},
889+
introspection: &auth.IntrospectionResult{
890+
AccountID: "introspection-account",
891+
WorkspaceID: "introspection-ws",
892+
},
893+
}
894+
895+
ctx, _ := cmdio.NewTestContextWithStdout(t.Context())
896+
err = discoveryLogin(ctx, dc, "DISCOVERY", time.Second, "", nil, func(string) error { return nil })
897+
require.NoError(t, err)
898+
899+
savedProfile, err := loadProfileByName(ctx, "DISCOVERY", profile.DefaultProfiler)
900+
require.NoError(t, err)
901+
require.NotNil(t, savedProfile)
902+
assert.Equal(t, "https://workspace.example.com", savedProfile.Host)
903+
assert.Equal(t, "introspection-account", savedProfile.AccountID, "account_id should fall back to introspection")
904+
assert.Equal(t, "introspection-ws", savedProfile.WorkspaceID, "workspace_id should fall back to introspection")
905+
}
906+
829907
func TestDiscoveryLogin_ClearsStaleRoutingFieldsFromUnifiedProfile(t *testing.T) {
830908
tmpDir := t.TempDir()
831909
configPath := filepath.Join(tmpDir, ".databrickscfg")
@@ -921,5 +999,6 @@ auth_type = databricks-cli
921999
require.NoError(t, err)
9221000
require.NotNil(t, savedProfile)
9231001
assert.Equal(t, "https://new-workspace.example.com", savedProfile.Host)
1002+
assert.Equal(t, "fresh-account", savedProfile.AccountID, "account_id should be saved from introspection")
9241003
assert.Equal(t, "222222", savedProfile.WorkspaceID, "workspace_id should be updated to fresh introspection value")
9251004
}

0 commit comments

Comments
 (0)