Skip to content

Commit e75b8e4

Browse files
committed
Use host metadata to determine GCP SA token requirement
Port of Python SDK PR #1322. Add requiresGcpSaAccessToken() which checks host metadata to determine if a GCP SA access token is needed. For workspace hosts (metadata has workspace_id), the SA token is skipped. For account hosts or when metadata is unavailable, falls back to checking AccountID. GoogleDefaultCredentials and GoogleCredentials now use this metadata-based decision instead of always attempting SA token creation. Co-authored-by: Isaac
1 parent ac75934 commit e75b8e4

4 files changed

Lines changed: 142 additions & 5 deletions

File tree

config/auth_gcp_google_credentials.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,21 @@ func (c GoogleCredentials) Configure(ctx context.Context, cfg *Config) (credenti
3434
if err != nil {
3535
return nil, fmt.Errorf("could not obtain OIDC token from JSON: %w", err)
3636
}
37-
// Obtain token source for creating Google Cloud Platform token.
37+
opts := cacheOptions(cfg)
38+
if !requiresGcpSaAccessToken(ctx, cfg) {
39+
logger.Infof(ctx, "Using Google Credentials for Workspace")
40+
visitor := refreshableVisitor(inner, opts...)
41+
return credentials.CredentialsProviderFn(visitor), nil
42+
}
43+
// Account-level host: obtain token source for creating Google Cloud Platform token.
3844
creds, err := google.CredentialsFromJSON(ctx, json,
3945
"https://www.googleapis.com/auth/cloud-platform",
4046
"https://www.googleapis.com/auth/compute")
4147
if err != nil {
4248
return nil, fmt.Errorf("could not obtain OAuth2 token from JSON: %w", err)
4349
}
44-
logger.Infof(ctx, "Using Google Credentials")
45-
visitor := serviceToServiceVisitorWithFallback(inner, creds.TokenSource, "X-Databricks-GCP-SA-Access-Token", cacheOptions(cfg)...)
50+
logger.Infof(ctx, "Using Google Credentials for Account")
51+
visitor := serviceToServiceVisitorWithFallback(inner, creds.TokenSource, "X-Databricks-GCP-SA-Access-Token", opts...)
4652
return credentials.NewOAuthCredentialsProvider(visitor, inner.Token), nil
4753
}
4854

config/auth_gcp_google_id.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ func (c GoogleDefaultCredentials) Configure(ctx context.Context, cfg *Config) (c
2929
return nil, err
3030
}
3131
opts := cacheOptions(cfg)
32-
// Always attempt to create SA token source for the secondary header.
32+
if !requiresGcpSaAccessToken(ctx, cfg) {
33+
logger.Infof(ctx, "Using Google Default Application Credentials for Workspace")
34+
visitor := refreshableVisitor(inner, opts...)
35+
return credentials.CredentialsProviderFn(visitor), nil
36+
}
37+
// Account-level host: attempt to create SA token source for the secondary header.
3338
// If it fails, fall back to refreshableVisitor with a warning.
3439
platform, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
3540
TargetPrincipal: cfg.GoogleServiceAccount,
@@ -43,7 +48,7 @@ func (c GoogleDefaultCredentials) Configure(ctx context.Context, cfg *Config) (c
4348
visitor := refreshableVisitor(inner, opts...)
4449
return credentials.CredentialsProviderFn(visitor), nil
4550
}
46-
logger.Infof(ctx, "Using Google Default Application Credentials")
51+
logger.Infof(ctx, "Using Google Default Application Credentials for Account")
4752
visitor := serviceToServiceVisitorWithFallback(inner, platform, "X-Databricks-GCP-SA-Access-Token", opts...)
4853
return credentials.NewOAuthCredentialsProvider(visitor, inner.Token), nil
4954
}

config/auth_gcp_helpers.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package config
2+
3+
import (
4+
"context"
5+
6+
"github.com/databricks/databricks-sdk-go/logger"
7+
)
8+
9+
// requiresGcpSaAccessToken determines whether a GCP SA access token is needed
10+
// for the X-Databricks-GCP-SA-Access-Token header. It uses host metadata to
11+
// determine if the host is an account-level host (no workspace_id), and falls
12+
// back to checking AccountID when metadata is unavailable.
13+
func requiresGcpSaAccessToken(ctx context.Context, cfg *Config) bool {
14+
if cfg.Host == "" {
15+
return cfg.AccountID != ""
16+
}
17+
if err := cfg.EnsureResolved(); err != nil {
18+
return cfg.AccountID != ""
19+
}
20+
meta, err := getHostMetadata(ctx, cfg.CanonicalHostName(), cfg.refreshClient)
21+
if err != nil {
22+
logger.Debugf(ctx, "Failed to fetch host metadata for GCP SA check: %v", err)
23+
return cfg.AccountID != ""
24+
}
25+
return meta.WorkspaceID == ""
26+
}

config/auth_gcp_helpers_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/databricks/databricks-sdk-go/httpclient/fixtures"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestRequiresGcpSaAccessToken_WorkspaceFromMetadata(t *testing.T) {
12+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
13+
cfg := &Config{
14+
Host: testHMHost,
15+
Loaders: []Loader{noopLoader},
16+
HTTPTransport: fixtures.SliceTransport{
17+
{
18+
Method: "GET",
19+
Resource: "/.well-known/databricks-config",
20+
ReuseRequest: true,
21+
Status: 200,
22+
Response: `{"oidc_endpoint": "` + testHMHost + `/oidc", "account_id": "` + testHMAccountID + `", "workspace_id": "` + testHMWorkspaceID + `"}`,
23+
},
24+
},
25+
}
26+
result := requiresGcpSaAccessToken(context.Background(), cfg)
27+
assert.False(t, result, "workspace host should not require GCP SA token")
28+
}
29+
30+
func TestRequiresGcpSaAccessToken_AccountFromMetadata(t *testing.T) {
31+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
32+
cfg := &Config{
33+
Host: testHMHost,
34+
Loaders: []Loader{noopLoader},
35+
HTTPTransport: fixtures.SliceTransport{
36+
{
37+
Method: "GET",
38+
Resource: "/.well-known/databricks-config",
39+
ReuseRequest: true,
40+
Status: 200,
41+
Response: `{"oidc_endpoint": "` + testHMHost + `/oidc", "account_id": "` + testHMAccountID + `"}`,
42+
},
43+
},
44+
}
45+
result := requiresGcpSaAccessToken(context.Background(), cfg)
46+
assert.True(t, result, "account host (no workspace_id) should require GCP SA token")
47+
}
48+
49+
func TestRequiresGcpSaAccessToken_MetadataError_FallsBackToAccountID(t *testing.T) {
50+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
51+
cfg := &Config{
52+
Host: testHMHost,
53+
AccountID: testHMAccountID,
54+
Loaders: []Loader{noopLoader},
55+
HTTPTransport: fixtures.SliceTransport{
56+
{
57+
Method: "GET",
58+
Resource: "/.well-known/databricks-config",
59+
ReuseRequest: true,
60+
Status: 500,
61+
Response: `{"error": "internal error"}`,
62+
},
63+
},
64+
}
65+
result := requiresGcpSaAccessToken(context.Background(), cfg)
66+
assert.True(t, result, "with account_id set and metadata error, should require SA token")
67+
}
68+
69+
func TestRequiresGcpSaAccessToken_MetadataError_NoAccountID(t *testing.T) {
70+
noopLoader := mockLoader(func(cfg *Config) error { return nil })
71+
cfg := &Config{
72+
Host: testHMHost,
73+
Loaders: []Loader{noopLoader},
74+
HTTPTransport: fixtures.SliceTransport{
75+
{
76+
Method: "GET",
77+
Resource: "/.well-known/databricks-config",
78+
ReuseRequest: true,
79+
Status: 500,
80+
Response: `{"error": "internal error"}`,
81+
},
82+
},
83+
}
84+
result := requiresGcpSaAccessToken(context.Background(), cfg)
85+
assert.False(t, result, "without account_id and metadata error, should not require SA token")
86+
}
87+
88+
func TestRequiresGcpSaAccessToken_NoHost_WithAccountID(t *testing.T) {
89+
cfg := &Config{
90+
AccountID: testHMAccountID,
91+
}
92+
result := requiresGcpSaAccessToken(context.Background(), cfg)
93+
assert.True(t, result, "no host with account_id should require SA token")
94+
}
95+
96+
func TestRequiresGcpSaAccessToken_NoHost_NoAccountID(t *testing.T) {
97+
cfg := &Config{}
98+
result := requiresGcpSaAccessToken(context.Background(), cfg)
99+
assert.False(t, result, "no host and no account_id should not require SA token")
100+
}

0 commit comments

Comments
 (0)