Skip to content

[CXH-3600] - Sync metastore privileges held by the workspace admins group - Databricks Connector - #90

Open
mateoHernandez123 wants to merge 5 commits into
mateoHernandez123/land-staging-on-mainfrom
mateoHernandez123/fix-metastore-workspace-admin-grants
Open

mateoHernandez123 wants to merge 5 commits into
mateoHernandez123/land-staging-on-mainfrom
mateoHernandez123/fix-metastore-workspace-admin-grants

Conversation

@mateoHernandez123

@mateoHernandez123 mateoHernandez123 commented Nov 2, 2026 •

Copy link
Copy Markdown

Description

  • Bug fix
  • New feature

QA found this during validation of CXH-2720 on v0.2.6-metastore-sync (#70, merged at 4b956905). Both QA metastores carry a privilege row for the workspace's built-in admins group. Unity Catalog names that principal _workspace_admins_<workspace_name>_<workspace_id>. The group is workspace-local, so the account SCIM API answers its id with a 404. The resolver found no match and dropped the whole row.

The cost is a silent gap. On AWS the row holds 7 privileges, on Azure 8. None of them reached the bundle. The sync still exited 0, and the only log line above Debug was a pair of 404 warnings. An access review that asks who can create a storage credential on the metastore got an incomplete answer, and nothing said so.

This PR resolves the alias to the group that holds the privileges, and syncs that group so the grants have a principal to point at.

What the connector syncs

Before. Groups hang off the account only. A workspace offers roles.

flowchart TD
  account[account]
  group[group]
  workspace[workspace]
  role[role]
  metastore[metastore]
  account --> group
  account --> workspace
  account --> metastore
  workspace --> role
Loading

After. A RUNNING workspace also offers its admins group. The metastore grants point at it.

flowchart TD
  account[account]
  group[group]
  workspace[workspace]
  role[role]
  metastore[metastore]
  wsgroup["group (admins)"]
  account --> group
  account --> workspace
  account --> metastore
  workspace --> role
  workspace --> wsgroup
  metastore -. privileges .-> wsgroup
  style wsgroup fill:#1f6feb,color:#fff,stroke:#1f6feb
Loading

How the alias resolves

Before. The account plane is the only scope the resolver reads.

flowchart LR
  row["privilege row<br/>_workspace_admins_prod_123"] --> acct["account SCIM<br/>by principal_id"]
  acct --> miss["404"]
  miss --> drop["grant dropped<br/>Debug log, exit 0"]
  style drop fill:#da3633,color:#fff,stroke:#da3633
Loading

After. The alias ends in the workspace id, so the connector reads the scope that holds the group.

flowchart LR
  row["privilege row<br/>_workspace_admins_prod_123"] --> parse["read workspace id<br/>from the alias"]
  parse --> ws["workspace SCIM<br/>confirm the group is admins"]
  ws --> grants["grants on<br/>workspace/&lt;deployment&gt;/group/&lt;id&gt;"]
  style grants fill:#1f6feb,color:#fff,stroke:#1f6feb
Loading

The alias names its own workspace. The connector does not use the workspace it read the permissions through, because a metastore can attach to more than one.

Order PR Delivers Base State
1 #91 staging landed on main main Open
2 #90 Fix for the metastore layer of #70 #91 This PR
3 #71 Catalog #91 Open
4 #72 Schema #71 Open
5 #73 Table and volume #72 Open

This PR is not a layer of the Unity Catalog stack. It fixes the metastore layer that #70 already shipped, so it branches from #91 beside #71.

Merge this PR before #71. The two branches change the same five files, and a merge test shows a content conflict in docs/connector.mdx, pkg/connector/connector.go, pkg/connector/metastore.go, pkg/connector/metastore_helpers.go and pkg/connector/metastore_test.go. The rollup shows both as clean, because GitHub compares each one against #91. Re-tested after the rebase: the same five files still conflict. The second branch to land must merge the first and resolve them by hand.

Sync:

  • Groups (group) — a RUNNING workspace now declares its admins group as a child. The listing is filtered to that one group. The connector emits a member grant only when the account holds the member, because it does not sync workspace-local identities.
  • Metastores (metastore) — the privilege rows for a workspace admins group now become grants. The grants carry GrantImmutable.
  • Workspaces (workspace) — unchanged surface. Whether a workspace offers its admins group is decided from the workspace status, so the syncer reads no Unity Catalog endpoint.
  • Users, Service principals, Roles — unchanged surface.

Provisioning:

  • Grant/Revoke metastore privileges — the connector refuses a workspace-local group with FailedPrecondition. Databricks does not allow a revoked privilege to be granted back to the system group, so the operation is one way.
  • Grant/Revoke group membership — the admins group's membership entitlement is not grantable. An identity-federated workspace rejects a membership PATCH with 400 and answers that the group is managed in the account. Account groups keep the entitlement they had.
  • Everything else is unchanged, including the GrantAlreadyExists and GrantAlreadyRevoked annotations.

Auth:

Unchanged. No new flag. The connector keeps account-level OAuth.

Architecture highlights:

  • The workspace syncer decides from the workspace status and reads no Unity Catalog endpoint. Asking Unity Catalog which workspaces carry a metastore was the alternative, and it is rejected here: metastores are opt-in while workspaces always sync, so a 403 on that plane would fail the whole workspace type, and its role and group children with it, on any tenant that cannot read it. This adds no such read; it does not remove one. At the base 4b956905, with Unity Catalog answering 403, workspaceBuilder.List already returned every workspace with no error.
  • The connector resolves the alias before the account lookups. This skips three reads that always miss, and removes the two 404 warnings the ticket reports.
  • A workspace in CANCELLING is skipped. It never becomes RUNNING, so waiting for it fails every sync.
  • The filtered workspace listing reports every group in totalResults. The connector asks for no second page.
  • The workspace-scoped group paths were unreachable after #65 removed PAT authentication. This PR brings them back for one group, so the guards above are part of the change.

Validation

Measured against the two QA tenants, with a binary built from 4b956905 and a binary built from this branch.

before after
resources 322 323
grants 1056 1143
metastore grants 268 345
workspace-scoped group resources 0 1
grants for principal 231086632550479 0 7
resources removed — 0
grants removed — 0
404 warnings for that principal 2 0

Azure shows the same shape. The 8 privileges, which add CREATE_CLEAN_ROOM, now appear on workspace/adb-2531901403506481.1/group/517918373872763.

Useful links:

Merge order and release flow

staging lands on main as its own PR, #91. The rest of the stack is rebased on top of it, so each PR below shows only the layer it adds. #84 is independent of this stack and merges on its own.

  1. Land staging on main - Databricks Connector #91 — staging landed on main (46 files)
  2. [CXH-3600] - Sync metastore privileges held by the workspace admins group - Databricks Connector #90 (this PR) — metastore privileges held by the workspace admins group (15 files)
  3. [CXH-2721] - Add Unity Catalog catalog sync - Databricks Connector #71 — Unity Catalog catalog sync (25 files)
  4. [CXH-2722] - Add Unity Catalog schema sync - Databricks Connector #72 — Unity Catalog schema sync (17 files)
  5. [CXH-2723] - Add Unity Catalog table and volume sync - Databricks Connector #73 — Unity Catalog table and volume sync (19 files)

#90 and #71 target #91 until it merges, and GitHub retargets them to main at that point. #72 and #73 keep their parent branch as base.

Rebased on 2026-11-03, as requested on #90. Before this, #90 and #71 showed 42 staging-only commits and 50+ files, which hid the layer each one adds. Every branch was rebuilt on top of #91. The tree at each head is byte-identical to the one reviewed before the rebase, so no code changed. The commits are squashed into one per PR, which marks earlier review threads as outdated; they stay readable.

Release flow. Cut a test release (-test) from the feature branch and move the PR to draft. The ticket moves to validation. QA validates that release. Merge to main. Cut the official release from main, and the ticket moves to rollout.

@linear-code

linear-code Bot commented Nov 2, 2026

Copy link
Copy Markdown

CXH-3600

@manuel-ts-14 manuel-ts-14 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA review from the CXH-2720 validation. The fix resolves the root cause reported in CXH-3600: reading the workspace ID from the alias and confirming admins on that workspace's SCIM is the right approach. The ID-reuse guard, the immutable grants and the FailedPrecondition on provisioning all look good. There are three findings, and none of them blocks validation:

  1. The docs say CANCELLING fails the sync, but the code skips it (inline on docs/docs-info.md).
  2. Every workspace-scoped admins group is named just admins (inline on pkg/connector/groups.go).
  3. The description and the commit message say the workspace syncer "no longer reads any Unity Catalog endpoint" and that "a 403 from Unity Catalog used to fail the whole workspace type". I can't find that change in the diff against staging. At 4b956905, pkg/connector/workspaces.go has no Unity Catalog read: git grep -i -E "unity|metastore" 4b956905 -- pkg/connector/workspaces.go matches only the comment at line 390. If that sentence describes an earlier iteration of this branch, could you drop it? If I've missed the hunk, please point me at it. QA scopes regression cases from the description, so a behaviour change that isn't in the diff sends us testing something that never changed.

Comment thread docs/docs-info.md Resolved
Comment thread pkg/connector/groups.go Resolved
@mateoHernandez123
mateoHernandez123 changed the base branch from staging to main November 3, 2026 15:05
@mateoHernandez123
mateoHernandez123 requested a review from a team November 3, 2026 15:05

@luisina-santos luisina-santos left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes because this PR's diff doesn't match what it says it does.

Against main, the diff has 42 commits and 52 files. The fix itself is one commit (860d82e2) touching 15 files. The rest comes from staging:

  • #70 (Unity Catalog foundation and metastore sync) and #56 (incremental sync, audit event feed, test-server). These are only on staging.
  • #81 as 6384cfa1. main already has it as 0c145865.

The description is also off in a few places:

  • The stack table lists this PR's base as staging, but it targets main.
  • It says the staging-only commits were "never reviewed as their own PRs". #56 and #70 were reviewed and merged into staging.
  • #88 (on staging since 2026-11-02) isn't in this branch, so it wouldn't reach main through this PR.

Could you:

  1. Rebase staging onto main and land it on main as its own PR, including #88 and the #81 resolution.
  2. Rebase this branch so its diff against main is only 860d82e2.
  3. Update the description and merge-order section to match. #71 has the same problem.

@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/fix-metastore-workspace-admin-grants branch from 860d82e to f4e64dc Compare November 3, 2026 18:27
@mateoHernandez123
mateoHernandez123 changed the base branch from main to mateoHernandez123/land-staging-on-main November 3, 2026 18:27
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit f4e64dc83be9

Connector PR Review: [CXH-3600] - Sync metastore privileges held by the workspace admins group - Databricks Connector

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - base ref mateoHernandez123/land-staging-on-main is not the default branch main for .claude/skills/ci-review.md.
Review mode: full
View review run

Review Summary

Metastore privilege rows for the Unity Catalog alias _workspace_admins_<name>_<id> were being dropped. The connector now parses the workspace ID at the end of the alias and confirms against that workspace's SCIM that the group is admins. It then emits the privileges as GrantImmutable grants on workspace/<deployment>/group/<id> (metastore_workspace_admins.go).

RUNNING workspaces now declare a group child, and its listing is filtered to admins with no further pages (groups.go:84-121). That group's membership is not grantable, and a member grant is emitted only when the account holds the member. Metastore Grant/Revoke rejects workspace-local groups with FailedPrecondition.

I scanned the full PR diff (15 files) for security and correctness, using the base criteria plus the connector mixin. No repo-local criteria loaded.

Points I checked:

  • Status handling matches on both sides. The workspace builder and the metastore resolver both use workspaceIsRunning. FAILED and BANNED workspaces are already dropped by workspaceIsUsable (metastore_helpers.go:253), so they cannot fail the sync forever. CANCELLING is skipped and PROVISIONING fails the sync, as the docs describe.
  • Errors are not swallowed (R7/F3). SCIM errors go through refuseUnreadable, and accountHolds errors fail Grants.
  • No new permission requirement (B8). Workspace-role grants already call workspace-plane APIs, so the SCIM reads add no new requirement.
  • Docs and dependencies. docs/connector.mdx and the README are updated, and go.mod/go.sum are unchanged.
  • Tests. They cover the alias by ID and by name, a group-name mismatch, the allowlist, the cancelling and provisioning paths, immutability, the provisioning refusal, the single-page listing, and account-held member filtering.

There were no prior findings to audit. Coverage gaps: none material.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Connector PR Review: [CXH-3600] - Sync metastore privileges held by the workspace admins group - Databricks Connector

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - base ref mateoHernandez123/land-staging-on-main is not the default branch main for .claude/skills/ci-review.md.
Review mode: full
View review run

Review Summary

Metastore privilege rows for the Unity Catalog alias _workspace_admins_<name>_<id> were being dropped. The connector now parses the workspace ID at the end of the alias and confirms against that workspace's SCIM that the group is admins. It then emits the privileges as GrantImmutable grants on workspace/<deployment>/group/<id> (metastore_workspace_admins.go).

RUNNING workspaces now declare a group child, and its listing is filtered to admins with no further pages (groups.go:84-121). That group's membership is not grantable, and a member grant is emitted only when the account holds the member. Metastore Grant/Revoke rejects workspace-local groups with FailedPrecondition.

I scanned the full PR diff (15 files) for security and correctness, using the base criteria plus the connector mixin. No repo-local criteria loaded.

Points I checked:

  • Status handling matches on both sides. The workspace builder and the metastore resolver both use workspaceIsRunning. FAILED and BANNED workspaces are already dropped by workspaceIsUsable (metastore_helpers.go:253), so they cannot fail the sync forever. CANCELLING is skipped and PROVISIONING fails the sync, as the docs describe.
  • Errors are not swallowed (R7/F3). SCIM errors go through refuseUnreadable, and accountHolds errors fail Grants.
  • No new permission requirement (B8). Workspace-role grants already call workspace-plane APIs, so the SCIM reads add no new requirement.
  • Docs and dependencies. docs/connector.mdx and the README are updated, and go.mod/go.sum are unchanged.
  • Tests. They cover the alias by ID and by name, a group-name mismatch, the allowlist, the cancelling and provisioning paths, immutability, the provisioning refusal, the single-page listing, and account-held member filtering.

There were no prior findings to audit. Coverage gaps: none material.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.


Reviewed commit: f4e64dc83be9

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found — see the full review report

mateoHernandez123 and others added 3 commits November 3, 2026 17:29
…mins group

Unity Catalog writes a metastore privilege row for each workspace's built-in
admins group under the alias _workspace_admins_<name>_<id>. That principal is
workspace-local, so the account SCIM API answers its id with a 404 and the row
was dropped: 7 privileges on AWS and 8 on Azure were absent from the bundle
while the sync exited 0 and named the drop only at Debug.

A RUNNING workspace now offers its admins group as a child, and the metastore
resolver maps the alias to that group through the workspace id the alias
carries, rather than through the workspace the permissions were read with. The
grants are immutable and provisioning refuses a workspace-local group, because
Databricks does not allow a revoked privilege to be granted back to the system
group. The group's membership is read-only for the same reason: an
identity-federated workspace rejects a membership PATCH with 400.

Resolving the alias before the account lookups skips the three reads that would
always miss, which is what logged the two 404 warnings.

The workspace syncer decides this from the workspace status and reads no Unity
Catalog endpoint. Asking Unity Catalog which workspaces carry a metastore was
the alternative, and it is rejected here: metastores are opt-in while
workspaces always sync, so a 403 on that plane would fail the whole workspace
type, and its role and group children with it, on any tenant that cannot read
it. No such read is removed; there was none. A member the account does not hold
is skipped instead of emitted, since only the account syncs users, service
principals and nested groups. The filtered workspace listing reports every
group in totalResults, so it asks for no second page, and a workspace in
CANCELLING offers no admins group, because it never becomes RUNNING. Metastore
routing treats that same status differently and keeps waiting, so a metastore
is not pruned while its workspace is still being deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…h workspace holds a metastore

Databricks names the built-in group "admins" in every workspace, so a metastore
shared by several of them listed one holder per workspace under one
indistinguishable name, and only the parent resource told them apart in an
access review. A workspace-scoped group is now named "admins (<workspace
name>)". The unqualified name stays on the resource profile and the resource ID
does not change, so the grants are unchanged: 875 on AWS and 98 on Azure before
and after. The name costs one account workspace listing per sync, which the
response cache already holds.

The docs said a privilege row whose workspace is provisioning or cancelling
fails the sync before SCIM. The resolver skips a cancelling workspace instead.

Routing treats CANCELLING as pending so the metastore is not pruned while the
deletion runs: pruning would report the metastore to ConductorOne as gone and
delete its grants, and the wait ends on its own when the account stops listing
the workspace. That reason lived in the Unity Catalog layer and was lost when
the layer was folded into the metastore syncer, which left a comment grouping
CANCELLING with the terminal statuses while the code did not. It is restored.

The error that covers the wait said only that the metastore has no running
workspace in sync scope, which reads as a misconfigured scope, and the other
path said the workspaces are on their way to RUNNING, which a workspace being
deleted is not. Routing now returns the workspaces that hold the metastore with
their status, and both messages name them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rkspace

Two comments still described CANCELLING as dropped from scope, which is
what workspaceIsUsable deliberately does not do: dropping the workspace
prunes the metastores that routed through it, and ConductorOne then
deletes grants that are still in force while the deletion runs. The
comment on the status constants already says this; the one on the scope
loop and the one on the status test did not.

The two answers are both intentional. The workspace syncer offers no
admins group for a cancelling workspace, because it will never be
RUNNING. Metastore routing keeps it, because the metastore is still
there until the account stops listing the workspace.

No behaviour changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/fix-metastore-workspace-admin-grants branch from d5403a1 to 9e8cdd8 Compare November 3, 2026 20:30
Comment thread pkg/connector/groups.go Resolved
Comment thread pkg/connector/metastore.go Resolved
Comment thread pkg/connector/groups.go Resolved
The mock answered every workspace group listing with the admins group and
never read the filter, so both List tests passed whether or not
`displayName eq "admins"` was sent. It now serves three groups and
filters them: dropping the filter fails
TestWorkspaceGroupListStopsAfterTheAdminsGroup and
TestWorkspaceGroupNameNamesItsWorkspace instead of passing silently.

The group name is a shared constant now, because the listing filter and
the alias check in metastore_workspace_admins.go have to agree on it and
each spelled it out.

The List godoc said it returns all groups, which stopped being true under
a workspace parent, and the alias comment in metastore Grants said the
alias skips three account lookups without saying that a row it cannot
resolve still runs them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ber type

accountHolds asked the account SCIM endpoint about one member at a time, so a
workspace admins group of n members cost n filtered reads. The members of a page
are now resolved in one request per member type, batched at 100 "id eq" terms
because Databricks refuses a SCIM filter above 200 nodes and an n-term chain is
2n-1 of them.

A filtered read that answers fewer rows than it matched now fails instead of
reading as "the account does not hold these ids". The members a short page left
out would be skipped, and a membership grant missing from a sync is revoked
access in ConductorOne.

The test mock now applies the whole or-chain rather than its first term, which a
caller that dropped the rest would otherwise have passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants