Skip to content

Commit d71fa7e

Browse files
committed
Merge branch 'main' into databricks-wif
2 parents fc39a52 + 55cc9ed commit d71fa7e

File tree

120 files changed

+4107
-283
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+4107
-283
lines changed

.codegen/_openapi_sha

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
31b3fea21dbe5a3a652937691602eb66d6dba30b
1+
05692f4dcf168be190bb7bcda725ee8b368b7ae3

.gitattributes

Lines changed: 40 additions & 0 deletions
Large diffs are not rendered by default.

.release_metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"timestamp": "2025-03-26 13:43:47+0000"
2+
"timestamp": "2025-04-14 14:27:28+0000"
33
}

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
# Version changelog
22

3+
## Release v0.45.0
4+
5+
### API Changes
6+
* Added `workspaceClient.enableExportNotebook()` service, `workspaceClient.enableNotebookTableClipboard()` service and `workspaceClient.enableResultsDownloading()` service.
7+
* Added `getCredentialsForTraceDataDownload()` and `getCredentialsForTraceDataUpload()` methods for `workspaceClient.experiments()` service.
8+
* Added `getDownloadFullQueryResult()` method for `workspaceClient.genie()` service.
9+
* Added `getPublishedDashboardTokenInfo()` method for `workspaceClient.lakeviewEmbedded()` service.
10+
* Added `bindingWorkspaceIds` field for `com.databricks.sdk.service.billing.BudgetPolicy`.
11+
* Added `downloadId` field for `com.databricks.sdk.service.dashboards.GenieGenerateDownloadFullQueryResultResponse`.
12+
* Added `dashboardOutput` field for `com.databricks.sdk.service.jobs.RunOutput`.
13+
* Added `dashboardTask` and `powerBiTask` fields for `com.databricks.sdk.service.jobs.RunTask`.
14+
* Added `dashboardTask` and `powerBiTask` fields for `com.databricks.sdk.service.jobs.SubmitTask`.
15+
* Added `dashboardTask` and `powerBiTask` fields for `com.databricks.sdk.service.jobs.Task`.
16+
* Added `includeFeatures` field for `com.databricks.sdk.service.ml.CreateForecastingExperimentRequest`.
17+
* Added `models` field for `com.databricks.sdk.service.ml.LogInputs`.
18+
* Added `datasetDigest`, `datasetName` and `modelId` fields for `com.databricks.sdk.service.ml.LogMetric`.
19+
* Added `datasetDigest`, `datasetName`, `modelId` and `runId` fields for `com.databricks.sdk.service.ml.Metric`.
20+
* Added `modelInputs` field for `com.databricks.sdk.service.ml.RunInputs`.
21+
* Added `clientApplication` field for `com.databricks.sdk.service.sql.QueryInfo`.
22+
* Added `GEOGRAPHY` and `GEOMETRY` enum values for `com.databricks.sdk.service.catalog.ColumnTypeName`.
23+
* Added `ALLOCATION_TIMEOUT_NO_HEALTHY_AND_WARMED_UP_CLUSTERS`, `DOCKER_CONTAINER_CREATION_EXCEPTION`, `DOCKER_IMAGE_TOO_LARGE_FOR_INSTANCE_EXCEPTION` and `DOCKER_INVALID_OS_EXCEPTION` enum values for `com.databricks.sdk.service.compute.TerminationReasonCode`.
24+
* Added `STANDARD` enum value for `com.databricks.sdk.service.jobs.PerformanceTarget`.
25+
* Added `CAN_VIEW` enum value for `com.databricks.sdk.service.sql.WarehousePermissionLevel`.
26+
* [Breaking] Changed `generateDownloadFullQueryResult()` method for `workspaceClient.genie()` service . Method path has changed.
27+
* [Breaking] Changed waiter for `workspaceClient.commandExecution().create()` method.
28+
* [Breaking] Changed waiter for `workspaceClient.commandExecution().execute()` method.
29+
* [Breaking] Removed `error`, `status` and `transientStatementId` fields for `com.databricks.sdk.service.dashboards.GenieGenerateDownloadFullQueryResultResponse`.
30+
* [Breaking] Removed `BALANCED` and `COST_OPTIMIZED` enum values for `com.databricks.sdk.service.jobs.PerformanceTarget`.
31+
* [Breaking] Removed `workspaceClient.pipelines().waitGetPipelineRunning()` method.
32+
33+
34+
## Release v0.44.0
35+
36+
### Bug Fixes
37+
* Fix issue deserializing HTTP responses with an empty body ([#426](https://github.com/databricks/databricks-sdk-java/pull/426)).
38+
39+
340
## Release v0.43.0
441

542
### API Changes

NEXT_CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# NEXT CHANGELOG
22

3-
## Release v0.44.0
3+
## Release v0.46.0
44

55
### New Features and Improvements
6+
* Added `TokenCache` to `ExternalBrowserCredentialsProvider` to reduce number of authentications needed for U2M OAuth.
7+
68
* Introduce support for Databricks Workload Identity Federation in GitHub workflows ([423](https://github.com/databricks/databricks-sdk-java/pull/423)).
79
See README.md for instructions.
810
* [Breaking] Users running their workflows in GitHub Actions, which use Cloud native authentication and also have a `DATABRICKS_CLIENT_ID` and `DATABRICKS_HOST`
911
environment variables set may see their authentication start failing due to the order in which the SDK tries different authentication methods.
1012

1113
### Bug Fixes
12-
* Fix issue deserializing HTTP responses with an empty body ([#426](https://github.com/databricks/databricks-sdk-java/pull/426)).
1314

1415
### Documentation
1516

databricks-sdk-java/pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>com.databricks</groupId>
77
<artifactId>databricks-sdk-parent</artifactId>
8-
<version>0.43.0</version>
8+
<version>0.45.0</version>
99
</parent>
1010
<artifactId>databricks-sdk-java</artifactId>
1111
<properties>
@@ -97,5 +97,11 @@
9797
<artifactId>google-auth-library-oauth2-http</artifactId>
9898
<version>1.20.0</version>
9999
</dependency>
100+
<!-- Jackson JSR310 module needed to serialize/deserialize java.time classes in TokenCache -->
101+
<dependency>
102+
<groupId>com.fasterxml.jackson.datatype</groupId>
103+
<artifactId>jackson-datatype-jsr310</artifactId>
104+
<version>${jackson.version}</version>
105+
</dependency>
100106
</dependencies>
101107
</project>

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,4 +685,14 @@ public DatabricksConfig newWithWorkspaceHost(String host) {
685685
"headerFactory"));
686686
return clone(fieldsToSkip).setHost(host);
687687
}
688+
689+
/**
690+
* Gets the default OAuth redirect URL. If one is not provided explicitly, uses
691+
* http://localhost:8080/callback
692+
*
693+
* @return The OAuth redirect URL to use
694+
*/
695+
public String getEffectiveOAuthRedirectUrl() {
696+
return redirectUrl != null ? redirectUrl : "http://localhost:8080/callback";
697+
}
688698
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public String getValue() {
3636
// TODO: check if reading from
3737
// /META-INF/maven/com.databricks/databrics-sdk-java/pom.properties
3838
// or getClass().getPackage().getImplementationVersion() is enough.
39-
private static final String version = "0.43.0";
39+
private static final String version = "0.45.0";
4040

4141
public static void withProduct(String product, String productVersion) {
4242
UserAgent.product = product;

databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/ExternalBrowserCredentialsProvider.java

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,38 @@
55
import com.databricks.sdk.core.DatabricksException;
66
import com.databricks.sdk.core.HeaderFactory;
77
import java.io.IOException;
8+
import java.nio.file.Path;
9+
import java.util.Objects;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
812

913
/**
1014
* A {@code CredentialsProvider} which implements the Authorization Code + PKCE flow by opening a
11-
* browser for the user to authorize the application.
15+
* browser for the user to authorize the application. Uses a specified TokenCache or creates a
16+
* default one if none is provided.
1217
*/
1318
public class ExternalBrowserCredentialsProvider implements CredentialsProvider {
19+
private static final Logger LOGGER =
20+
LoggerFactory.getLogger(ExternalBrowserCredentialsProvider.class);
21+
22+
private TokenCache tokenCache;
23+
24+
/**
25+
* Creates a new ExternalBrowserCredentialsProvider with the specified TokenCache.
26+
*
27+
* @param tokenCache the TokenCache to use for caching tokens
28+
*/
29+
public ExternalBrowserCredentialsProvider(TokenCache tokenCache) {
30+
this.tokenCache = tokenCache;
31+
}
32+
33+
/**
34+
* Creates a new ExternalBrowserCredentialsProvider with a default TokenCache. A FileTokenCache
35+
* will be created when credentials are configured.
36+
*/
37+
public ExternalBrowserCredentialsProvider() {
38+
this(null);
39+
}
1440

1541
@Override
1642
public String authType() {
@@ -19,16 +45,87 @@ public String authType() {
1945

2046
@Override
2147
public HeaderFactory configure(DatabricksConfig config) {
22-
if (config.getHost() == null || config.getAuthType() != "external-browser") {
48+
if (config.getHost() == null || !Objects.equals(config.getAuthType(), "external-browser")) {
2349
return null;
2450
}
51+
52+
// Use the utility class to resolve client ID and client secret
53+
String clientId = OAuthClientUtils.resolveClientId(config);
54+
String clientSecret = OAuthClientUtils.resolveClientSecret(config);
55+
2556
try {
26-
OAuthClient client = new OAuthClient(config);
27-
Consent consent = client.initiateConsent();
28-
SessionCredentials creds = consent.launchExternalBrowser();
29-
return creds.configure(config);
57+
if (tokenCache == null) {
58+
// Create a default FileTokenCache based on config
59+
Path cachePath =
60+
TokenCacheUtils.getCacheFilePath(config.getHost(), clientId, config.getScopes());
61+
tokenCache = new FileTokenCache(cachePath);
62+
}
63+
64+
// First try to use the cached token if available (will return null if disabled)
65+
Token cachedToken = tokenCache.load();
66+
if (cachedToken != null && cachedToken.getRefreshToken() != null) {
67+
LOGGER.debug("Found cached token for {}:{}", config.getHost(), clientId);
68+
69+
try {
70+
// Create SessionCredentials with the cached token and try to refresh if needed
71+
SessionCredentials cachedCreds =
72+
new SessionCredentials.Builder()
73+
.withToken(cachedToken)
74+
.withHttpClient(config.getHttpClient())
75+
.withClientId(clientId)
76+
.withClientSecret(clientSecret)
77+
.withTokenUrl(config.getOidcEndpoints().getTokenEndpoint())
78+
.withRedirectUrl(config.getEffectiveOAuthRedirectUrl())
79+
.withTokenCache(tokenCache)
80+
.build();
81+
82+
LOGGER.debug("Using cached token, will immediately refresh");
83+
cachedCreds.token = cachedCreds.refresh();
84+
return cachedCreds.configure(config);
85+
} catch (Exception e) {
86+
// If token refresh fails, log and continue to browser auth
87+
LOGGER.info("Token refresh failed: {}, falling back to browser auth", e.getMessage());
88+
}
89+
}
90+
91+
// If no cached token or refresh failed, perform browser auth
92+
SessionCredentials credentials =
93+
performBrowserAuth(config, clientId, clientSecret, tokenCache);
94+
tokenCache.save(credentials.getToken());
95+
return credentials.configure(config);
3096
} catch (IOException | DatabricksException e) {
97+
LOGGER.error("Failed to authenticate: {}", e.getMessage());
3198
return null;
3299
}
33100
}
101+
102+
SessionCredentials performBrowserAuth(
103+
DatabricksConfig config, String clientId, String clientSecret, TokenCache tokenCache)
104+
throws IOException {
105+
LOGGER.debug("Performing browser authentication");
106+
OAuthClient client =
107+
new OAuthClient.Builder()
108+
.withHttpClient(config.getHttpClient())
109+
.withClientId(clientId)
110+
.withClientSecret(clientSecret)
111+
.withHost(config.getHost())
112+
.withRedirectUrl(config.getEffectiveOAuthRedirectUrl())
113+
.withScopes(config.getScopes())
114+
.build();
115+
Consent consent = client.initiateConsent();
116+
117+
// Use the existing browser flow to get credentials
118+
SessionCredentials credentials = consent.launchExternalBrowser();
119+
120+
// Create a new SessionCredentials with the same token but with our token cache
121+
return new SessionCredentials.Builder()
122+
.withToken(credentials.getToken())
123+
.withHttpClient(config.getHttpClient())
124+
.withClientId(config.getClientId())
125+
.withClientSecret(config.getClientSecret())
126+
.withTokenUrl(config.getOidcEndpoints().getTokenEndpoint())
127+
.withRedirectUrl(config.getEffectiveOAuthRedirectUrl())
128+
.withTokenCache(tokenCache)
129+
.build();
130+
}
34131
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.databricks.sdk.core.oauth;
2+
3+
import com.databricks.sdk.core.utils.SerDeUtils;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import java.io.File;
6+
import java.nio.charset.StandardCharsets;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.util.Objects;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
13+
/** A TokenCache implementation that stores tokens as plain files. */
14+
public class FileTokenCache implements TokenCache {
15+
private static final Logger LOGGER = LoggerFactory.getLogger(FileTokenCache.class);
16+
17+
private final Path cacheFile;
18+
private final ObjectMapper mapper;
19+
20+
/**
21+
* Constructs a new SimpleFileTokenCache instance.
22+
*
23+
* @param cacheFilePath The path where the token cache will be stored
24+
*/
25+
public FileTokenCache(Path cacheFilePath) {
26+
Objects.requireNonNull(cacheFilePath, "cacheFilePath must be defined");
27+
28+
this.cacheFile = cacheFilePath;
29+
this.mapper = SerDeUtils.createMapper();
30+
}
31+
32+
@Override
33+
public void save(Token token) {
34+
try {
35+
Files.createDirectories(cacheFile.getParent());
36+
37+
// Serialize token to JSON
38+
String json = mapper.writeValueAsString(token);
39+
byte[] dataToWrite = json.getBytes(StandardCharsets.UTF_8);
40+
41+
Files.write(cacheFile, dataToWrite);
42+
// Set file permissions to be readable only by the owner (equivalent to 0600)
43+
File file = cacheFile.toFile();
44+
file.setReadable(false, false);
45+
file.setReadable(true, true);
46+
file.setWritable(false, false);
47+
file.setWritable(true, true);
48+
49+
LOGGER.debug("Successfully saved token to cache: {}", cacheFile);
50+
} catch (Exception e) {
51+
LOGGER.warn("Failed to save token to cache: {}", cacheFile, e);
52+
}
53+
}
54+
55+
@Override
56+
public Token load() {
57+
try {
58+
if (!Files.exists(cacheFile)) {
59+
LOGGER.debug("No token cache file found at: {}", cacheFile);
60+
return null;
61+
}
62+
63+
byte[] fileContent = Files.readAllBytes(cacheFile);
64+
65+
// Deserialize token from JSON
66+
String json = new String(fileContent, StandardCharsets.UTF_8);
67+
Token token = mapper.readValue(json, Token.class);
68+
LOGGER.debug("Successfully loaded token from cache: {}", cacheFile);
69+
return token;
70+
} catch (Exception e) {
71+
// If there's any issue loading the token, return null
72+
// to allow a fresh token to be obtained
73+
LOGGER.warn("Failed to load token from cache: {}", e.getMessage());
74+
return null;
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)