Skip to content

Commit 7717a1b

Browse files
committed
added tests, refactored code
1 parent f46ac44 commit 7717a1b

File tree

6 files changed

+279
-94
lines changed

6 files changed

+279
-94
lines changed

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### New Features and Improvements
66

7+
* Add native support for Azure DevOps OIDC authentication
8+
79
### Bug Fixes
810

911
### Documentation

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,11 @@ Depending on the Databricks authentication method, the SDK uses the following in
116116

117117
### Databricks native authentication
118118

119-
By default, the Databricks SDK for Java initially tries [Databricks token authentication](https://docs.databricks.com/dev-tools/api/latest/authentication.html) (`auth_type='pat'` argument). If the SDK is unsuccessful, it then tries Databricks Workload Identity Federation (WIF) authentication using OIDC (`auth_type="github-oidc"` argument).
119+
By default, the Databricks SDK for Java initially tries [Databricks token authentication](https://docs.databricks.com/dev-tools/api/latest/authentication.html) (`auth_type='pat'` argument). If the SDK is unsuccessful, it then tries Workload Identity Federation (WIF). See [Supported WIF](https://docs.databricks.com/aws/en/dev-tools/auth/oauth-federation-provider) for the supported JWT token providers.
120120

121121
- For Databricks token authentication, you must provide `host` and `token`; or their environment variable or `.databrickscfg` file field equivalents.
122122
- For Databricks OIDC authentication, you must provide the `host`, `client_id` and `token_audience` _(optional)_ either directly, through the corresponding environment variables, or in your `.databrickscfg` configuration file.
123+
- For Azure DevOps OIDC authentication, the `token_audience` is irrelevant as the audience is always set to `api://AzureADTokenExchange`. Also, the `System.AccessToken` pipeline variable required for OIDC request must be exposed as the `SYSTEM_ACCESSTOKEN` environment variable, following [Pipeline variables](https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken)
123124

124125
| Argument | Description | Environment variable |
125126
|--------------|-------------|-------------------|

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,13 +431,17 @@ public DatabricksConfig setAzureUseMsi(boolean azureUseMsi) {
431431
return this;
432432
}
433433

434-
/** @deprecated Use {@link #getAzureUseMsi()} instead. */
434+
/**
435+
* @deprecated Use {@link #getAzureUseMsi()} instead.
436+
*/
435437
@Deprecated()
436438
public boolean getAzureUseMSI() {
437439
return azureUseMsi;
438440
}
439441

440-
/** @deprecated Use {@link #getAzureUseMsi()} instead. */
442+
/**
443+
* @deprecated Use {@link #getAzureUseMsi()} instead.
444+
*/
441445
@Deprecated
442446
public DatabricksConfig setAzureUseMSI(boolean azureUseMsi) {
443447
this.azureUseMsi = azureUseMsi;

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,7 @@ private void addOIDCCredentialsProviders(DatabricksConfig config) {
131131
try {
132132
namedIdTokenSources.add(
133133
new NamedIDTokenSource(
134-
"azure-devops-oidc",
135-
new AzureDevOpsIDTokenSource(config.getHttpClient())));
134+
"azure-devops-oidc", new AzureDevOpsIDTokenSource(config.getHttpClient())));
136135
} catch (DatabricksException e) {
137136
LOG.debug("Azure DevOps OIDC provider not available: {}", e.getMessage());
138137
}

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

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.databricks.sdk.core.http.HttpClient;
55
import com.databricks.sdk.core.http.Request;
66
import com.databricks.sdk.core.http.Response;
7+
import com.databricks.sdk.core.utils.Environment;
78
import com.fasterxml.jackson.databind.ObjectMapper;
89
import com.fasterxml.jackson.databind.node.ObjectNode;
910
import com.google.common.base.Strings;
@@ -13,7 +14,7 @@
1314
* AzureDevOpsIDTokenSource retrieves JWT Tokens from Azure DevOps Pipelines. This class implements
1415
* the IDTokenSource interface and provides a method for obtaining ID tokens specifically from Azure
1516
* DevOps Pipeline environment.
16-
*
17+
*
1718
* <p>This implementation follows the Azure DevOps OIDC token API as documented at:
1819
* https://learn.microsoft.com/en-us/rest/api/azure/devops/distributedtask/oidctoken/create
1920
*/
@@ -32,32 +33,56 @@ public class AzureDevOpsIDTokenSource implements IDTokenSource {
3233
private final String azureDevOpsHostType;
3334
/* HTTP client for making requests to Azure DevOps */
3435
private final HttpClient httpClient;
36+
/* Environment for reading configuration values */
37+
private final Environment environment;
3538
/* JSON mapper for parsing response data */
3639
private static final ObjectMapper mapper = new ObjectMapper();
3740

3841
/**
39-
* Constructs a new AzureDevOpsIDTokenSource by reading environment variables.
40-
* This constructor implements fail-early validation - if any required environment
41-
* variables are missing, it will throw a DatabricksException immediately.
42+
* Constructs a new AzureDevOpsIDTokenSource by reading environment variables. This constructor
43+
* implements fail-early validation - if any required environment variables are missing, it will
44+
* throw a DatabricksException immediately.
4245
*
4346
* @param httpClient The HTTP client to use for making requests
4447
* @throws DatabricksException if any required environment variables are missing
4548
*/
4649
public AzureDevOpsIDTokenSource(HttpClient httpClient) {
50+
this(httpClient, createDefaultEnvironment());
51+
}
52+
53+
/**
54+
* Constructs a new AzureDevOpsIDTokenSource with a custom environment. This constructor is
55+
* primarily used for testing to inject mock environment variables.
56+
*
57+
* @param httpClient The HTTP client to use for making requests
58+
* @param environment The environment to read configuration from
59+
* @throws DatabricksException if httpClient is null or any required environment variables are
60+
* missing
61+
*/
62+
public AzureDevOpsIDTokenSource(HttpClient httpClient, Environment environment) {
4763
if (httpClient == null) {
4864
throw new DatabricksException("HttpClient cannot be null");
4965
}
5066
this.httpClient = httpClient;
67+
this.environment = environment;
5168

52-
// Fail early: validate all required environment variables
5369
this.azureDevOpsAccessToken = validateEnvironmentVariable("SYSTEM_ACCESSTOKEN");
54-
this.azureDevOpsTeamFoundationCollectionUri = validateEnvironmentVariable("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI");
70+
this.azureDevOpsTeamFoundationCollectionUri =
71+
validateEnvironmentVariable("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI");
5572
this.azureDevOpsPlanId = validateEnvironmentVariable("SYSTEM_PLANID");
5673
this.azureDevOpsJobId = validateEnvironmentVariable("SYSTEM_JOBID");
5774
this.azureDevOpsTeamProjectId = validateEnvironmentVariable("SYSTEM_TEAMPROJECTID");
5875
this.azureDevOpsHostType = validateEnvironmentVariable("SYSTEM_HOSTTYPE");
5976
}
6077

78+
/** Creates a default Environment using system environment variables. */
79+
private static Environment createDefaultEnvironment() {
80+
String pathEnv = System.getenv("PATH");
81+
String[] pathArray =
82+
pathEnv != null ? pathEnv.split(java.io.File.pathSeparator) : new String[0];
83+
return new Environment(System.getenv(), pathArray, System.getProperty("os.name"));
84+
}
85+
6186
/**
6287
* Validates that an environment variable is present and not empty.
6388
*
@@ -66,8 +91,14 @@ public AzureDevOpsIDTokenSource(HttpClient httpClient) {
6691
* @throws DatabricksException if the environment variable is missing or empty
6792
*/
6893
private String validateEnvironmentVariable(String varName) {
69-
String value = System.getenv(varName);
94+
String value = environment.get(varName);
7095
if (Strings.isNullOrEmpty(value)) {
96+
if (varName.equals("SYSTEM_ACCESSTOKEN")) {
97+
throw new DatabricksException(
98+
String.format(
99+
"Missing %s, if calling from Azure DevOps Pipeline, please set this env var following https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken",
100+
varName));
101+
}
71102
throw new DatabricksException(
72103
String.format("Missing %s, likely not calling from Azure DevOps Pipeline", varName));
73104
}
@@ -76,9 +107,9 @@ private String validateEnvironmentVariable(String varName) {
76107

77108
/**
78109
* Retrieves an ID token from Azure DevOps Pipelines. This method makes an authenticated request
79-
* to Azure DevOps to obtain a JWT token that can later be exchanged for a Databricks access token.
110+
* to Azure DevOps to obtain a JWT token that can later be exchanged for a Databricks access
111+
* token.
80112
*
81-
*
82113
* <p>Note: The audience parameter is ignored for Azure DevOps OIDC tokens as they have a
83114
* hardcoded audience for Azure AD integration.
84115
*
@@ -89,26 +120,30 @@ private String validateEnvironmentVariable(String varName) {
89120
@Override
90121
public IDToken getIDToken(String audience) {
91122

92-
// Build Azure DevOps OIDC endpoint URL
93-
// Format: {collectionUri}/{teamProjectId}/_apis/distributedtask/hubs/{hostType}/plans/{planId}/jobs/{jobId}/oidctoken?api-version=7.2-preview.1
94-
String requestUrl = String.format(
95-
"%s/%s/_apis/distributedtask/hubs/%s/plans/%s/jobs/%s/oidctoken?api-version=7.2-preview.1",
96-
azureDevOpsTeamFoundationCollectionUri,
97-
azureDevOpsTeamProjectId,
98-
azureDevOpsHostType,
99-
azureDevOpsPlanId,
100-
azureDevOpsJobId);
123+
// Build Azure DevOps OIDC endpoint URL.
124+
// Format:
125+
// {collectionUri}/{teamProjectId}/_apis/distributedtask/hubs/{hostType}/plans/{planId}/jobs/{jobId}/oidctoken?api-version=7.2-preview.1
126+
String requestUrl =
127+
String.format(
128+
"%s/%s/_apis/distributedtask/hubs/%s/plans/%s/jobs/%s/oidctoken?api-version=7.2-preview.1",
129+
azureDevOpsTeamFoundationCollectionUri,
130+
azureDevOpsTeamProjectId,
131+
azureDevOpsHostType,
132+
azureDevOpsPlanId,
133+
azureDevOpsJobId);
101134

102-
Request req = new Request("POST", requestUrl)
103-
.withHeader("Authorization", "Bearer " + azureDevOpsAccessToken)
104-
.withHeader("Content-Type", "application/json");
135+
Request req =
136+
new Request("POST", requestUrl)
137+
.withHeader("Authorization", "Bearer " + azureDevOpsAccessToken)
138+
.withHeader("Content-Type", "application/json");
105139

106140
Response resp;
107141
try {
108142
resp = httpClient.execute(req);
109143
} catch (IOException e) {
110144
throw new DatabricksException(
111-
"Failed to request ID token from Azure DevOps at " + requestUrl + ": " + e.getMessage(), e);
145+
"Failed to request ID token from Azure DevOps at " + requestUrl + ": " + e.getMessage(),
146+
e);
112147
}
113148

114149
if (resp.getStatusCode() != 200) {
@@ -129,7 +164,6 @@ public IDToken getIDToken(String audience) {
129164
"Failed to parse Azure DevOps OIDC token response: " + e.getMessage(), e);
130165
}
131166

132-
// Validate response structure and token value
133167
if (!jsonResp.has("oidcToken")) {
134168
throw new DatabricksException("Azure DevOps OIDC token response missing 'oidcToken' field");
135169
}
@@ -141,7 +175,8 @@ public IDToken getIDToken(String audience) {
141175
}
142176
return new IDToken(tokenValue);
143177
} catch (IllegalArgumentException e) {
144-
throw new DatabricksException("Received invalid OIDC token from Azure DevOps: " + e.getMessage(), e);
178+
throw new DatabricksException(
179+
"Received invalid OIDC token from Azure DevOps: " + e.getMessage(), e);
145180
}
146181
}
147182
}

0 commit comments

Comments
 (0)