Skip to content

Commit 1cb43a3

Browse files
fixes / improvements
1 parent dd6ee4e commit 1cb43a3

3 files changed

Lines changed: 57 additions & 38 deletions

File tree

databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksCliCredentialsProvider.java

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import com.databricks.sdk.core.oauth.CachedTokenSource;
44
import com.databricks.sdk.core.oauth.OAuthHeaderFactory;
55
import com.databricks.sdk.core.oauth.Token;
6+
import com.databricks.sdk.core.oauth.TokenSource;
67
import com.databricks.sdk.core.utils.OSUtils;
78
import com.databricks.sdk.support.InternalApi;
9+
import com.fasterxml.jackson.core.JsonProcessingException;
810
import com.fasterxml.jackson.databind.ObjectMapper;
911
import java.nio.charset.StandardCharsets;
1012
import java.util.*;
@@ -20,6 +22,13 @@ public class DatabricksCliCredentialsProvider implements CredentialsProvider {
2022

2123
private static final ObjectMapper MAPPER = new ObjectMapper();
2224

25+
/** Thrown when the cached CLI token's scopes don't match the SDK's configured scopes. */
26+
static class ScopeMismatchException extends DatabricksException {
27+
ScopeMismatchException(String message) {
28+
super(message);
29+
}
30+
}
31+
2332
/**
2433
* offline_access controls whether the IdP issues a refresh token. It does not grant any API
2534
* permissions, so its presence or absence should not cause a scope mismatch error.
@@ -104,18 +113,38 @@ public OAuthHeaderFactory configure(DatabricksConfig config) {
104113
return null;
105114
}
106115

116+
// Wrap the token source with scope validation so that every token — both the
117+
// initial fetch and subsequent refreshes — is checked against the configured scopes.
118+
TokenSource effectiveSource;
119+
if (config.isScopesExplicitlySet()) {
120+
List<String> scopes = config.getScopes();
121+
effectiveSource =
122+
() -> {
123+
Token t = tokenSource.getToken();
124+
validateTokenScopes(t, scopes, host);
125+
return t;
126+
};
127+
} else {
128+
effectiveSource = tokenSource;
129+
}
130+
107131
CachedTokenSource cachedTokenSource =
108-
new CachedTokenSource.Builder(tokenSource)
132+
new CachedTokenSource.Builder(effectiveSource)
109133
.setAsyncDisabled(config.getDisableAsyncTokenRefresh())
110134
.build();
111-
Token token =
112-
cachedTokenSource.getToken(); // We need this for checking if databricks CLI is installed.
113-
114-
if (config.isScopesExplicitlySet()) {
115-
validateTokenScopes(token, config.getScopes(), config.getHost());
116-
}
135+
cachedTokenSource.getToken(); // We need this for checking if databricks CLI is installed.
117136

118137
return OAuthHeaderFactory.fromTokenSource(cachedTokenSource);
138+
} catch (ScopeMismatchException e) {
139+
// Scope validation failed. When the user explicitly selected databricks-cli auth,
140+
// surface the mismatch immediately so they get an actionable error. When we're being
141+
// tried as part of the default credential chain, step aside so other providers get
142+
// a chance.
143+
if (DATABRICKS_CLI.equals(config.getAuthType())) {
144+
throw e;
145+
}
146+
LOG.warn("Databricks CLI token scope mismatch, skipping: {}", e.getMessage());
147+
return null;
119148
} catch (DatabricksException e) {
120149
String stderr = e.getMessage();
121150
if (stderr.contains("not found")) {
@@ -126,17 +155,6 @@ public OAuthHeaderFactory configure(DatabricksConfig config) {
126155
LOG.info("OAuth not configured or not available");
127156
return null;
128157
}
129-
// Scope validation failed. When the user explicitly selected databricks-cli auth,
130-
// surface the mismatch immediately so they get an actionable error. When we're being
131-
// tried as part of the default credential chain, step aside so other providers get
132-
// a chance.
133-
if (stderr.contains("do not match the configured scopes")) {
134-
if (DATABRICKS_CLI.equals(config.getAuthType())) {
135-
throw e;
136-
}
137-
LOG.warn("Databricks CLI token scope mismatch, skipping: {}", e.getMessage());
138-
return null;
139-
}
140158
throw e;
141159
}
142160
}
@@ -179,15 +197,13 @@ static void validateTokenScopes(Token token, List<String> requestedScopes, Strin
179197
List<String> sortedRequested = new ArrayList<>(requested);
180198
Collections.sort(sortedRequested);
181199

182-
// Build a re-auth command hint with scopes (excluding offline_access)
183-
String scopesArg = String.join(",", sortedRequested);
184-
185-
throw new DatabricksException(
200+
throw new ScopeMismatchException(
186201
String.format(
187202
"Token issued by Databricks CLI has scopes %s which do not match "
188-
+ "the configured scopes %s. Please re-authenticate with the desired scopes "
189-
+ "by running `databricks auth login --host %s --scopes %s`.",
190-
sortedTokenScopes, sortedRequested, host, scopesArg));
203+
+ "the configured scopes %s. Please re-authenticate "
204+
+ "with the desired scopes by running `databricks auth login` with the --scopes flag."
205+
+ "Scopes default to all-apis.",
206+
sortedTokenScopes, sortedRequested));
191207
}
192208
}
193209

@@ -196,18 +212,18 @@ static void validateTokenScopes(Token token, List<String> requestedScopes, Strin
196212
* valid JWT.
197213
*/
198214
private static Map<String, Object> getJwtClaims(String accessToken) {
215+
String[] parts = accessToken.split("\\.");
216+
if (parts.length != 3) {
217+
LOG.debug("Tried to decode access token as JWT, but failed: {} components", parts.length);
218+
return null;
219+
}
199220
try {
200-
String[] parts = accessToken.split("\\.");
201-
if (parts.length != 3) {
202-
LOG.debug("Tried to decode access token as JWT, but failed: {} components", parts.length);
203-
return null;
204-
}
205221
byte[] payloadBytes = Base64.getUrlDecoder().decode(parts[1]);
206222
String payloadJson = new String(payloadBytes, StandardCharsets.UTF_8);
207223
@SuppressWarnings("unchecked")
208224
Map<String, Object> claims = MAPPER.readValue(payloadJson, Map.class);
209225
return claims;
210-
} catch (Exception e) {
226+
} catch (IllegalArgumentException | JsonProcessingException e) {
211227
LOG.debug("Failed to decode JWT claims: {}", e.getMessage());
212228
return null;
213229
}

databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -435,11 +435,11 @@ public DatabricksConfig setScopes(List<String> scopes) {
435435
}
436436

437437
/**
438-
* Returns true if scopes were explicitly configured (either directly in code or loaded from a CLI
439-
* profile/config file). When scopes are not set, getScopes() defaults to ["all-apis"], which
438+
* Returns true if scopes were explicitly configured (either directly in code or loaded from a
439+
* config file). When scopes are not set, getScopes() defaults to ["all-apis"], which
440440
* would cause false-positive mismatches during scope validation.
441441
*/
442-
public boolean isScopesExplicitlySet() {
442+
boolean isScopesExplicitlySet() {
443443
return scopes != null && !scopes.isEmpty();
444444
}
445445

databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksCliScopeValidationTest.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ void testScopeValidation(
8585

8686
if (expectError) {
8787
assertThrows(
88-
DatabricksException.class,
88+
DatabricksCliCredentialsProvider.ScopeMismatchException.class,
8989
() ->
9090
DatabricksCliCredentialsProvider.validateTokenScopes(token, configuredScopes, HOST));
9191
} else {
@@ -116,14 +116,17 @@ void testNonJwtTokenSkipsValidation() {
116116
@Test
117117
void testErrorMessageContainsReauthCommand() {
118118
Token token = makeToken(Collections.singletonMap("scope", "all-apis"));
119-
DatabricksException e =
119+
DatabricksCliCredentialsProvider.ScopeMismatchException e =
120120
assertThrows(
121-
DatabricksException.class,
121+
DatabricksCliCredentialsProvider.ScopeMismatchException.class,
122122
() ->
123123
DatabricksCliCredentialsProvider.validateTokenScopes(
124124
token, Arrays.asList("sql", "offline_access"), HOST));
125125
assertTrue(
126-
e.getMessage().contains("databricks auth login --host " + HOST + " --scopes sql"),
126+
e.getMessage().contains("databricks auth login"),
127127
"Expected re-auth command in error message, got: " + e.getMessage());
128+
assertTrue(
129+
e.getMessage().contains("do not match the configured scopes"),
130+
"Expected scope mismatch details in error message, got: " + e.getMessage());
128131
}
129132
}

0 commit comments

Comments
 (0)