Skip to content

Commit 86536c6

Browse files
Support GCP auth (#196)
## Changes <!-- Summary of your changes that are easy to understand --> - Make java-sdk support gcp auth ## Tests <!-- How is this tested? --> - Integration tests - Unit tests - Manual tests
1 parent aa83a03 commit 86536c6

File tree

9 files changed

+229
-9
lines changed

9 files changed

+229
-9
lines changed

NOTICE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ This software contains code from the following open source projects, licensed un
3030
x/oauth2 - https://cs.opensource.google/go/x/oauth2/+/master:oauth2.go
3131
Copyright 2014 The Go Authors. All rights reserved.
3232
License - https://cs.opensource.google/go/x/oauth2/+/master:LICENSE
33+
34+
googleapis/google-auth-library-java - https://github.com/googleapis/google-auth-library-java
35+
Copyright 2014, Google inc. All rights reserved.
36+
License - https://github.com/googleapis/google-auth-library-java/blob/main/LICENSE
3337
—--
3438

3539
This Software contains code from the following open source projects, licensed under the MIT license:

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,36 @@ import com.databricks.sdk.core.DatabricksConfig;
173173
WorkspaceClient workspace=new WorkspaceClient(config);
174174
```
175175

176+
### Google Cloud Platform native authentication
177+
178+
By default, the Databricks SDK for Java first tries GCP credentials authentication (`auth_type='google-credentials'`, argument). If the SDK is unsuccessful, it then tries Google Cloud Platform (GCP) ID authentication (`auth_type='google-id'`, argument).
179+
180+
The Databricks SDK for Java picks up an OAuth token in the scope of the Google Default Application Credentials (DAC) flow. This means that if you have run `gcloud auth application-default login` on your development machine, or launch the application on the compute, that is allowed to impersonate the Google Cloud service account specified in `google_service_account`. Authentication should then work out of the box. See [Creating and managing service accounts](https://cloud.google.com/iam/docs/creating-managing-service-accounts).
181+
182+
To authenticate as a Google Cloud service account, you must provide one of the following:
183+
184+
- `host` and `google_credentials`; or their environment variable or `.databrickscfg` file field equivalents.
185+
- `host` and `google_service_account`; or their environment variable or `.databrickscfg` file field equivalents.
186+
187+
| Argument | Description | Environment variable |
188+
|--------------------------|-------------|--------------------------------------------------|
189+
| `google_credentials` | _(String)_ GCP Service Account Credentials JSON or the location of these credentials on the local filesystem. | `GOOGLE_CREDENTIALS` |
190+
| `google_service_account` | _(String)_ The Google Cloud Platform (GCP) service account e-mail used for impersonation in the Default Application Credentials Flow that does not require a password. | `DATABRICKS_GOOGLE_SERVICE_ACCOUNT` |
191+
192+
For example, to use Google ID authentication:
193+
194+
```java
195+
import com.databricks.sdk.WorkspaceClient;
196+
import com.databricks.sdk.core.DatabricksConfig;
197+
...
198+
DatabricksConfig config=new DatabricksConfig()
199+
.setAuthType("google-credentials")
200+
.setHost("https://my-databricks-instance.com")
201+
.setGoogleServiceAccgount("google-service-account");
202+
WorkspaceClient workspace=new WorkspaceClient(config);
203+
204+
```
205+
176206
### Overriding `.databrickscfg`
177207

178208
For [Databricks native authentication](#databricks-native-authentication), you can override the default behavior for using `.databrickscfg` as follows:

databricks-sdk-java/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,11 @@
8686
<version>${mockito.version}</version>
8787
<scope>test</scope>
8888
</dependency>
89+
<!-- Google Auth Library -->
90+
<dependency>
91+
<groupId>com.google.auth</groupId>
92+
<artifactId>google-auth-library-oauth2-http</artifactId>
93+
<version>1.20.0</version>
94+
</dependency>
8995
</dependencies>
9096
</project>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ public DefaultCredentialsProvider() {
2929
new AzureCliCredentialsProvider(),
3030
new ExternalBrowserCredentialsProvider(),
3131
new DatabricksCliCredentialsProvider(),
32-
new NotebookNativeCredentialsProvider());
32+
new NotebookNativeCredentialsProvider(),
33+
new GoogleCredentialsCredentialsProvider(),
34+
new GoogleIdCredentialsProvider());
3335
}
3436

3537
@Override
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.databricks.sdk.core;
2+
3+
import static com.databricks.sdk.core.utils.GoogleUtils.GCP_SCOPES;
4+
import static com.databricks.sdk.core.utils.GoogleUtils.SA_ACCESS_TOKEN_HEADER;
5+
6+
import com.google.auth.oauth2.*;
7+
import com.google.auth.oauth2.IdTokenProvider.Option;
8+
import java.io.ByteArrayInputStream;
9+
import java.io.IOException;
10+
import java.nio.charset.StandardCharsets;
11+
import java.nio.file.Files;
12+
import java.nio.file.Paths;
13+
import java.util.*;
14+
import org.slf4j.Logger;
15+
import org.slf4j.LoggerFactory;
16+
17+
public class GoogleCredentialsCredentialsProvider implements CredentialsProvider {
18+
19+
private static final Logger LOG =
20+
LoggerFactory.getLogger(GoogleCredentialsCredentialsProvider.class);
21+
22+
@Override
23+
public String authType() {
24+
return "google-credentials";
25+
}
26+
27+
@Override
28+
public HeaderFactory configure(DatabricksConfig config) {
29+
String host = config.getHost();
30+
String googleCredentials = config.getGoogleCredentials();
31+
if (host == null || googleCredentials == null || !config.isGcp()) {
32+
return null;
33+
}
34+
35+
ServiceAccountCredentials serviceAccountCredentials;
36+
37+
try {
38+
serviceAccountCredentials =
39+
ServiceAccountCredentials.fromStream(Files.newInputStream(Paths.get(googleCredentials)));
40+
} catch (IOException e) {
41+
try {
42+
// If file doesn't exist, we try to parse the config as the content.
43+
serviceAccountCredentials =
44+
ServiceAccountCredentials.fromStream(
45+
new ByteArrayInputStream(googleCredentials.getBytes(StandardCharsets.UTF_8)));
46+
} catch (IOException ex) {
47+
LOG.warn("Failed to get Google service account credentials." + ex);
48+
return null;
49+
}
50+
}
51+
52+
List<Option> tokenOption = Collections.emptyList();
53+
54+
ServiceAccountCredentials finalServiceAccountCredentials = serviceAccountCredentials;
55+
return () -> {
56+
IdToken idToken;
57+
try {
58+
idToken = finalServiceAccountCredentials.idTokenWithAudience(host, tokenOption);
59+
} catch (IOException e) {
60+
String message = "Failed to get id token from Google service account credentials.";
61+
LOG.error(message + e);
62+
throw new DatabricksException(message, e);
63+
}
64+
Map<String, String> headers = new HashMap<>();
65+
headers.put("Authorization", String.format("Bearer %s", idToken.getTokenValue()));
66+
67+
if (config.isAccountClient()) {
68+
AccessToken token;
69+
try {
70+
token = finalServiceAccountCredentials.createScoped(GCP_SCOPES).refreshAccessToken();
71+
} catch (IOException e) {
72+
String message =
73+
"Failed to refresh access token from Google service account credentials.";
74+
LOG.error(message + e);
75+
throw new DatabricksException(message, e);
76+
}
77+
headers.put(SA_ACCESS_TOKEN_HEADER, token.getTokenValue());
78+
}
79+
80+
return headers;
81+
};
82+
}
83+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.databricks.sdk.core;
2+
3+
import static com.databricks.sdk.core.utils.GoogleUtils.GCP_SCOPES;
4+
import static com.databricks.sdk.core.utils.GoogleUtils.SA_ACCESS_TOKEN_HEADER;
5+
6+
import com.google.auth.oauth2.GoogleCredentials;
7+
import com.google.auth.oauth2.IdTokenCredentials;
8+
import com.google.auth.oauth2.IdTokenProvider;
9+
import com.google.auth.oauth2.ImpersonatedCredentials;
10+
import java.io.IOException;
11+
import java.util.*;
12+
import org.slf4j.Logger;
13+
import org.slf4j.LoggerFactory;
14+
15+
public class GoogleIdCredentialsProvider implements CredentialsProvider {
16+
17+
private static final Logger LOG = LoggerFactory.getLogger(GoogleIdCredentialsProvider.class);
18+
19+
@Override
20+
public String authType() {
21+
return "google-id";
22+
}
23+
24+
@Override
25+
public HeaderFactory configure(DatabricksConfig config) {
26+
String host = config.getHost();
27+
String googleServiceAccount = config.getGoogleServiceAccount();
28+
if (host == null || googleServiceAccount == null || !config.isGcp()) {
29+
return null;
30+
}
31+
32+
GoogleCredentials googleCredentials;
33+
try {
34+
googleCredentials = GoogleCredentials.getApplicationDefault();
35+
} catch (IOException e) {
36+
LOG.warn("Failed to get Google application default credential." + e);
37+
return null;
38+
}
39+
40+
// Create the impersonated credential. Use 3600s as the lifetime, which is the default value in
41+
// google-auth.
42+
ImpersonatedCredentials impersonatedCredentials =
43+
ImpersonatedCredentials.create(
44+
googleCredentials, googleServiceAccount, null, new ArrayList<>(), 3600);
45+
46+
IdTokenCredentials idTokenCredentials =
47+
IdTokenCredentials.newBuilder()
48+
.setIdTokenProvider(impersonatedCredentials)
49+
.setTargetAudience(host)
50+
// Setting this will include email in the id token.
51+
.setOptions(Collections.singletonList(IdTokenProvider.Option.INCLUDE_EMAIL))
52+
.build();
53+
54+
ImpersonatedCredentials gcpScopedCredentials =
55+
ImpersonatedCredentials.create(
56+
googleCredentials, googleServiceAccount, null, GCP_SCOPES, 3600);
57+
58+
return () -> {
59+
Map<String, String> headers = new HashMap<>();
60+
try {
61+
headers.put(
62+
"Authorization",
63+
String.format("Bearer %s", idTokenCredentials.refreshAccessToken().getTokenValue()));
64+
} catch (IOException e) {
65+
String message = "Failed to refresh access token from id token credentials.";
66+
LOG.error(message + e);
67+
throw new DatabricksException(message, e);
68+
}
69+
70+
if (config.isAccountClient()) {
71+
try {
72+
headers.put(
73+
SA_ACCESS_TOKEN_HEADER, gcpScopedCredentials.refreshAccessToken().getTokenValue());
74+
} catch (IOException e) {
75+
String message = "Failed to refresh access token from scoped id token credentials.";
76+
LOG.error(message + e);
77+
throw new DatabricksException(message, e);
78+
}
79+
}
80+
81+
return headers;
82+
};
83+
}
84+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.databricks.sdk.core.utils;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
6+
public class GoogleUtils {
7+
public static List<String> GCP_SCOPES =
8+
Arrays.asList(
9+
"https://www.googleapis.com/auth/cloud-platform",
10+
"https://www.googleapis.com/auth/compute");
11+
12+
public static String SA_ACCESS_TOKEN_HEADER = "X-Databricks-GCP-SA-Access-Token";
13+
}

databricks-sdk-java/src/test/java/com/databricks/sdk/integration/CredentialsIT.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
public class CredentialsIT {
1616
@Test
1717
void lists(AccountClient a) {
18-
Iterable<Credential> list = a.credentials().list();
18+
// Skipping this test for GCP because this api is not enabled in GCP.
19+
if (!a.config().isGcp()) {
20+
Iterable<Credential> list = a.credentials().list();
1921

20-
java.util.List<Credential> all = CollectionUtils.asList(list);
22+
java.util.List<Credential> all = CollectionUtils.asList(list);
2123

22-
CollectionUtils.assertUnique(all);
24+
CollectionUtils.assertUnique(all);
25+
}
2326
}
2427
}

databricks-sdk-java/src/test/java/com/databricks/sdk/integration/FilesIT.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.databricks.sdk.integration.framework.EnvTest;
66
import com.databricks.sdk.integration.framework.NameUtils;
77
import com.databricks.sdk.integration.framework.ResourceWithCleanup;
8-
import com.databricks.sdk.service.files.FileInfo;
98
import java.io.ByteArrayInputStream;
109
import java.io.IOException;
1110
import java.io.InputStream;
@@ -46,10 +45,6 @@ private void writeFileAndReadFileInner(
4645
// Write the file to DBFS.
4746
workspace.files().upload(fileName, inputStream);
4847

49-
// Read file status
50-
FileInfo fileStatus = workspace.files().getStatus(fileName);
51-
Assertions.assertEquals(fileStatus.getPath(), fileName);
52-
5348
// Read the file back from DBFS.
5449
try (InputStream readContents = workspace.files().download(fileName).getContents()) {
5550
byte[] result = new byte[fileContents.length];

0 commit comments

Comments
 (0)