1818import java .util .*;
1919import org .apache .http .HttpMessage ;
2020
21+ /**
22+ * Configuration for Databricks SDK clients.
23+ *
24+ * <p>This class holds all configuration needed to authenticate and connect to Databricks services,
25+ * including support for:
26+ *
27+ * <ul>
28+ * <li>Traditional workspace and account hosts
29+ * <li>Unified hosts (SPOG) that support both workspace and account operations
30+ * <li>Multiple authentication methods (PAT, OAuth, Azure, etc.)
31+ * </ul>
32+ *
33+ * <p><b>Unified Host Support:</b> When using a unified host, set {@code experimentalIsUnifiedHost}
34+ * to {@code true} and optionally provide a {@code workspaceId} for workspace-scoped operations. Use
35+ * {@link #getHostType()} and {@link #getClientType()} instead of the deprecated {@link
36+ * #isAccountClient()} method.
37+ */
2138public class DatabricksConfig {
2239 private CredentialsProvider credentialsProvider = new DefaultCredentialsProvider ();
2340
@@ -27,6 +44,27 @@ public class DatabricksConfig {
2744 @ ConfigAttribute (env = "DATABRICKS_ACCOUNT_ID" )
2845 private String accountId ;
2946
47+ /**
48+ * Workspace ID for unified host operations. When using a unified host that supports both
49+ * workspace and account-level operations, this field specifies which workspace context to operate
50+ * under for workspace-level API calls.
51+ *
52+ * <p><b>Note:</b> This API is experimental and may change or be removed in future releases
53+ * without notice.
54+ */
55+ @ ConfigAttribute (env = "DATABRICKS_WORKSPACE_ID" )
56+ private String workspaceId ;
57+
58+ /**
59+ * Flag to explicitly mark a host as a unified host. When true, the host is treated as supporting
60+ * both workspace and account-level operations through a single endpoint.
61+ *
62+ * <p><b>Note:</b> This API is experimental and may change or be removed in future releases
63+ * without notice.
64+ */
65+ @ ConfigAttribute (env = "DATABRICKS_EXPERIMENTAL_IS_UNIFIED_HOST" )
66+ private Boolean experimentalIsUnifiedHost ;
67+
3068 @ ConfigAttribute (env = "DATABRICKS_TOKEN" , auth = "pat" , sensitive = true )
3169 private String token ;
3270
@@ -233,8 +271,16 @@ public synchronized Map<String, String> authenticate() throws DatabricksExceptio
233271 if (headerFactory == null ) {
234272 // Calling authenticate without resolve
235273 ConfigLoader .fixHostIfNeeded (this );
236- headerFactory = credentialsProvider .configure (this );
274+ HeaderFactory rawHeaderFactory = credentialsProvider .configure (this );
237275 setAuthType (credentialsProvider .authType ());
276+
277+ // For unified hosts with workspace operations, wrap the header factory
278+ // to inject the X-Databricks-Org-Id header
279+ if (getClientType () == ClientType .WORKSPACE_ON_UNIFIED ) {
280+ headerFactory = new UnifiedHostHeaderFactory (rawHeaderFactory , workspaceId );
281+ } else {
282+ headerFactory = rawHeaderFactory ;
283+ }
238284 }
239285 return headerFactory .headers ();
240286 } catch (DatabricksException e ) {
@@ -298,6 +344,24 @@ public DatabricksConfig setAccountId(String accountId) {
298344 return this ;
299345 }
300346
347+ public String getWorkspaceId () {
348+ return workspaceId ;
349+ }
350+
351+ public DatabricksConfig setWorkspaceId (String workspaceId ) {
352+ this .workspaceId = workspaceId ;
353+ return this ;
354+ }
355+
356+ public Boolean getExperimentalIsUnifiedHost () {
357+ return experimentalIsUnifiedHost ;
358+ }
359+
360+ public DatabricksConfig setExperimentalIsUnifiedHost (Boolean experimentalIsUnifiedHost ) {
361+ this .experimentalIsUnifiedHost = experimentalIsUnifiedHost ;
362+ return this ;
363+ }
364+
301365 public String getDatabricksCliPath () {
302366 return this .databricksCliPath ;
303367 }
@@ -679,12 +743,73 @@ public boolean isAws() {
679743 }
680744
681745 public boolean isAccountClient () {
746+ if (getHostType () == HostType .UNIFIED ) {
747+ throw new DatabricksException (
748+ "Cannot determine account client status for unified hosts. "
749+ + "Use getHostType() or getClientType() instead. "
750+ + "For unified hosts, client type depends on whether workspaceId is set." );
751+ }
682752 if (host == null ) {
683753 return false ;
684754 }
685755 return host .startsWith ("https://accounts." ) || host .startsWith ("https://accounts-dod." );
686756 }
687757
758+ /**
759+ * Determines the type of host based on configuration settings and host URL.
760+ *
761+ * <p>Detection logic:
762+ *
763+ * <ol>
764+ * <li>If experimentalIsUnifiedHost is true → UNIFIED
765+ * <li>If host starts with "accounts." or "accounts-dod." → ACCOUNTS
766+ * <li>Otherwise → WORKSPACE
767+ * </ol>
768+ *
769+ * @return The detected host type
770+ */
771+ public HostType getHostType () {
772+ if (experimentalIsUnifiedHost != null && experimentalIsUnifiedHost ) {
773+ return HostType .UNIFIED ;
774+ }
775+ if (host == null ) {
776+ return HostType .WORKSPACE ;
777+ }
778+ if (host .startsWith ("https://accounts." ) || host .startsWith ("https://accounts-dod." )) {
779+ return HostType .ACCOUNTS ;
780+ }
781+ return HostType .WORKSPACE ;
782+ }
783+
784+ /**
785+ * Determines the client type based on host type and workspace ID configuration.
786+ *
787+ * <p>Client type logic:
788+ *
789+ * <ul>
790+ * <li>UNIFIED host + workspaceId set → WORKSPACE_ON_UNIFIED
791+ * <li>UNIFIED host + no workspaceId → ACCOUNT_ON_UNIFIED
792+ * <li>ACCOUNTS host → ACCOUNT
793+ * <li>WORKSPACE host → WORKSPACE
794+ * </ul>
795+ *
796+ * @return The determined client type
797+ */
798+ public ClientType getClientType () {
799+ HostType hostType = getHostType ();
800+ switch (hostType ) {
801+ case UNIFIED :
802+ return (workspaceId != null && !workspaceId .isEmpty ())
803+ ? ClientType .WORKSPACE_ON_UNIFIED
804+ : ClientType .ACCOUNT_ON_UNIFIED ;
805+ case ACCOUNTS :
806+ return ClientType .ACCOUNT ;
807+ case WORKSPACE :
808+ default :
809+ return ClientType .WORKSPACE ;
810+ }
811+ }
812+
688813 public OpenIDConnectEndpoints getOidcEndpoints () throws IOException {
689814 if (discoveryUrl == null ) {
690815 return fetchDefaultOidcEndpoints ();
@@ -705,10 +830,36 @@ private OpenIDConnectEndpoints fetchOidcEndpointsFromDiscovery() {
705830 return null ;
706831 }
707832
833+ /**
834+ * Fetches OIDC endpoints for unified hosts using the account ID.
835+ *
836+ * <p>For unified hosts, the OIDC endpoints follow the pattern:
837+ * {host}/oidc/accounts/{accountId}/v1/{token|authorize}
838+ *
839+ * @param accountId The account ID to use for endpoint construction
840+ * @return OpenIDConnectEndpoints configured for the unified host
841+ * @throws DatabricksException if accountId is null or empty
842+ * @throws IOException if endpoint construction fails
843+ */
844+ private OpenIDConnectEndpoints getUnifiedOidcEndpoints (String accountId ) throws IOException {
845+ if (accountId == null || accountId .isEmpty ()) {
846+ throw new DatabricksException (
847+ "account_id is required for unified host OIDC endpoint discovery" );
848+ }
849+ String prefix = getHost () + "/oidc/accounts/" + accountId ;
850+ return new OpenIDConnectEndpoints (prefix + "/v1/token" , prefix + "/v1/authorize" );
851+ }
852+
708853 private OpenIDConnectEndpoints fetchDefaultOidcEndpoints () throws IOException {
709854 if (getHost () == null ) {
710855 return null ;
711856 }
857+
858+ // For unified hosts, use account-based OIDC endpoints
859+ if (getHostType () == HostType .UNIFIED ) {
860+ return getUnifiedOidcEndpoints (getAccountId ());
861+ }
862+
712863 if (isAzure () && getAzureClientId () != null ) {
713864 Request request = new Request ("GET" , getHost () + "/oidc/oauth2/v2.0/authorize" );
714865 request .setRedirectionBehavior (false );
0 commit comments