From bc5cb4cd7f03df913a6b9d49a720f13b4b3191ec Mon Sep 17 00:00:00 2001 From: Hector Castejon Diaz Date: Thu, 9 Apr 2026 12:26:29 +0000 Subject: [PATCH 1/3] Add X-Databricks-Org-Id header to deprecated workspace SCIM APIs The deprecated Groups, ServicePrincipals, and Users workspace services were missing the X-Databricks-Org-Id header that all other workspace services include when workspaceId is configured. This is required for SPOG (unified) host compatibility. Co-authored-by: Isaac Signed-off-by: Hector Castejon Diaz --- NEXT_CHANGELOG.md | 3 +- .../sdk/service/iam/GroupsImpl.java | 18 ++++++++++ .../service/iam/ServicePrincipalsImpl.java | 18 ++++++++++ .../databricks/sdk/service/iam/UsersImpl.java | 30 ++++++++++++++++ .../sdk/integration/UnifiedHostGroupsIT.java | 36 +++++++++++++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 0bd9e224b..e7017db7a 100755 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -6,6 +6,7 @@ * Added automatic detection of AI coding agents (Antigravity, Claude Code, Cline, Codex, Copilot CLI, Cursor, Gemini CLI, OpenCode) in the user-agent string. The SDK now appends `agent/` to HTTP request headers when running inside a known AI agent environment. ### Bug Fixes +* Added `X-Databricks-Org-Id` header to deprecated workspace SCIM APIs (Groups, ServicePrincipals, Users) for SPOG host compatibility. * Fixed Databricks CLI authentication to detect when the cached token's scopes don't match the SDK's configured scopes. Previously, a scope mismatch was silently ignored, causing requests to use wrong permissions. The SDK now raises an error with instructions to re-authenticate. ### Security Vulnerabilities @@ -23,4 +24,4 @@ * Add `cascade` field for `com.databricks.sdk.service.pipelines.DeletePipelineRequest`. * Add `defaultBranch` field for `com.databricks.sdk.service.postgres.ProjectSpec`. * Add `defaultBranch` field for `com.databricks.sdk.service.postgres.ProjectStatus`. -* Add `ingress` and `ingressDryRun` fields for `com.databricks.sdk.service.settings.AccountNetworkPolicy`. \ No newline at end of file +* Add `ingress` and `ingressDryRun` fields for `com.databricks.sdk.service.settings.AccountNetworkPolicy`. diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/GroupsImpl.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/GroupsImpl.java index fabddee61..196538b23 100755 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/GroupsImpl.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/GroupsImpl.java @@ -24,6 +24,9 @@ public Group create(Group request) { ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, Group.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -36,6 +39,9 @@ public void delete(DeleteGroupRequest request) { try { Request req = new Request("DELETE", path); ApiClient.setQuery(req, request); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } apiClient.execute(req, Void.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -49,6 +55,9 @@ public Group get(GetGroupRequest request) { Request req = new Request("GET", path); ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, Group.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -62,6 +71,9 @@ public ListGroupsResponse list(ListGroupsRequest request) { Request req = new Request("GET", path); ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, ListGroupsResponse.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -75,6 +87,9 @@ public void patch(PartialUpdate request) { Request req = new Request("PATCH", path, apiClient.serialize(request)); ApiClient.setQuery(req, request); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } apiClient.execute(req, Void.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -88,6 +103,9 @@ public void update(Group request) { Request req = new Request("PUT", path, apiClient.serialize(request)); ApiClient.setQuery(req, request); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } apiClient.execute(req, Void.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/ServicePrincipalsImpl.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/ServicePrincipalsImpl.java index f43216c63..ec79919cb 100755 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/ServicePrincipalsImpl.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/ServicePrincipalsImpl.java @@ -24,6 +24,9 @@ public ServicePrincipal create(ServicePrincipal request) { ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, ServicePrincipal.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -36,6 +39,9 @@ public void delete(DeleteServicePrincipalRequest request) { try { Request req = new Request("DELETE", path); ApiClient.setQuery(req, request); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } apiClient.execute(req, Void.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -49,6 +55,9 @@ public ServicePrincipal get(GetServicePrincipalRequest request) { Request req = new Request("GET", path); ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, ServicePrincipal.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -62,6 +71,9 @@ public ListServicePrincipalResponse list(ListServicePrincipalsRequest request) { Request req = new Request("GET", path); ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, ListServicePrincipalResponse.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -75,6 +87,9 @@ public void patch(PartialUpdate request) { Request req = new Request("PATCH", path, apiClient.serialize(request)); ApiClient.setQuery(req, request); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } apiClient.execute(req, Void.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -88,6 +103,9 @@ public void update(ServicePrincipal request) { Request req = new Request("PUT", path, apiClient.serialize(request)); ApiClient.setQuery(req, request); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } apiClient.execute(req, Void.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/UsersImpl.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/UsersImpl.java index ba2f1a67a..acc6a521b 100755 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/UsersImpl.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/service/iam/UsersImpl.java @@ -24,6 +24,9 @@ public User create(User request) { ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, User.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -36,6 +39,9 @@ public void delete(DeleteUserRequest request) { try { Request req = new Request("DELETE", path); ApiClient.setQuery(req, request); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } apiClient.execute(req, Void.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -49,6 +55,9 @@ public User get(GetUserRequest request) { Request req = new Request("GET", path); ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, User.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -61,6 +70,9 @@ public GetPasswordPermissionLevelsResponse getPermissionLevels() { try { Request req = new Request("GET", path); req.withHeader("Accept", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, GetPasswordPermissionLevelsResponse.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -73,6 +85,9 @@ public PasswordPermissions getPermissions() { try { Request req = new Request("GET", path); req.withHeader("Accept", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, PasswordPermissions.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -86,6 +101,9 @@ public ListUsersResponse list(ListUsersRequest request) { Request req = new Request("GET", path); ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, ListUsersResponse.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -99,6 +117,9 @@ public void patch(PartialUpdate request) { Request req = new Request("PATCH", path, apiClient.serialize(request)); ApiClient.setQuery(req, request); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } apiClient.execute(req, Void.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -113,6 +134,9 @@ public PasswordPermissions setPermissions(PasswordPermissionsRequest request) { ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, PasswordPermissions.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -126,6 +150,9 @@ public void update(User request) { Request req = new Request("PUT", path, apiClient.serialize(request)); ApiClient.setQuery(req, request); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } apiClient.execute(req, Void.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); @@ -140,6 +167,9 @@ public PasswordPermissions updatePermissions(PasswordPermissionsRequest request) ApiClient.setQuery(req, request); req.withHeader("Accept", "application/json"); req.withHeader("Content-Type", "application/json"); + if (apiClient.workspaceId() != null) { + req.withHeader("X-Databricks-Org-Id", apiClient.workspaceId()); + } return apiClient.execute(req, PasswordPermissions.class); } catch (IOException e) { throw new DatabricksException("IO error: " + e.getMessage(), e); diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java new file mode 100644 index 000000000..2b99d61be --- /dev/null +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java @@ -0,0 +1,36 @@ +package com.databricks.sdk.integration; + +import static org.junit.jupiter.api.Assertions.*; + +import com.databricks.sdk.WorkspaceClient; +import com.databricks.sdk.core.DatabricksConfig; +import com.databricks.sdk.integration.framework.EnvContext; +import com.databricks.sdk.integration.framework.EnvOrSkip; +import com.databricks.sdk.integration.framework.EnvTest; +import com.databricks.sdk.service.iam.Group; +import com.databricks.sdk.service.iam.ListGroupsRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@EnvContext("account") +@ExtendWith(EnvTest.class) +public class UnifiedHostGroupsIT { + @Test + void listWorkspaceGroupsViaUnifiedHost( + @EnvOrSkip("UNIFIED_HOST") String host, + @EnvOrSkip("TEST_WORKSPACE_ID") String workspaceId, + @EnvOrSkip("DATABRICKS_CLIENT_ID") String clientId, + @EnvOrSkip("DATABRICKS_CLIENT_SECRET") String clientSecret) { + DatabricksConfig config = + new DatabricksConfig() + .setHost(host) + .setWorkspaceId(workspaceId) + .setClientId(clientId) + .setClientSecret(clientSecret); + WorkspaceClient ws = new WorkspaceClient(config); + + Iterable groups = ws.groups().list(new ListGroupsRequest().setAttributes("displayName")); + Group first = groups.iterator().next(); + assertNotNull(first.getDisplayName()); + } +} From 2622f1b4e10f4d5730aa17e061a1d00e680ce916 Mon Sep 17 00:00:00 2001 From: Hector Castejon Diaz Date: Fri, 10 Apr 2026 09:12:24 +0000 Subject: [PATCH 2/3] Fix UnifiedHostGroupsIT to follow unified config test pattern Co-authored-by: Isaac --- .../sdk/integration/UnifiedHostGroupsIT.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java index 2b99d61be..efd080400 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; +import com.databricks.sdk.AccountClient; import com.databricks.sdk.WorkspaceClient; import com.databricks.sdk.core.DatabricksConfig; import com.databricks.sdk.integration.framework.EnvContext; @@ -10,23 +11,28 @@ import com.databricks.sdk.service.iam.Group; import com.databricks.sdk.service.iam.ListGroupsRequest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.api.extension.ExtendWith; @EnvContext("account") @ExtendWith(EnvTest.class) +@EnabledIfEnvironmentVariable(named = "UNIFIED_HOST", matches = ".+") public class UnifiedHostGroupsIT { @Test + @DisabledIfEnvironmentVariable(named = "CLOUD_PROVIDER", matches = "GCP") void listWorkspaceGroupsViaUnifiedHost( - @EnvOrSkip("UNIFIED_HOST") String host, + AccountClient a, + @EnvOrSkip("UNIFIED_HOST") String unifiedHost, @EnvOrSkip("TEST_WORKSPACE_ID") String workspaceId, - @EnvOrSkip("DATABRICKS_CLIENT_ID") String clientId, - @EnvOrSkip("DATABRICKS_CLIENT_SECRET") String clientSecret) { + @EnvOrSkip("TEST_ACCOUNT_ID") String accountId) { DatabricksConfig config = new DatabricksConfig() - .setHost(host) + .setHost(unifiedHost) + .setClientId(a.config().getClientId()) + .setClientSecret(a.config().getClientSecret()) .setWorkspaceId(workspaceId) - .setClientId(clientId) - .setClientSecret(clientSecret); + .setAccountId(accountId); WorkspaceClient ws = new WorkspaceClient(config); Iterable groups = ws.groups().list(new ListGroupsRequest().setAttributes("displayName")); From 9625a4415f8ad4c42b608076382a3ba5cd5de929 Mon Sep 17 00:00:00 2001 From: Hector Castejon Diaz Date: Fri, 10 Apr 2026 09:36:43 +0000 Subject: [PATCH 3/3] Fix NPE in UnifiedHostGroupsIT by calling hasNext() before next() The Dedupe iterator wrapping the Groups list only populates its current element inside hasNext(). Calling next() without hasNext() returns null. Co-authored-by: Isaac --- .../com/databricks/sdk/integration/UnifiedHostGroupsIT.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java index efd080400..a1fa61aec 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/integration/UnifiedHostGroupsIT.java @@ -10,6 +10,7 @@ import com.databricks.sdk.integration.framework.EnvTest; import com.databricks.sdk.service.iam.Group; import com.databricks.sdk.service.iam.ListGroupsRequest; +import java.util.Iterator; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; @@ -36,7 +37,9 @@ void listWorkspaceGroupsViaUnifiedHost( WorkspaceClient ws = new WorkspaceClient(config); Iterable groups = ws.groups().list(new ListGroupsRequest().setAttributes("displayName")); - Group first = groups.iterator().next(); + Iterator it = groups.iterator(); + assertTrue(it.hasNext(), "Expected at least one group"); + Group first = it.next(); assertNotNull(first.getDisplayName()); } }