From d443694ef2851ea29f9c1898222019eea04d8b72 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Tue, 23 Sep 2025 16:26:07 +0000 Subject: [PATCH 01/14] add azure devops OIDC authentication --- .../sdk/core/DefaultCredentialsProvider.java | 11 ++ .../core/oauth/AzureDevOpsIDTokenSource.java | 147 ++++++++++++++++++ .../oauth/AzureDevOpsIDTokenSourceTest.java | 93 +++++++++++ 3 files changed, 251 insertions(+) create mode 100644 databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java create mode 100644 databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java index f72aa435b..ec4c65607 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java @@ -125,6 +125,17 @@ private void addOIDCCredentialsProviders(DatabricksConfig config) { config.getActionsIdTokenRequestUrl(), config.getActionsIdTokenRequestToken(), config.getHttpClient()))); + + // Try to create Azure DevOps token source - if environment variables are missing, + // skip this provider gracefully. + try { + namedIdTokenSources.add( + new NamedIDTokenSource( + "azure-devops-oidc", + new AzureDevOpsIDTokenSource(config.getHttpClient()))); + } catch (DatabricksException e) { + LOG.debug("Azure DevOps OIDC provider not available: {}", e.getMessage()); + } // Add new IDTokenSources and ID providers here. Example: // namedIdTokenSources.add(new NamedIDTokenSource("custom-oidc", new CustomIDTokenSource(...))); diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java new file mode 100644 index 000000000..8b518beb5 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java @@ -0,0 +1,147 @@ +package com.databricks.sdk.core.oauth; + +import com.databricks.sdk.core.DatabricksException; +import com.databricks.sdk.core.http.HttpClient; +import com.databricks.sdk.core.http.Request; +import com.databricks.sdk.core.http.Response; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Strings; +import java.io.IOException; + +/** + * AzureDevOpsIDTokenSource retrieves JWT Tokens from Azure DevOps Pipelines. This class implements + * the IDTokenSource interface and provides a method for obtaining ID tokens specifically from Azure + * DevOps Pipeline environment. + * + *

This implementation follows the Azure DevOps OIDC token API as documented at: + * https://learn.microsoft.com/en-us/rest/api/azure/devops/distributedtask/oidctoken/create + */ +public class AzureDevOpsIDTokenSource implements IDTokenSource { + /* Access token for authenticating with Azure DevOps API */ + private final String azureDevOpsAccessToken; + /* Team Foundation Collection URI (e.g., https://dev.azure.com/organization) */ + private final String azureDevOpsTeamFoundationCollectionUri; + /* Plan ID for the current pipeline run */ + private final String azureDevOpsPlanId; + /* Job ID for the current pipeline job */ + private final String azureDevOpsJobId; + /* Team Project ID where the pipeline is running */ + private final String azureDevOpsTeamProjectId; + /* Host type (e.g., "build", "release") */ + private final String azureDevOpsHostType; + /* HTTP client for making requests to Azure DevOps */ + private final HttpClient httpClient; + /* JSON mapper for parsing response data */ + private static final ObjectMapper mapper = new ObjectMapper(); + + /** + * Constructs a new AzureDevOpsIDTokenSource by reading environment variables. + * This constructor implements fail-early validation - if any required environment + * variables are missing, it will throw a DatabricksException immediately. + * + * @param httpClient The HTTP client to use for making requests + * @throws DatabricksException if any required environment variables are missing + */ + public AzureDevOpsIDTokenSource(HttpClient httpClient) { + if (httpClient == null) { + throw new DatabricksException("HttpClient cannot be null"); + } + this.httpClient = httpClient; + + // Fail early: validate all required environment variables + this.azureDevOpsAccessToken = validateEnvironmentVariable("SYSTEM_ACCESSTOKEN"); + this.azureDevOpsTeamFoundationCollectionUri = validateEnvironmentVariable("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"); + this.azureDevOpsPlanId = validateEnvironmentVariable("SYSTEM_PLANID"); + this.azureDevOpsJobId = validateEnvironmentVariable("SYSTEM_JOBID"); + this.azureDevOpsTeamProjectId = validateEnvironmentVariable("SYSTEM_TEAMPROJECTID"); + this.azureDevOpsHostType = validateEnvironmentVariable("SYSTEM_HOSTTYPE"); + } + + /** + * Validates that an environment variable is present and not empty. + * + * @param varName The environment variable name + * @return The environment variable value + * @throws DatabricksException if the environment variable is missing or empty + */ + private String validateEnvironmentVariable(String varName) { + String value = System.getenv(varName); + if (Strings.isNullOrEmpty(value)) { + throw new DatabricksException( + String.format("Missing %s, likely not calling from Azure DevOps Pipeline", varName)); + } + return value; + } + + /** + * Retrieves an ID token from Azure DevOps Pipelines. This method makes an authenticated request + * to Azure DevOps to obtain a JWT token that can later be exchanged for a Databricks access token. + * + * + *

Note: The audience parameter is ignored for Azure DevOps OIDC tokens as they have a + * hardcoded audience for Azure AD integration. + * + * @param audience Ignored for Azure DevOps OIDC tokens + * @return An IDToken object containing the JWT token value + * @throws DatabricksException if the token request fails + */ + @Override + public IDToken getIDToken(String audience) { + + // Build Azure DevOps OIDC endpoint URL + // Format: {collectionUri}/{teamProjectId}/_apis/distributedtask/hubs/{hostType}/plans/{planId}/jobs/{jobId}/oidctoken?api-version=7.2-preview.1 + String requestUrl = String.format( + "%s/%s/_apis/distributedtask/hubs/%s/plans/%s/jobs/%s/oidctoken?api-version=7.2-preview.1", + azureDevOpsTeamFoundationCollectionUri, + azureDevOpsTeamProjectId, + azureDevOpsHostType, + azureDevOpsPlanId, + azureDevOpsJobId); + + Request req = new Request("POST", requestUrl) + .withHeader("Authorization", "Bearer " + azureDevOpsAccessToken) + .withHeader("Content-Type", "application/json"); + + Response resp; + try { + resp = httpClient.execute(req); + } catch (IOException e) { + throw new DatabricksException( + "Failed to request ID token from Azure DevOps at " + requestUrl + ": " + e.getMessage(), e); + } + + if (resp.getStatusCode() != 200) { + throw new DatabricksException( + "Failed to request ID token from Azure DevOps: status code " + + resp.getStatusCode() + + ", response body: " + + resp.getBody().toString()); + } + + // Parse the JSON response + // Azure DevOps returns {"oidcToken":"***"} format, not {"value":"***"} like GitHub Actions + ObjectNode jsonResp; + try { + jsonResp = mapper.readValue(resp.getBody(), ObjectNode.class); + } catch (IOException e) { + throw new DatabricksException( + "Failed to parse Azure DevOps OIDC token response: " + e.getMessage(), e); + } + + // Validate response structure and token value + if (!jsonResp.has("oidcToken")) { + throw new DatabricksException("Azure DevOps OIDC token response missing 'oidcToken' field"); + } + + try { + String tokenValue = jsonResp.get("oidcToken").textValue(); + if (Strings.isNullOrEmpty(tokenValue)) { + throw new DatabricksException("Received empty OIDC token from Azure DevOps"); + } + return new IDToken(tokenValue); + } catch (IllegalArgumentException e) { + throw new DatabricksException("Received invalid OIDC token from Azure DevOps: " + e.getMessage(), e); + } + } +} diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java new file mode 100644 index 000000000..295f77d8d --- /dev/null +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java @@ -0,0 +1,93 @@ +package com.databricks.sdk.core.oauth; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.databricks.sdk.core.DatabricksException; +import com.databricks.sdk.core.http.HttpClient; +import com.databricks.sdk.core.http.Request; +import com.databricks.sdk.core.http.Response; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit tests for AzureDevOpsIDTokenSource. + * + * Note: These tests focus on the core functionality. Environment variable validation + * tests are limited since the class now reads directly from System.getenv(). + * Integration tests should be used to test the full environment variable behavior. + */ +public class AzureDevOpsIDTokenSourceTest { + + @Mock private HttpClient httpClient; + @Mock private Response response; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testNullHttpClient() { + // Act & Assert + DatabricksException exception = assertThrows(DatabricksException.class, + () -> new AzureDevOpsIDTokenSource(null)); + assertTrue(exception.getMessage().contains("HttpClient cannot be null")); + } + + /** + * Test that audience parameter is ignored (Azure DevOps has hardcoded audience). + * This test verifies that the URL construction doesn't include audience parameter. + */ + @Test + void testAudienceParameterIgnored() throws IOException { + // This test can only run if environment variables are set (e.g., in Azure DevOps) + // Skip if not in Azure DevOps environment + if (System.getenv("SYSTEM_ACCESSTOKEN") == null) { + return; // Skip test if not in Azure DevOps environment + } + + // Arrange + String audience = "https://databricks.com"; + String expectedToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."; + String responseBody = String.format("{\"oidcToken\":\"%s\"}", expectedToken); + + when(response.getStatusCode()).thenReturn(200); + when(response.getBody()).thenReturn(new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8))); + when(httpClient.execute(any(Request.class))).thenReturn(response); + + AzureDevOpsIDTokenSource tokenSource = new AzureDevOpsIDTokenSource(httpClient); + + // Act + IDToken result = tokenSource.getIDToken(audience); + + // Assert + assertNotNull(result); + assertEquals(expectedToken, result.getValue()); + + // Verify the request URL does NOT include the audience parameter (it's ignored) + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + verify(httpClient).execute(requestCaptor.capture()); + + Request capturedRequest = requestCaptor.getValue(); + String requestUri = capturedRequest.getUri().toString(); + assertFalse(requestUri.contains("audience="), + "Audience parameter should be ignored for Azure DevOps OIDC tokens"); + assertTrue(requestUri.contains("api-version=7.2-preview.1")); + } + + /** + * Note: Most environment variable validation tests are not included here + * since the class now reads directly from System.getenv(). These should be + * tested in integration tests where the environment can be controlled. + * + * The tests below focus on the core HTTP functionality that can be unit tested. + */ +} From 7717a1b485ee9751966db175718ae210c06a714a Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Wed, 24 Sep 2025 10:44:02 +0000 Subject: [PATCH 02/14] added tests, refactored code --- NEXT_CHANGELOG.md | 2 + README.md | 3 +- .../databricks/sdk/core/DatabricksConfig.java | 8 +- .../sdk/core/DefaultCredentialsProvider.java | 3 +- .../core/oauth/AzureDevOpsIDTokenSource.java | 83 ++++-- .../oauth/AzureDevOpsIDTokenSourceTest.java | 274 +++++++++++++----- 6 files changed, 279 insertions(+), 94 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 5c19ecb8b..ac03b8014 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -4,6 +4,8 @@ ### New Features and Improvements +* Add native support for Azure DevOps OIDC authentication + ### Bug Fixes ### Documentation diff --git a/README.md b/README.md index 8a8d2858a..4e6d6af2e 100644 --- a/README.md +++ b/README.md @@ -116,10 +116,11 @@ Depending on the Databricks authentication method, the SDK uses the following in ### Databricks native authentication -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). +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. - For Databricks token authentication, you must provide `host` and `token`; or their environment variable or `.databrickscfg` file field equivalents. - 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. +- 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) | Argument | Description | Environment variable | |--------------|-------------|-------------------| diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java index 074e97974..775b52a79 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java @@ -431,13 +431,17 @@ public DatabricksConfig setAzureUseMsi(boolean azureUseMsi) { return this; } - /** @deprecated Use {@link #getAzureUseMsi()} instead. */ + /** + * @deprecated Use {@link #getAzureUseMsi()} instead. + */ @Deprecated() public boolean getAzureUseMSI() { return azureUseMsi; } - /** @deprecated Use {@link #getAzureUseMsi()} instead. */ + /** + * @deprecated Use {@link #getAzureUseMsi()} instead. + */ @Deprecated public DatabricksConfig setAzureUseMSI(boolean azureUseMsi) { this.azureUseMsi = azureUseMsi; diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java index 9f87c514c..9cd580a43 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java @@ -131,8 +131,7 @@ private void addOIDCCredentialsProviders(DatabricksConfig config) { try { namedIdTokenSources.add( new NamedIDTokenSource( - "azure-devops-oidc", - new AzureDevOpsIDTokenSource(config.getHttpClient()))); + "azure-devops-oidc", new AzureDevOpsIDTokenSource(config.getHttpClient()))); } catch (DatabricksException e) { LOG.debug("Azure DevOps OIDC provider not available: {}", e.getMessage()); } diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java index 8b518beb5..0717f070b 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java @@ -4,6 +4,7 @@ import com.databricks.sdk.core.http.HttpClient; import com.databricks.sdk.core.http.Request; import com.databricks.sdk.core.http.Response; +import com.databricks.sdk.core.utils.Environment; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; @@ -13,7 +14,7 @@ * AzureDevOpsIDTokenSource retrieves JWT Tokens from Azure DevOps Pipelines. This class implements * the IDTokenSource interface and provides a method for obtaining ID tokens specifically from Azure * DevOps Pipeline environment. - * + * *

This implementation follows the Azure DevOps OIDC token API as documented at: * https://learn.microsoft.com/en-us/rest/api/azure/devops/distributedtask/oidctoken/create */ @@ -32,32 +33,56 @@ public class AzureDevOpsIDTokenSource implements IDTokenSource { private final String azureDevOpsHostType; /* HTTP client for making requests to Azure DevOps */ private final HttpClient httpClient; + /* Environment for reading configuration values */ + private final Environment environment; /* JSON mapper for parsing response data */ private static final ObjectMapper mapper = new ObjectMapper(); /** - * Constructs a new AzureDevOpsIDTokenSource by reading environment variables. - * This constructor implements fail-early validation - if any required environment - * variables are missing, it will throw a DatabricksException immediately. + * Constructs a new AzureDevOpsIDTokenSource by reading environment variables. This constructor + * implements fail-early validation - if any required environment variables are missing, it will + * throw a DatabricksException immediately. * * @param httpClient The HTTP client to use for making requests * @throws DatabricksException if any required environment variables are missing */ public AzureDevOpsIDTokenSource(HttpClient httpClient) { + this(httpClient, createDefaultEnvironment()); + } + + /** + * Constructs a new AzureDevOpsIDTokenSource with a custom environment. This constructor is + * primarily used for testing to inject mock environment variables. + * + * @param httpClient The HTTP client to use for making requests + * @param environment The environment to read configuration from + * @throws DatabricksException if httpClient is null or any required environment variables are + * missing + */ + public AzureDevOpsIDTokenSource(HttpClient httpClient, Environment environment) { if (httpClient == null) { throw new DatabricksException("HttpClient cannot be null"); } this.httpClient = httpClient; + this.environment = environment; - // Fail early: validate all required environment variables this.azureDevOpsAccessToken = validateEnvironmentVariable("SYSTEM_ACCESSTOKEN"); - this.azureDevOpsTeamFoundationCollectionUri = validateEnvironmentVariable("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"); + this.azureDevOpsTeamFoundationCollectionUri = + validateEnvironmentVariable("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"); this.azureDevOpsPlanId = validateEnvironmentVariable("SYSTEM_PLANID"); this.azureDevOpsJobId = validateEnvironmentVariable("SYSTEM_JOBID"); this.azureDevOpsTeamProjectId = validateEnvironmentVariable("SYSTEM_TEAMPROJECTID"); this.azureDevOpsHostType = validateEnvironmentVariable("SYSTEM_HOSTTYPE"); } + /** Creates a default Environment using system environment variables. */ + private static Environment createDefaultEnvironment() { + String pathEnv = System.getenv("PATH"); + String[] pathArray = + pathEnv != null ? pathEnv.split(java.io.File.pathSeparator) : new String[0]; + return new Environment(System.getenv(), pathArray, System.getProperty("os.name")); + } + /** * Validates that an environment variable is present and not empty. * @@ -66,8 +91,14 @@ public AzureDevOpsIDTokenSource(HttpClient httpClient) { * @throws DatabricksException if the environment variable is missing or empty */ private String validateEnvironmentVariable(String varName) { - String value = System.getenv(varName); + String value = environment.get(varName); if (Strings.isNullOrEmpty(value)) { + if (varName.equals("SYSTEM_ACCESSTOKEN")) { + throw new DatabricksException( + String.format( + "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", + varName)); + } throw new DatabricksException( String.format("Missing %s, likely not calling from Azure DevOps Pipeline", varName)); } @@ -76,9 +107,9 @@ private String validateEnvironmentVariable(String varName) { /** * Retrieves an ID token from Azure DevOps Pipelines. This method makes an authenticated request - * to Azure DevOps to obtain a JWT token that can later be exchanged for a Databricks access token. + * to Azure DevOps to obtain a JWT token that can later be exchanged for a Databricks access + * token. * - * *

Note: The audience parameter is ignored for Azure DevOps OIDC tokens as they have a * hardcoded audience for Azure AD integration. * @@ -89,26 +120,30 @@ private String validateEnvironmentVariable(String varName) { @Override public IDToken getIDToken(String audience) { - // Build Azure DevOps OIDC endpoint URL - // Format: {collectionUri}/{teamProjectId}/_apis/distributedtask/hubs/{hostType}/plans/{planId}/jobs/{jobId}/oidctoken?api-version=7.2-preview.1 - String requestUrl = String.format( - "%s/%s/_apis/distributedtask/hubs/%s/plans/%s/jobs/%s/oidctoken?api-version=7.2-preview.1", - azureDevOpsTeamFoundationCollectionUri, - azureDevOpsTeamProjectId, - azureDevOpsHostType, - azureDevOpsPlanId, - azureDevOpsJobId); + // Build Azure DevOps OIDC endpoint URL. + // Format: + // {collectionUri}/{teamProjectId}/_apis/distributedtask/hubs/{hostType}/plans/{planId}/jobs/{jobId}/oidctoken?api-version=7.2-preview.1 + String requestUrl = + String.format( + "%s/%s/_apis/distributedtask/hubs/%s/plans/%s/jobs/%s/oidctoken?api-version=7.2-preview.1", + azureDevOpsTeamFoundationCollectionUri, + azureDevOpsTeamProjectId, + azureDevOpsHostType, + azureDevOpsPlanId, + azureDevOpsJobId); - Request req = new Request("POST", requestUrl) - .withHeader("Authorization", "Bearer " + azureDevOpsAccessToken) - .withHeader("Content-Type", "application/json"); + Request req = + new Request("POST", requestUrl) + .withHeader("Authorization", "Bearer " + azureDevOpsAccessToken) + .withHeader("Content-Type", "application/json"); Response resp; try { resp = httpClient.execute(req); } catch (IOException e) { throw new DatabricksException( - "Failed to request ID token from Azure DevOps at " + requestUrl + ": " + e.getMessage(), e); + "Failed to request ID token from Azure DevOps at " + requestUrl + ": " + e.getMessage(), + e); } if (resp.getStatusCode() != 200) { @@ -129,7 +164,6 @@ public IDToken getIDToken(String audience) { "Failed to parse Azure DevOps OIDC token response: " + e.getMessage(), e); } - // Validate response structure and token value if (!jsonResp.has("oidcToken")) { throw new DatabricksException("Azure DevOps OIDC token response missing 'oidcToken' field"); } @@ -141,7 +175,8 @@ public IDToken getIDToken(String audience) { } return new IDToken(tokenValue); } catch (IllegalArgumentException e) { - throw new DatabricksException("Received invalid OIDC token from Azure DevOps: " + e.getMessage(), e); + throw new DatabricksException( + "Received invalid OIDC token from Azure DevOps: " + e.getMessage(), e); } } } diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java index 295f77d8d..26c9eeebf 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java @@ -2,92 +2,236 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; import com.databricks.sdk.core.DatabricksException; import com.databricks.sdk.core.http.HttpClient; import com.databricks.sdk.core.http.Request; import com.databricks.sdk.core.http.Response; -import java.io.ByteArrayInputStream; +import com.databricks.sdk.core.utils.Environment; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; -/** - * Unit tests for AzureDevOpsIDTokenSource. - * - * Note: These tests focus on the core functionality. Environment variable validation - * tests are limited since the class now reads directly from System.getenv(). - * Integration tests should be used to test the full environment variable behavior. - */ +/** Tests for AzureDevOpsIDTokenSource. */ public class AzureDevOpsIDTokenSourceTest { - @Mock private HttpClient httpClient; - @Mock private Response response; + private static final String TEST_ID_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."; + private static final String TEST_ACCESS_TOKEN = "test-access-token"; + private static final String TEST_COLLECTION_URI = "https://dev.azure.com/testorg"; + private static final String TEST_PLAN_ID = "test-plan-id"; + private static final String TEST_JOB_ID = "test-job-id"; + private static final String TEST_PROJECT_ID = "test-project-id"; + private static final String TEST_HOST_TYPE = "build"; - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); + /** Creates a mock Environment with all required Azure DevOps environment variables. */ + private static Environment createValidEnvironment() { + Map envVars = new HashMap<>(); + envVars.put("SYSTEM_ACCESSTOKEN", TEST_ACCESS_TOKEN); + envVars.put("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", TEST_COLLECTION_URI); + envVars.put("SYSTEM_PLANID", TEST_PLAN_ID); + envVars.put("SYSTEM_JOBID", TEST_JOB_ID); + envVars.put("SYSTEM_TEAMPROJECTID", TEST_PROJECT_ID); + envVars.put("SYSTEM_HOSTTYPE", TEST_HOST_TYPE); + return new Environment(envVars, new String[0], "test"); } - @Test - void testNullHttpClient() { - // Act & Assert - DatabricksException exception = assertThrows(DatabricksException.class, - () -> new AzureDevOpsIDTokenSource(null)); - assertTrue(exception.getMessage().contains("HttpClient cannot be null")); + /** Creates a mock Environment missing the specified environment variable. */ + private static Environment createEnvironmentMissing(String missingVar) { + Map envVars = new HashMap<>(); + envVars.put("SYSTEM_ACCESSTOKEN", TEST_ACCESS_TOKEN); + envVars.put("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", TEST_COLLECTION_URI); + envVars.put("SYSTEM_PLANID", TEST_PLAN_ID); + envVars.put("SYSTEM_JOBID", TEST_JOB_ID); + envVars.put("SYSTEM_TEAMPROJECTID", TEST_PROJECT_ID); + envVars.put("SYSTEM_HOSTTYPE", TEST_HOST_TYPE); + envVars.remove(missingVar); // Remove the specified variable + return new Environment(envVars, new String[0], "test"); } - /** - * Test that audience parameter is ignored (Azure DevOps has hardcoded audience). - * This test verifies that the URL construction doesn't include audience parameter. - */ - @Test - void testAudienceParameterIgnored() throws IOException { - // This test can only run if environment variables are set (e.g., in Azure DevOps) - // Skip if not in Azure DevOps environment - if (System.getenv("SYSTEM_ACCESSTOKEN") == null) { - return; // Skip test if not in Azure DevOps environment + /** Creates a mock HttpClient that returns the specified response. */ + private static HttpClient createHttpMock( + String responseBody, int statusCode, IOException exception) throws IOException { + HttpClient client = mock(HttpClient.class); + if (exception != null) { + when(client.execute(any(Request.class))).thenThrow(exception); + } else { + when(client.execute(any(Request.class))).thenReturn(makeResponse(responseBody, statusCode)); } + return client; + } - // Arrange - String audience = "https://databricks.com"; - String expectedToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."; - String responseBody = String.format("{\"oidcToken\":\"%s\"}", expectedToken); - - when(response.getStatusCode()).thenReturn(200); - when(response.getBody()).thenReturn(new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8))); - when(httpClient.execute(any(Request.class))).thenReturn(response); + /** Creates a Response with the specified body and status code. */ + private static Response makeResponse(String body, int statusCode) throws MalformedURLException { + return new Response(body, statusCode, "OK", new URL("https://databricks.com/")); + } - AzureDevOpsIDTokenSource tokenSource = new AzureDevOpsIDTokenSource(httpClient); + /** Creates a mock HttpClient that returns a successful OIDC token response. */ + private static HttpClient createValidHttpMock() throws IOException { + return createHttpMock("{\"oidcToken\":\"" + TEST_ID_TOKEN + "\"}", 200, null); + } - // Act - IDToken result = tokenSource.getIDToken(audience); + /** Predicate to validate that the HTTP request is constructed correctly. */ + private static final Predicate REQUEST_VALIDATOR = + request -> + request.getMethod().equals("POST") + && request.getUri().toString().contains("api-version=7.2-preview.1") + && request.getUri().toString().contains(TEST_COLLECTION_URI) + && request.getUri().toString().contains(TEST_PROJECT_ID) + && request.getUri().toString().contains(TEST_HOST_TYPE) + && request.getUri().toString().contains(TEST_PLAN_ID) + && request.getUri().toString().contains(TEST_JOB_ID) + && request.getHeaders().get("Authorization").equals("Bearer " + TEST_ACCESS_TOKEN) + && request.getHeaders().get("Content-Type").equals("application/json"); - // Assert - assertNotNull(result); - assertEquals(expectedToken, result.getValue()); + private static Stream provideAllTestScenarios() throws IOException { + return Stream.of( + // Constructor validation tests + Arguments.of( + "Null HttpClient", + null, + createValidEnvironment(), + null, + null, + DatabricksException.class), + Arguments.of( + "Missing SYSTEM_ACCESSTOKEN", + mock(HttpClient.class), + createEnvironmentMissing("SYSTEM_ACCESSTOKEN"), + null, + null, + DatabricksException.class), + Arguments.of( + "Missing SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", + mock(HttpClient.class), + createEnvironmentMissing("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"), + null, + null, + DatabricksException.class), + Arguments.of( + "Missing SYSTEM_PLANID", + mock(HttpClient.class), + createEnvironmentMissing("SYSTEM_PLANID"), + null, + null, + DatabricksException.class), + Arguments.of( + "Missing SYSTEM_JOBID", + mock(HttpClient.class), + createEnvironmentMissing("SYSTEM_JOBID"), + null, + null, + DatabricksException.class), + Arguments.of( + "Missing SYSTEM_TEAMPROJECTID", + mock(HttpClient.class), + createEnvironmentMissing("SYSTEM_TEAMPROJECTID"), + null, + null, + DatabricksException.class), + Arguments.of( + "Missing SYSTEM_HOSTTYPE", + mock(HttpClient.class), + createEnvironmentMissing("SYSTEM_HOSTTYPE"), + null, + null, + DatabricksException.class), - // Verify the request URL does NOT include the audience parameter (it's ignored) - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); - verify(httpClient).execute(requestCaptor.capture()); - - Request capturedRequest = requestCaptor.getValue(); - String requestUri = capturedRequest.getUri().toString(); - assertFalse(requestUri.contains("audience="), - "Audience parameter should be ignored for Azure DevOps OIDC tokens"); - assertTrue(requestUri.contains("api-version=7.2-preview.1")); + // HTTP request/response tests + Arguments.of( + "Successful token retrieval", + createValidHttpMock(), + createValidEnvironment(), + REQUEST_VALIDATOR, + TEST_ID_TOKEN, + null), + Arguments.of( + "HTTP request failure", + createHttpMock(null, 0, new IOException("Network error")), + createValidEnvironment(), + null, + null, + DatabricksException.class), + Arguments.of( + "Non-200 status code", + createHttpMock("Error message", 401, null), + createValidEnvironment(), + null, + null, + DatabricksException.class), + Arguments.of( + "Invalid JSON response", + createHttpMock("invalid json", 200, null), + createValidEnvironment(), + null, + null, + DatabricksException.class), + Arguments.of( + "Missing oidcToken field", + createHttpMock("{\"someOtherField\":\"value\"}", 200, null), + createValidEnvironment(), + null, + null, + DatabricksException.class), + Arguments.of( + "Empty oidcToken field", + createHttpMock("{\"oidcToken\":\"\"}", 200, null), + createValidEnvironment(), + null, + null, + DatabricksException.class), + Arguments.of( + "Null oidcToken field", + createHttpMock("{\"oidcToken\":null}", 200, null), + createValidEnvironment(), + null, + null, + DatabricksException.class)); } - /** - * Note: Most environment variable validation tests are not included here - * since the class now reads directly from System.getenv(). These should be - * tested in integration tests where the environment can be controlled. - * - * The tests below focus on the core HTTP functionality that can be unit tested. - */ + @ParameterizedTest(name = "{0}") + @MethodSource("provideAllTestScenarios") + void testAllScenarios( + String testName, + HttpClient httpClient, + Environment environment, + Predicate requestValidator, + String expectedToken, + Class expectedException) { + + if (expectedException != null) { + // Test constructor or runtime exceptions + assertThrows( + expectedException, + () -> { + AzureDevOpsIDTokenSource tokenSource = + new AzureDevOpsIDTokenSource(httpClient, environment); + // If constructor succeeds, try getIDToken to trigger runtime exceptions + tokenSource.getIDToken("ignored-audience"); + }); + } else { + // Test successful cases + AzureDevOpsIDTokenSource tokenSource = new AzureDevOpsIDTokenSource(httpClient, environment); + IDToken token = tokenSource.getIDToken("ignored-audience"); + assertNotNull(token); + assertEquals(expectedToken, token.getValue()); + + // Verify the HTTP request was made correctly + if (requestValidator != null) { + try { + verify(httpClient).execute(argThat(request -> requestValidator.test(request))); + } catch (IOException e) { + fail("Unexpected IOException during request verification: " + e.getMessage()); + } + } + } + } } From 255739eb15be4f8b59ddb49e622039b7734d7f42 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Wed, 24 Sep 2025 11:15:50 +0000 Subject: [PATCH 03/14] testing changing push workflow --- .github/workflows/push.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 7f017fdfb..91b565317 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -15,6 +15,11 @@ jobs: with: java-version: 8 + - name: Set up Maven 3.9.11 + uses: stCarolas/setup-maven@v4.5 + with: + maven-version: 3.9.11 + - name: Checkout uses: actions/checkout@v2 From 2d0cd7b7c16811d6329e133ea2b8f2d7d2a03374 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Wed, 24 Sep 2025 11:20:45 +0000 Subject: [PATCH 04/14] trying JDK 11 --- .github/workflows/push.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 91b565317..00cd18226 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -10,10 +10,10 @@ jobs: fmt: runs-on: ubuntu-latest steps: - - name: Set up JDK 8 + - name: Set up JDK 11 uses: actions/setup-java@v1 with: - java-version: 8 + java-version: 11 - name: Set up Maven 3.9.11 uses: stCarolas/setup-maven@v4.5 From 5d38f2148eb0de2ed8d69cf6794af72ce664160c Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Wed, 24 Sep 2025 11:24:11 +0000 Subject: [PATCH 05/14] removed maven part in workflow --- .github/workflows/push.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 00cd18226..0f653b118 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -15,11 +15,6 @@ jobs: with: java-version: 11 - - name: Set up Maven 3.9.11 - uses: stCarolas/setup-maven@v4.5 - with: - maven-version: 3.9.11 - - name: Checkout uses: actions/checkout@v2 From d8833e532fd26306edf61064eef59862d0213510 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Wed, 24 Sep 2025 11:51:10 +0000 Subject: [PATCH 06/14] reverted to original workflow and fromatting --- .github/workflows/push.yml | 4 ++-- .../java/com/databricks/sdk/core/DatabricksConfig.java | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 0f653b118..7f017fdfb 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -10,10 +10,10 @@ jobs: fmt: runs-on: ubuntu-latest steps: - - name: Set up JDK 11 + - name: Set up JDK 8 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 8 - name: Checkout uses: actions/checkout@v2 diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java index 775b52a79..074e97974 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java @@ -431,17 +431,13 @@ public DatabricksConfig setAzureUseMsi(boolean azureUseMsi) { return this; } - /** - * @deprecated Use {@link #getAzureUseMsi()} instead. - */ + /** @deprecated Use {@link #getAzureUseMsi()} instead. */ @Deprecated() public boolean getAzureUseMSI() { return azureUseMsi; } - /** - * @deprecated Use {@link #getAzureUseMsi()} instead. - */ + /** @deprecated Use {@link #getAzureUseMsi()} instead. */ @Deprecated public DatabricksConfig setAzureUseMSI(boolean azureUseMsi) { this.azureUseMsi = azureUseMsi; From e167257f3b16d951f6dadb011e5df595ce72f586 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Wed, 24 Sep 2025 12:19:22 +0000 Subject: [PATCH 07/14] removed outdated github OIDC files --- .../core/oauth/GitHubOidcTokenSupplier.java | 79 ------------------- .../oauth/GithubOidcCredentialsProvider.java | 71 ----------------- 2 files changed, 150 deletions(-) delete mode 100644 databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/GitHubOidcTokenSupplier.java delete mode 100644 databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/GithubOidcCredentialsProvider.java diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/GitHubOidcTokenSupplier.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/GitHubOidcTokenSupplier.java deleted file mode 100644 index 523c0df1f..000000000 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/GitHubOidcTokenSupplier.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.databricks.sdk.core.oauth; - -import com.databricks.sdk.core.DatabricksException; -import com.databricks.sdk.core.http.HttpClient; -import com.databricks.sdk.core.http.Request; -import com.databricks.sdk.core.http.Response; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.io.IOException; - -public class GitHubOidcTokenSupplier { - - private final ObjectMapper mapper = new ObjectMapper(); - private final HttpClient httpClient; - private final String idTokenRequestUrl; - private final String idTokenRequestToken; - private final String tokenAudience; - - public GitHubOidcTokenSupplier( - HttpClient httpClient, - String idTokenRequestUrl, - String idTokenRequestToken, - String tokenAudience) { - this.httpClient = httpClient; - this.idTokenRequestUrl = idTokenRequestUrl; - this.idTokenRequestToken = idTokenRequestToken; - this.tokenAudience = tokenAudience; - } - - /** Checks if the required parameters are present to request a GitHub's OIDC token. */ - public Boolean enabled() { - return idTokenRequestUrl != null && idTokenRequestToken != null; - } - - /** - * Requests a GitHub's OIDC token. - * - * @return A GitHub OIDC token. - */ - public String getOidcToken() { - if (!enabled()) { - throw new DatabricksException("Failed to request ID token: missing required parameters"); - } - - String requestUrl = idTokenRequestUrl; - if (tokenAudience != null) { - requestUrl += "&audience=" + tokenAudience; - } - - Request req = - new Request("GET", requestUrl).withHeader("Authorization", "Bearer " + idTokenRequestToken); - - Response resp; - try { - resp = httpClient.execute(req); - } catch (IOException e) { - throw new DatabricksException( - "Failed to request ID token from " + requestUrl + ":" + e.getMessage(), e); - } - - if (resp.getStatusCode() != 200) { - throw new DatabricksException( - "Failed to request ID token: status code " - + resp.getStatusCode() - + ", response body: " - + resp.getBody().toString()); - } - - ObjectNode jsonResp; - try { - jsonResp = mapper.readValue(resp.getBody(), ObjectNode.class); - } catch (IOException e) { - throw new DatabricksException( - "Failed to request ID token: corrupted token: " + e.getMessage()); - } - - return jsonResp.get("value").textValue(); - } -} diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/GithubOidcCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/GithubOidcCredentialsProvider.java deleted file mode 100644 index c52fcf09d..000000000 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/GithubOidcCredentialsProvider.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.databricks.sdk.core.oauth; - -import com.databricks.sdk.core.CredentialsProvider; -import com.databricks.sdk.core.DatabricksConfig; -import com.databricks.sdk.core.DatabricksException; -import com.databricks.sdk.core.HeaderFactory; -import com.google.common.collect.ImmutableMap; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -/** - * GithubOidcCredentialsProvider uses a Token Supplier to get a GitHub OIDC JWT Token and exchanges - * it for a Databricks Token. - */ -public class GithubOidcCredentialsProvider implements CredentialsProvider { - - @Override - public String authType() { - return "github-oidc"; - } - - @Override - public HeaderFactory configure(DatabricksConfig config) throws DatabricksException { - GitHubOidcTokenSupplier idTokenProvider = - new GitHubOidcTokenSupplier( - config.getHttpClient(), - config.getActionsIdTokenRequestUrl(), - config.getActionsIdTokenRequestToken(), - config.getTokenAudience()); - - if (!idTokenProvider.enabled() || config.getHost() == null || config.getClientId() == null) { - return null; - } - - String endpointUrl; - - try { - endpointUrl = config.getOidcEndpoints().getTokenEndpoint(); - } catch (IOException e) { - throw new DatabricksException("Unable to fetch OIDC endpoint: " + e.getMessage(), e); - } - - ClientCredentials clientCredentials = - new ClientCredentials.Builder() - .withHttpClient(config.getHttpClient()) - .withClientId(config.getClientId()) - .withTokenUrl(endpointUrl) - .withScopes(config.getScopes()) - .withAuthParameterPosition(AuthParameterPosition.HEADER) - .withEndpointParametersSupplier( - () -> - new ImmutableMap.Builder() - .put("subject_token_type", "urn:ietf:params:oauth:token-type:jwt") - .put("subject_token", idTokenProvider.getOidcToken()) - .put("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") - .build()) - .build(); - - CachedTokenSource cachedTokenSource = - new CachedTokenSource.Builder(clientCredentials) - .setAsyncDisabled(config.getDisableAsyncTokenRefresh()) - .build(); - - return () -> { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + cachedTokenSource.getToken().getAccessToken()); - return headers; - }; - } -} From 770a6f7eccca9e17c0f685f5832d92e1fe0f9fc8 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia <171924202+Divyansh-db@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:08:59 +0200 Subject: [PATCH 08/14] Update databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java Co-authored-by: Renaud Hartert --- .../databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java index 0717f070b..99ec374d2 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java @@ -15,8 +15,7 @@ * the IDTokenSource interface and provides a method for obtaining ID tokens specifically from Azure * DevOps Pipeline environment. * - *

This implementation follows the Azure DevOps OIDC token API as documented at: - * https://learn.microsoft.com/en-us/rest/api/azure/devops/distributedtask/oidctoken/create + *

This implementation relies on the Azure DevOps OIDC token API. */ public class AzureDevOpsIDTokenSource implements IDTokenSource { /* Access token for authenticating with Azure DevOps API */ From f33405dc36b480d8a58ddb1c7de40fca607907d5 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia <171924202+Divyansh-db@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:09:38 +0200 Subject: [PATCH 09/14] Update databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java Co-authored-by: Renaud Hartert --- .../databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java index 99ec374d2..3e1f2c207 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java @@ -95,11 +95,11 @@ private String validateEnvironmentVariable(String varName) { if (varName.equals("SYSTEM_ACCESSTOKEN")) { throw new DatabricksException( String.format( - "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", + "Missing environment variable %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", varName)); } throw new DatabricksException( - String.format("Missing %s, likely not calling from Azure DevOps Pipeline", varName)); + String.format("Missing environment variable %s, likely not calling from Azure DevOps Pipeline", varName)); } return value; } From daa4122abdc20241d6c2f891549b599d0cf04242 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia <171924202+Divyansh-db@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:10:03 +0200 Subject: [PATCH 10/14] Update databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java Co-authored-by: Renaud Hartert --- .../databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java index 3e1f2c207..17a0a3fb8 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java @@ -153,8 +153,6 @@ public IDToken getIDToken(String audience) { + resp.getBody().toString()); } - // Parse the JSON response - // Azure DevOps returns {"oidcToken":"***"} format, not {"value":"***"} like GitHub Actions ObjectNode jsonResp; try { jsonResp = mapper.readValue(resp.getBody(), ObjectNode.class); @@ -163,6 +161,7 @@ public IDToken getIDToken(String audience) { "Failed to parse Azure DevOps OIDC token response: " + e.getMessage(), e); } + // Azure DevOps returns {"oidcToken":"***"} format, not {"value":"***"} like GitHub Actions. if (!jsonResp.has("oidcToken")) { throw new DatabricksException("Azure DevOps OIDC token response missing 'oidcToken' field"); } From e2e8936e515b48d973ae5e18e78117ba7728a1be Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia <171924202+Divyansh-db@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:10:14 +0200 Subject: [PATCH 11/14] Update databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java Co-authored-by: Renaud Hartert --- .../sdk/core/oauth/AzureDevOpsIDTokenSource.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java index 17a0a3fb8..8f8a1e464 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java @@ -168,13 +168,14 @@ public IDToken getIDToken(String audience) { try { String tokenValue = jsonResp.get("oidcToken").textValue(); - if (Strings.isNullOrEmpty(tokenValue)) { - throw new DatabricksException("Received empty OIDC token from Azure DevOps"); - } - return new IDToken(tokenValue); } catch (IllegalArgumentException e) { throw new DatabricksException( "Received invalid OIDC token from Azure DevOps: " + e.getMessage(), e); } + + if (Strings.isNullOrEmpty(tokenValue)) { + throw new DatabricksException("Received empty OIDC token from Azure DevOps"); + } + return new IDToken(tokenValue); } } From 0c49d8b7b0cc399e7cf8d82334c226b2086b310a Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia <171924202+Divyansh-db@users.noreply.github.com> Date: Tue, 30 Sep 2025 16:10:33 +0200 Subject: [PATCH 12/14] Update databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java Co-authored-by: Renaud Hartert --- .../com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java index 8f8a1e464..f989246e0 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java @@ -120,8 +120,6 @@ private String validateEnvironmentVariable(String varName) { public IDToken getIDToken(String audience) { // Build Azure DevOps OIDC endpoint URL. - // Format: - // {collectionUri}/{teamProjectId}/_apis/distributedtask/hubs/{hostType}/plans/{planId}/jobs/{jobId}/oidctoken?api-version=7.2-preview.1 String requestUrl = String.format( "%s/%s/_apis/distributedtask/hubs/%s/plans/%s/jobs/%s/oidctoken?api-version=7.2-preview.1", From b6f1e48aed2ee5c4dd02819082a04f6e21bb80f4 Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Tue, 30 Sep 2025 14:19:05 +0000 Subject: [PATCH 13/14] chaned constructor to protected --- NEXT_CHANGELOG.md | 2 +- .../com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 8cd2954fd..da376b995 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -4,7 +4,7 @@ ### New Features and Improvements -* Add native support for Azure DevOps OIDC authentication +* Add native support for Azure DevOps OIDC authentication. ### Bug Fixes diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java index f989246e0..5d12e9d0c 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java @@ -58,7 +58,7 @@ public AzureDevOpsIDTokenSource(HttpClient httpClient) { * @throws DatabricksException if httpClient is null or any required environment variables are * missing */ - public AzureDevOpsIDTokenSource(HttpClient httpClient, Environment environment) { + protected AzureDevOpsIDTokenSource(HttpClient httpClient, Environment environment) { if (httpClient == null) { throw new DatabricksException("HttpClient cannot be null"); } From fc83237d461335e79dbda9f63779169f3d227a1e Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Tue, 30 Sep 2025 14:44:54 +0000 Subject: [PATCH 14/14] refactored code --- .../core/oauth/AzureDevOpsIDTokenSource.java | 18 ++++++++---------- .../oauth/AzureDevOpsIDTokenSourceTest.java | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java index 5d12e9d0c..0e1e747af 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSource.java @@ -15,7 +15,9 @@ * the IDTokenSource interface and provides a method for obtaining ID tokens specifically from Azure * DevOps Pipeline environment. * - *

This implementation relies on the Azure DevOps OIDC token API. + *

This implementation relies on the Azure + * DevOps OIDC token API. */ public class AzureDevOpsIDTokenSource implements IDTokenSource { /* Access token for authenticating with Azure DevOps API */ @@ -99,7 +101,9 @@ private String validateEnvironmentVariable(String varName) { varName)); } throw new DatabricksException( - String.format("Missing environment variable %s, likely not calling from Azure DevOps Pipeline", varName)); + String.format( + "Missing environment variable %s, likely not calling from Azure DevOps Pipeline", + varName)); } return value; } @@ -164,15 +168,9 @@ public IDToken getIDToken(String audience) { throw new DatabricksException("Azure DevOps OIDC token response missing 'oidcToken' field"); } - try { - String tokenValue = jsonResp.get("oidcToken").textValue(); - } catch (IllegalArgumentException e) { - throw new DatabricksException( - "Received invalid OIDC token from Azure DevOps: " + e.getMessage(), e); - } - + String tokenValue = jsonResp.get("oidcToken").textValue(); if (Strings.isNullOrEmpty(tokenValue)) { - throw new DatabricksException("Received empty OIDC token from Azure DevOps"); + throw new DatabricksException("Received empty OIDC token from Azure DevOps"); } return new IDToken(tokenValue); } diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java index 26c9eeebf..6e59cf073 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/oauth/AzureDevOpsIDTokenSourceTest.java @@ -194,6 +194,20 @@ private static Stream provideAllTestScenarios() throws IOException { createValidEnvironment(), null, null, + DatabricksException.class), + Arguments.of( + "Non-string oidcToken field (number)", + createHttpMock("{\"oidcToken\":123}", 200, null), + createValidEnvironment(), + null, + null, + DatabricksException.class), + Arguments.of( + "Non-string oidcToken field (object)", + createHttpMock("{\"oidcToken\":{\"nested\":\"value\"}}", 200, null), + createValidEnvironment(), + null, + null, DatabricksException.class)); }