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 8388c218e..627b0ab2f 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 @@ -5,6 +5,7 @@ import com.databricks.sdk.core.http.Request; import com.databricks.sdk.core.http.Response; import com.databricks.sdk.core.oauth.ErrorTokenSource; +import com.databricks.sdk.core.oauth.HostMetadata; import com.databricks.sdk.core.oauth.OAuthHeaderFactory; import com.databricks.sdk.core.oauth.OpenIDConnectEndpoints; import com.databricks.sdk.core.oauth.TokenSource; @@ -800,6 +801,71 @@ public OpenIDConnectEndpoints getDatabricksOidcEndpoints() throws IOException { return fetchOidcEndpointsFromDiscovery(); } + /** + * [Experimental] Fetch the raw Databricks well-known configuration from + * {host}/.well-known/databricks-config. + * + *

Note: This API is experimental and may change or be removed in future releases + * without notice. + * + * @return Parsed {@link HostMetadata} as returned by the server. + * @throws DatabricksException if the request fails or the server returns a non-200 status. + */ + HostMetadata getHostMetadata() throws IOException { + String url = host + "/.well-known/databricks-config"; + try { + Request request = new Request("GET", url); + Response resp = getHttpClient().execute(request); + if (resp.getStatusCode() != 200) { + throw new DatabricksException( + "Failed to fetch host metadata from " + url + ": HTTP " + resp.getStatusCode()); + } + return new ObjectMapper().readValue(resp.getBody(), HostMetadata.class); + } catch (IOException e) { + throw new DatabricksException( + "Failed to fetch host metadata from " + url + ": " + e.getMessage(), e); + } + } + + /** + * [Experimental] Populate missing config fields from the host's /.well-known/databricks-config + * discovery endpoint. + * + *

Fills in {@code accountId}, {@code workspaceId}, and {@code discoveryUrl} (derived from + * {@code oidc_endpoint}, with any {@code {account_id}} placeholder substituted) if not already + * set. + * + *

Note: This API is experimental and may change or be removed in future releases + * without notice. + * + * @throws DatabricksException if {@code accountId} cannot be resolved or {@code oidc_endpoint} is + * missing from the host metadata. + */ + void resolveHostMetadata() throws IOException { + if (host == null) { + return; + } + HostMetadata meta = getHostMetadata(); + if (accountId == null && meta.getAccountId() != null) { + accountId = meta.getAccountId(); + } + if (accountId == null) { + throw new DatabricksException( + "account_id is not configured and could not be resolved from host metadata"); + } + if (workspaceId == null && meta.getWorkspaceId() != null) { + workspaceId = meta.getWorkspaceId(); + } + if (discoveryUrl == null) { + if (meta.getOidcEndpoint() != null && !meta.getOidcEndpoint().isEmpty()) { + discoveryUrl = meta.getOidcEndpoint().replace("{account_id}", accountId); + } else { + throw new DatabricksException( + "discovery_url is not configured and could not be resolved from host metadata"); + } + } + } + private OpenIDConnectEndpoints fetchOidcEndpointsFromDiscovery() { try { Request request = new Request("GET", discoveryUrl); diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/HostMetadata.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/HostMetadata.java new file mode 100644 index 000000000..d45844262 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/HostMetadata.java @@ -0,0 +1,42 @@ +package com.databricks.sdk.core.oauth; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * [Experimental] Parsed response from the /.well-known/databricks-config discovery endpoint. + * + *

Note: This API is experimental and may change or be removed in future releases without + * notice. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class HostMetadata { + @JsonProperty("oidc_endpoint") + private String oidcEndpoint; + + @JsonProperty("account_id") + private String accountId; + + @JsonProperty("workspace_id") + private String workspaceId; + + public HostMetadata() {} + + public HostMetadata(String oidcEndpoint, String accountId, String workspaceId) { + this.oidcEndpoint = oidcEndpoint; + this.accountId = accountId; + this.workspaceId = workspaceId; + } + + public String getOidcEndpoint() { + return oidcEndpoint; + } + + public String getAccountId() { + return accountId; + } + + public String getWorkspaceId() { + return workspaceId; + } +} diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksConfigTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksConfigTest.java index ec7f0395f..378581195 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksConfigTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksConfigTest.java @@ -6,6 +6,7 @@ import com.databricks.sdk.core.commons.CommonsHttpClient; import com.databricks.sdk.core.http.HttpClient; import com.databricks.sdk.core.oauth.ErrorTokenSource; +import com.databricks.sdk.core.oauth.HostMetadata; import com.databricks.sdk.core.oauth.OAuthHeaderFactory; import com.databricks.sdk.core.oauth.OpenIDConnectEndpoints; import com.databricks.sdk.core.oauth.Token; @@ -421,4 +422,192 @@ public void testGetClientTypeAccountOnUnified() { .setExperimentalIsUnifiedHost(true) .getClientType()); } + + // --- HostMetadata tests --- + + private static final String DUMMY_ACCOUNT_ID = "00000000-0000-0000-0000-000000000001"; + private static final String DUMMY_WORKSPACE_ID = "111111111111111"; + + private static Environment emptyEnv() { + return new Environment(new HashMap<>(), new ArrayList<>(), System.getProperty("os.name")); + } + + @Test + public void testGetHostMetadataWorkspaceStaticOidcEndpoint() throws IOException { + String response = + "{\"oidc_endpoint\":\"https://ws.databricks.com/oidc\"," + + "\"account_id\":\"" + + DUMMY_ACCOUNT_ID + + "\"," + + "\"workspace_id\":\"" + + DUMMY_WORKSPACE_ID + + "\"}"; + try (FixtureServer server = + new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) { + DatabricksConfig config = new DatabricksConfig().setHost(server.getUrl()); + config.resolve(emptyEnv()); + HostMetadata meta = config.getHostMetadata(); + assertEquals("https://ws.databricks.com/oidc", meta.getOidcEndpoint()); + assertEquals(DUMMY_ACCOUNT_ID, meta.getAccountId()); + assertEquals(DUMMY_WORKSPACE_ID, meta.getWorkspaceId()); + } + } + + @Test + public void testGetHostMetadataAccountRawOidcTemplate() throws IOException { + String response = + "{\"oidc_endpoint\":\"https://acc.databricks.com/oidc/accounts/{account_id}\"}"; + try (FixtureServer server = + new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) { + DatabricksConfig config = new DatabricksConfig().setHost(server.getUrl()); + config.resolve(emptyEnv()); + HostMetadata meta = config.getHostMetadata(); + assertEquals("https://acc.databricks.com/oidc/accounts/{account_id}", meta.getOidcEndpoint()); + assertNull(meta.getAccountId()); + assertNull(meta.getWorkspaceId()); + } + } + + @Test + public void testGetHostMetadataRaisesOnHttpError() throws IOException { + try (FixtureServer server = + new FixtureServer().with("GET", "/.well-known/databricks-config", "{}", 404)) { + DatabricksConfig config = new DatabricksConfig().setHost(server.getUrl()); + config.resolve(emptyEnv()); + DatabricksException ex = + assertThrows(DatabricksException.class, () -> config.getHostMetadata()); + assertTrue(ex.getMessage().contains("Failed to fetch host metadata")); + } + } + + // --- resolveHostMetadata tests --- + + @Test + public void testResolveHostMetadataWorkspacePopulatesAllFields() throws IOException { + String response = + "{\"oidc_endpoint\":\"https://ws.databricks.com/oidc\"," + + "\"account_id\":\"" + + DUMMY_ACCOUNT_ID + + "\"," + + "\"workspace_id\":\"" + + DUMMY_WORKSPACE_ID + + "\"}"; + try (FixtureServer server = + new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) { + DatabricksConfig config = new DatabricksConfig().setHost(server.getUrl()); + config.resolve(emptyEnv()); + config.resolveHostMetadata(); + assertEquals(DUMMY_ACCOUNT_ID, config.getAccountId()); + assertEquals(DUMMY_WORKSPACE_ID, config.getWorkspaceId()); + assertEquals("https://ws.databricks.com/oidc", config.getDiscoveryUrl()); + } + } + + @Test + public void testResolveHostMetadataAccountSubstitutesAccountId() throws IOException { + String response = + "{\"oidc_endpoint\":\"https://acc.databricks.com/oidc/accounts/{account_id}\"}"; + try (FixtureServer server = + new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) { + DatabricksConfig config = + new DatabricksConfig().setHost(server.getUrl()).setAccountId(DUMMY_ACCOUNT_ID); + config.resolve(emptyEnv()); + config.resolveHostMetadata(); + assertEquals( + "https://acc.databricks.com/oidc/accounts/" + DUMMY_ACCOUNT_ID, config.getDiscoveryUrl()); + } + } + + @Test + public void testResolveHostMetadataDoesNotOverwriteExistingFields() throws IOException { + String existingAccountId = "existing-account-id"; + String existingWorkspaceId = "existing-workspace-id"; + String response = + "{\"oidc_endpoint\":\"https://ws.databricks.com/oidc\"," + + "\"account_id\":\"other-account\"," + + "\"workspace_id\":\"other-ws\"}"; + try (FixtureServer server = + new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) { + DatabricksConfig config = + new DatabricksConfig() + .setHost(server.getUrl()) + .setAccountId(existingAccountId) + .setWorkspaceId(existingWorkspaceId); + config.resolve(emptyEnv()); + config.resolveHostMetadata(); + assertEquals(existingAccountId, config.getAccountId()); + assertEquals(existingWorkspaceId, config.getWorkspaceId()); + } + } + + @Test + public void testResolveHostMetadataRaisesWhenAccountIdUnresolvable() throws IOException { + String response = + "{\"oidc_endpoint\":\"https://acc.databricks.com/oidc/accounts/{account_id}\"}"; + try (FixtureServer server = + new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) { + DatabricksConfig config = new DatabricksConfig().setHost(server.getUrl()); + config.resolve(emptyEnv()); + DatabricksException ex = + assertThrows(DatabricksException.class, () -> config.resolveHostMetadata()); + assertTrue(ex.getMessage().contains("account_id is not configured")); + } + } + + @Test + public void testResolveHostMetadataRaisesWhenOidcEndpointMissing() throws IOException { + String response = "{\"account_id\":\"" + DUMMY_ACCOUNT_ID + "\"}"; + try (FixtureServer server = + new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) { + DatabricksConfig config = new DatabricksConfig().setHost(server.getUrl()); + config.resolve(emptyEnv()); + DatabricksException ex = + assertThrows(DatabricksException.class, () -> config.resolveHostMetadata()); + assertTrue(ex.getMessage().contains("discovery_url is not configured")); + } + } + + @Test + public void testResolveHostMetadataRaisesOnHttpError() throws IOException { + try (FixtureServer server = + new FixtureServer().with("GET", "/.well-known/databricks-config", "{}", 500)) { + DatabricksConfig config = new DatabricksConfig().setHost(server.getUrl()); + config.resolve(emptyEnv()); + DatabricksException ex = + assertThrows(DatabricksException.class, () -> config.resolveHostMetadata()); + assertTrue(ex.getMessage().contains("Failed to fetch host metadata")); + } + } + + // --- discoveryUrl / OIDC endpoint tests --- + + @Test + public void testDiscoveryUrlFromEnv() { + Map env = new HashMap<>(); + env.put("DATABRICKS_DISCOVERY_URL", "https://custom.idp.example.com/oidc"); + DatabricksConfig config = new DatabricksConfig(); + config.resolve(new Environment(env, new ArrayList<>(), System.getProperty("os.name"))); + assertEquals("https://custom.idp.example.com/oidc", config.getDiscoveryUrl()); + } + + @Test + public void testDatabricksOidcEndpointsUsesDiscoveryUrl() throws IOException { + String discoveryUrlSuffix = "/oidc"; + String discoveryUrlResponse = + "{\"authorization_endpoint\":\"https://ws.databricks.com/oidc/v1/authorize\"," + + "\"token_endpoint\":\"https://ws.databricks.com/oidc/v1/token\"}"; + try (FixtureServer server = + new FixtureServer().with("GET", discoveryUrlSuffix, discoveryUrlResponse, 200)) { + String discoveryUrl = server.getUrl() + discoveryUrlSuffix; + OpenIDConnectEndpoints endpoints = + new DatabricksConfig() + .setHost(server.getUrl()) + .setDiscoveryUrl(discoveryUrl) + .setHttpClient(new CommonsHttpClient.Builder().withTimeoutSeconds(30).build()) + .getDatabricksOidcEndpoints(); + assertEquals( + "https://ws.databricks.com/oidc/v1/authorize", endpoints.getAuthorizationEndpoint()); + assertEquals("https://ws.databricks.com/oidc/v1/token", endpoints.getTokenEndpoint()); + } + } }