Skip to content

[CXH-2721] - Add Unity Catalog catalog sync - Databricks Connector - #71

Open
mateoHernandez123 wants to merge 3 commits into
mateoHernandez123/land-staging-on-mainfrom
mateoHernandez123/unity-catalog-catalog-sync
Open

mateoHernandez123 wants to merge 3 commits into
mateoHernandez123/land-staging-on-mainfrom
mateoHernandez123/unity-catalog-catalog-sync

Conversation

@mateoHernandez123

@mateoHernandez123 mateoHernandez123 commented Oct 9, 2026 •

Copy link
Copy Markdown

Description

  • Bug fix
  • New feature

Second of four stacked PRs for Unity Catalog (CXH-2721). A catalog is the named container a customer picks in an access review. Its parent is the metastore. The workspace host is only the access path.

#70 is merged. This PR is based on #91, which lands staging on main.

What the connector syncs

Before, on #91. The metastore is a leaf.

flowchart TD
  account[account]
  user[user]
  group[group]
  sp[service principal]
  role[role]
  workspace[workspace]
  metastore[metastore]
  account --> user
  account --> group
  account --> sp
  account --> role
  account --> workspace
  account --> metastore
Loading

After this PR. Catalogs hang from the metastore, with their own entitlements and grants.

flowchart TD
  account[account]
  user[user]
  group[group]
  sp[service principal]
  role[role]
  workspace[workspace]
  metastore[metastore]
  catalog[catalog]
  account --> user
  account --> group
  account --> sp
  account --> role
  account --> workspace
  account --> metastore
  metastore --> catalog
  style catalog fill:#1f6feb,color:#fff,stroke:#1f6feb
Loading
Order PR Delivers Base State
1 #91 staging landed on main main Open
2 #90 Fix for the metastore layer of #70 #91 Open
3 #71 Catalog #91 This PR
4 #72 Schema #71 Open
5 #73 Table and volume #72 Open

#90 is a fix to the metastore layer, not a layer of this stack. It branches from #91 beside this PR and changes five of the same files. Whichever lands second must merge the other and resolve them by hand.

Sync:

  • Catalogs (catalog, NEW, opt-in) — child of metastore, keyed {metastore_id}::{name}. One entitlement per catalog privilege, plus a read-only owner.
  • The connector reads catalogs through every running in-scope workspace attached to the metastore. An ISOLATED catalog is listed only by the workspaces bound to it, so each workspace is read. A catalog that several workspaces list is emitted once per route. The store keys a resource by (external_id, sync_id) and upserts, so the copies collapse into one row. Measured on a live tenant: with every route pushed twice, the metastore still synced one catalog per name.
  • The routes come from the sync's session store. Metastore List resolves each metastore's running attached workspaces and its owner once and writes them there; catalog List reads them back, so ListWorkspaces, ListMetastoreWorkspaces and ListMetastores do not run again per metastore. A targeted sync that never ran metastore List resolves them once and stores the result. The page token carries a workspace name, not a serialised route. This needs the session store, so main.go passes connectorrunner.WithSessionStoreEnabled().
  • A catalog whose privilege list cannot be proved complete fails List with PermissionDenied. The error names the catalog and the remedy. ConductorOne does not remove the catalog. See Auth below.
  • A permissions 404 skips only that catalog when the body carries CATALOG_DOES_NOT_EXIST. Any other 404 is FailedPrecondition, so a refusal never reads as an empty page.
  • The built-in system catalog and anything Databricks types as SYSTEM_CATALOG, which includes samples, are excluded whatever the connector owns. They used to appear while the service principal owned the metastore and disappear when it stopped, taking their grants with them. Measured on the Baton account: the sync goes from 3 catalogs and 2647 grants to 1 catalog and 1087 grants, and the 1560 that go are samples (1300) and system (260). The one real catalog is unchanged at 29.
  • A metastore the account stopped listing has no catalogs to list. The catalog List reports none instead of failing, because the metastore syncer removes the parent in the same sync.
  • List is what refuses a partial privilege list. Grants does not prove it a second time.
  • Metastores and the existing types are unchanged.

Provisioning:

  • Grant/Revoke catalog privileges — PATCH .../permissions/catalog/{name}. A pre-read past the cache decides GrantAlreadyExists and GrantAlreadyRevoked. The echoed assignments confirm the change.
  • Owner is read-only. A Hive-era alias or a metastore privilege is rejected with InvalidArgument.
  • Grant and Revoke read the metastore first, because the owner decides trust and cannot come from the profile, and then try the workspace the last sync recorded: one GetCatalog that names this metastore proves the workspace still serves it, and the account's workspace listing and the metastore's assignment listing are skipped. Any other answer falls through to those listings and the full walk, and the workspace already read is not read twice. Scope is answered locally, since the profile carries the deployment name, the workspace name and the numeric id, which are the three identifiers the configuration matches on.
  • Every read that decides a provisioning write bypasses the response cache. The cache holds a GET for an hour and a PATCH does not invalidate it, so a Revoke could answer GrantAlreadyRevoked while the privilege was still held. The resource is rebuilt from the live read, so trust is decided from the owners Databricks returns now, not from the owners the last sync stored.

A catalog that no workspace lists is not proof of deletion. An unbound workspace answers the same 404 as a deleted catalog. The connector asks the metastore owner for the listing with include_unbound=true before it answers:

flowchart TD
  start[Grant or Revoke] --> route[rebuild the route]
  route -->|a workspace serves the catalog| write[PATCH permissions]
  route -->|every workspace answers CATALOG_DOES_NOT_EXIST| proof{connector owns the metastore?}
  proof -->|no| unknown[FailedPrecondition: deletion cannot be proved]
  proof -->|yes| unbound[list with include_unbound=true]
  unbound -->|catalog is in the listing| exists[FailedPrecondition: it exists, out of reach]
  unbound -->|catalog is absent| gone[Revoke: GrantAlreadyRevoked / Grant: NotFound]
Loading

A deleted metastore takes the same two answers: Revoke returns GrantAlreadyRevoked, Grant returns NotFound.

Measured on the Baton account, where the service principal owns the metastore:

  • Revoke on a catalog that does not exist returns GrantAlreadyRevoked with no error.
  • Grant on the same catalog returns NotFound.
  • A Grant and Revoke round trip on a real catalog writes and reads back correctly, with the profile route and with the profile naming a workspace that no longer exists. Group Test1 held USE_CATALOG, BROWSE was granted and revoked, and the group was left holding USE_CATALOG again. The fallback is visible in the timing: 4.56s through the profile route against 6.57s when the listings and the walk have to run.
  • GET /catalogs/{name} and GET /permissions/catalog/{name} both answer 404 CATALOG_DOES_NOT_EXIST for a catalog that does not exist.

Auth:

Credentials are unchanged. There is no new config field.

A catalog is synced when the service principal owns the metastore, owns the catalog, holds MANAGE on the catalog, or is a direct Admin of the workspace whose admins group owns it. The admins group is named _workspace_admins_<workspace>_<id>, and the connector reads the workspace id off that name. The check runs against that workspace whatever route the catalog is listed through, because permission visibility follows the caller, not the host.

Databricks answers anyone else with a 200 and only their own grants. A catalog that meets none of these rules fails List with PermissionDenied naming the remedy. ConductorOne does not remove it. Grant and Revoke on such a catalog fail with PermissionDenied and send no PATCH.

Architecture highlights:

  • Shared securable path: securableLevel holds what differs per level — the securable type, the privilege set, the remedies, the name parser, the gone error codes and the trust rule. securableOps holds what the shared path needs and is embedded in both builders, so the three operations are methods: m.syncGrants, m.grant, m.revoke.
  • Profile: List writes the workspace, workspace id, workspace name, catalog owner and metastore owner. Grants reads the workspace back, which saves an account round trip per catalog.
  • Routing: catalogRoutes reads the metastore's routes from the sync session and resolves them live only on a miss. The pagination.Bag holds one page state per workspace, keyed by deployment name. The workspace Admin check is remembered per workspace for the length of a sync; provisioning has no sync session and always reads the current assignments.
  • Privileges: the catalog set follows the Databricks privileges reference. The PATCH accepts and echoes every entry on a live tenant, including the ones the databricks-sdk-go Privilege enum omits.
  • include_unbound answers whether a catalog exists. It does not grant access to it, so List does not use it. A workspace cannot read the grants of a catalog it is not bound to.

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 — metastore privileges held by the workspace admins group (15 files)
  3. [CXH-2721] - Add Unity Catalog catalog sync - Databricks Connector #71 (this PR) — 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 Oct 9, 2026

Copy link
Copy Markdown

CXH-2721

@FeliLucero1 FeliLucero1 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.

Did a pass on this one. Main thing is the first comment: Revoke can say "already revoked" while the privilege still exists. The rest are perf and dupe stuff.

Comments tagged (out of scope / non-blocking) are low-confidence or nits I didn't fully verify, so treat them as FYI. Also out of scope: the databricks-connector: error prefix vs baton-databricks: is a convention that predates this PR, not something it introduced.

Heads up: the unreachable-catalog thing below is still the same in #72 / #73.

Comment thread pkg/connector/catalog.go Resolved
Comment thread pkg/connector/unity_catalog.go Resolved
Comment thread docs/docs-info.md Resolved
Comment thread pkg/connector/unity_catalog.go Resolved
Comment thread pkg/connector/catalog.go Resolved
Comment thread pkg/connector/catalog.go Resolved
Comment thread pkg/connector/catalog.go Resolved
Comment thread pkg/connector/catalog_filter.go Resolved
Comment thread pkg/connector/unity_catalog.go Resolved

@FeliLucero1 FeliLucero1 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.

Image

@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-catalog-sync branch from 5933db6 to 6add251 Compare October 9, 2026 19:31
Comment thread pkg/config/config.go Resolved
Comment thread pkg/connector/privileges.go Resolved
Comment thread pkg/connector/unity_catalog.go Resolved
Comment thread pkg/connector/catalog.go Resolved
Comment thread pkg/connector/catalog.go Resolved
Comment thread README.md Resolved

@sergiocorral-conductorone sergiocorral-conductorone 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.

Automated review

drafted 1 → confirmed 1, low-confidence 0, dropped 0, added 0 (panel: opus-5.5 / gpt-6-astra / gemini-4-argon, all high).

Comment thread pkg/connector/metastore.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
@github-actions

github-actions Bot commented Nov 3, 2026 •

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

Connector PR Review: [CXH-2721] - Add Unity Catalog catalog sync - Databricks Connector

Blocking Issues: 1 | Suggestions: 5 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 0c145865eb23.
Review mode: full
View review run

Review Summary

This PR adds an opt-in Unity Catalog catalog resource under metastore. Catalogs are listed through each in-scope running workspace, the routes are cached in the sync session store (main.go now enables it), and catalog privileges can be granted and revoked with a live read that bypasses the cache. The PR targets main, so the diff (51 files, +11312/−155) also carries the staging-only audit event feed, SQL client, metastore layer and test-server. I scanned the full diff for security and correctness. The repo-local criteria were applied to List/Grants, skip-on-404 and 404 semantics (S1–S6, R7), gRPC code mapping (E2), provisioning entity sources (PR1–PR3) and ID stability (I1/B3).

Nothing in the catalog List/Grants path returns an empty result after an error:

  • Skipping a catalog requires the CATALOG_DOES_NOT_EXIST error code.
  • A catalog whose privilege list cannot be proved complete fails with PermissionDenied.
  • Syncers are registered unconditionally.
  • The SQL statements bind their parameters, and the SCIM filter values are now quoted.

Coverage gaps: the test-server/ directory and the *_test.go files were not reviewed line by line.

Security Issues

None found.

Correctness Issues

  • New, pkg/databricks/unity_catalog.go:132: Grant/Revoke can write privileges on a different catalog. GetCatalog joins the raw catalog name into the path without the addressesSecurable guard that permissionsUrl applies, and prepareRequest (request.go:267-272) unescapes the URL again. A name containing ?, # or % therefore addresses a different catalog: sales?x reads sales. catalogThrough (catalog.go:391-409) checks only MetastoreID. routedEntitlement (catalog.go:698-712) rebuilds the resource from the returned catalog.Name, so the PATCH goes to that catalog. Confidence: high on the code path, medium on how often such names occur.

Suggestions

  • New, pkg/connector/metastore_helpers.go:263: workspaceIsUsable returns true for CANCELLING, but the comments at lines 34-35 and 245-247 say those workspaces are out of scope. A metastore attached only to a cancelling workspace fails every sync with FailedPrecondition. This fails closed. Confidence: high.
  • New, PR scope and description: the description says the PR targets staging, and that staging should land in main before the stack is retargeted. The PR actually targets main, and the head merges main in, so merging it brings in about 42 staging-only commits: the audit event feed, the SQL statement client, the CI workflow and the test-server. Those have not been reviewed as their own PRs. Retarget the PR or update the description so reviewers know what they are approving. Confidence: high.
  • New, pkg/databricks/request.go:149-154: the GetUncached comment says a fresh read updates the entry later readers see. Cache-Control is part of the cache key (client.go:111), so the no-cache response is stored under a key that normal reads never look up. The behavior is safe, but the comment is wrong. Confidence: high.
  • New, pkg/connector/audit_event_feed.go:677-678: GetGroup(ctx, "", nativeId) checks whether a principal exists but fetches the full SCIM group, members included, and caches it for an hour. Pass databricks.NewGroupAttrVars() the way groups.go Get does. Confidence: high.
  • New, pkg/connector/metastore.go:201-205: Revoke returns GrantAlreadyRevoked when ListMetastoreWorkspaces returns a 404 with METASTORE_DOES_NOT_EXIST. The client method (unity_catalog.go:60) has no doc comment saying that this 404 means the metastore is absent. Criteria S6 requires that note before a 404 is treated as absence. Confidence: medium.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `pkg/databricks/unity_catalog.go`:
- Around line 122-139 (GetCatalog): the URL is built with JoinPath(unityCatalogCatalogsEndpoint, name) with no check that the name survives URL assembly. prepareRequest unescapes and re-parses the URL, so a catalog name containing '?', '#' or '%' addresses a different catalog. Run the same addressesSecurable(u, "/"+name) check that permissionsUrl uses (adjust the suffix to the catalogs path) and return invalidArgument when it fails.

In `pkg/connector/catalog.go`:
- Around line 391-409 (catalogThrough): after GetCatalog succeeds, also reject a response whose catalog.Name differs from catalogName (return an InvalidArgument or FailedPrecondition error). routedEntitlement rebuilds the resource from catalog.Name, so a mismatched answer would make Grant/Revoke PATCH the wrong catalog.

## Suggestions

In `pkg/connector/metastore_helpers.go`:
- Around line 263 (workspaceIsUsable): CANCELLING is treated as usable, but the comments at lines 34-35 and 245-247 say cancelling workspaces are left out of scope. Either drop workspaceStatusCancelling from the usable set or fix the comments. Update metastore_test.go:364 to match.

In the PR itself:
- The PR targets main, and its diff includes all staging-only commits (audit event feed, SQL client, CI workflow, test-server). Retarget the PR to staging, or land staging in main first, or update the description to say these are included.

In `pkg/databricks/request.go`:
- Around line 149-154: the GetUncached comment says a fresh read updates the cache entry later readers see. Cache-Control is part of the cache key (client.go:111), so it does not. Correct the comment.

In `pkg/connector/audit_event_feed.go`:
- Around line 677-678: pass databricks.NewGroupAttrVars() to client.GetGroup so the existence check does not fetch and cache the full member list.

In `pkg/databricks/unity_catalog.go`:
- Around line 60 (ListMetastoreWorkspaces): add a doc comment stating that a 404 with METASTORE_DOES_NOT_EXIST means the metastore is absent. metastore.go:201-205 relies on that to return GrantAlreadyRevoked.

Comment thread pkg/databricks/unity_catalog.go Resolved
Comment thread pkg/connector/metastore_helpers.go Resolved
@github-actions

github-actions Bot commented Nov 3, 2026 •

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

Connector PR Review: [CXH-2721] - Add Unity Catalog catalog sync - Databricks Connector

Blocking Issues: 1 | Suggestions: 5 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 0c145865eb23.
Review mode: full
View review run

Review Summary

This PR adds an opt-in Unity Catalog catalog resource under metastore. Catalogs are listed through each in-scope running workspace, the routes are cached in the sync session store (main.go now enables it), and catalog privileges can be granted and revoked with a live read that bypasses the cache. The PR targets main, so the diff (51 files, +11312/−155) also carries the staging-only audit event feed, SQL client, metastore layer and test-server. I scanned the full diff for security and correctness. The repo-local criteria were applied to List/Grants, skip-on-404 and 404 semantics (S1–S6, R7), gRPC code mapping (E2), provisioning entity sources (PR1–PR3) and ID stability (I1/B3).

Nothing in the catalog List/Grants path returns an empty result after an error:

  • Skipping a catalog requires the CATALOG_DOES_NOT_EXIST error code.
  • A catalog whose privilege list cannot be proved complete fails with PermissionDenied.
  • Syncers are registered unconditionally.
  • The SQL statements bind their parameters, and the SCIM filter values are now quoted.

Coverage gaps: the test-server/ directory and the *_test.go files were not reviewed line by line.

Security Issues

None found.

Correctness Issues

  • New, pkg/databricks/unity_catalog.go:132: Grant/Revoke can write privileges on a different catalog. GetCatalog joins the raw catalog name into the path without the addressesSecurable guard that permissionsUrl applies, and prepareRequest (request.go:267-272) unescapes the URL again. A name containing ?, # or % therefore addresses a different catalog: sales?x reads sales. catalogThrough (catalog.go:391-409) checks only MetastoreID. routedEntitlement (catalog.go:698-712) rebuilds the resource from the returned catalog.Name, so the PATCH goes to that catalog. Confidence: high on the code path, medium on how often such names occur.

Suggestions

  • New, pkg/connector/metastore_helpers.go:263: workspaceIsUsable returns true for CANCELLING, but the comments at lines 34-35 and 245-247 say those workspaces are out of scope. A metastore attached only to a cancelling workspace fails every sync with FailedPrecondition. This fails closed. Confidence: high.
  • New, PR scope and description: the description says the PR targets staging, and that staging should land in main before the stack is retargeted. The PR actually targets main, and the head merges main in, so merging it brings in about 42 staging-only commits: the audit event feed, the SQL statement client, the CI workflow and the test-server. Those have not been reviewed as their own PRs. Retarget the PR or update the description so reviewers know what they are approving. Confidence: high.
  • New, pkg/databricks/request.go:149-154: the GetUncached comment says a fresh read updates the entry later readers see. Cache-Control is part of the cache key (client.go:111), so the no-cache response is stored under a key that normal reads never look up. The behavior is safe, but the comment is wrong. Confidence: high.
  • New, pkg/connector/audit_event_feed.go:677-678: GetGroup(ctx, "", nativeId) checks whether a principal exists but fetches the full SCIM group, members included, and caches it for an hour. Pass databricks.NewGroupAttrVars() the way groups.go Get does. Confidence: high.
  • New, pkg/connector/metastore.go:201-205: Revoke returns GrantAlreadyRevoked when ListMetastoreWorkspaces returns a 404 with METASTORE_DOES_NOT_EXIST. The client method (unity_catalog.go:60) has no doc comment saying that this 404 means the metastore is absent. Criteria S6 requires that note before a 404 is treated as absence. Confidence: medium.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `pkg/databricks/unity_catalog.go`:
- Around line 122-139 (GetCatalog): the URL is built with JoinPath(unityCatalogCatalogsEndpoint, name) with no check that the name survives URL assembly. prepareRequest unescapes and re-parses the URL, so a catalog name containing '?', '#' or '%' addresses a different catalog. Run the same addressesSecurable(u, "/"+name) check that permissionsUrl uses (adjust the suffix to the catalogs path) and return invalidArgument when it fails.

In `pkg/connector/catalog.go`:
- Around line 391-409 (catalogThrough): after GetCatalog succeeds, also reject a response whose catalog.Name differs from catalogName (return an InvalidArgument or FailedPrecondition error). routedEntitlement rebuilds the resource from catalog.Name, so a mismatched answer would make Grant/Revoke PATCH the wrong catalog.

## Suggestions

In `pkg/connector/metastore_helpers.go`:
- Around line 263 (workspaceIsUsable): CANCELLING is treated as usable, but the comments at lines 34-35 and 245-247 say cancelling workspaces are left out of scope. Either drop workspaceStatusCancelling from the usable set or fix the comments. Update metastore_test.go:364 to match.

In the PR itself:
- The PR targets main, and its diff includes all staging-only commits (audit event feed, SQL client, CI workflow, test-server). Retarget the PR to staging, or land staging in main first, or update the description to say these are included.

In `pkg/databricks/request.go`:
- Around line 149-154: the GetUncached comment says a fresh read updates the cache entry later readers see. Cache-Control is part of the cache key (client.go:111), so it does not. Correct the comment.

In `pkg/connector/audit_event_feed.go`:
- Around line 677-678: pass databricks.NewGroupAttrVars() to client.GetGroup so the existence check does not fetch and cache the full member list.

In `pkg/databricks/unity_catalog.go`:
- Around line 60 (ListMetastoreWorkspaces): add a doc comment stating that a 404 with METASTORE_DOES_NOT_EXIST means the metastore is absent. metastore.go:201-205 relies on that to return GrantAlreadyRevoked.

Reviewed commit: 60501e2f4e00

@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.

Blocking issues found — see the full review report

CXH-2721. Catalogs hang from the metastore, keyed {metastore_id}::{name},
with one entitlement per catalog privilege plus a read-only owner. The
connector reads them through every running in-scope workspace attached to
the metastore, caches the routes in the sync session store, and grants and
revokes privileges with a live read that bypasses the response cache.

The tree is unchanged from 60501e2, which is the state reviewed on #71.
The branch is rebuilt on top of the staging-to-main landing so the PR shows
only the catalog layer instead of the 42 staging commits it used to carry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/unity-catalog-catalog-sync branch from 60501e2 to a343b68 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 a343b6888be7

Connector PR Review: [CXH-2721] - Add Unity Catalog catalog sync - Databricks Connector

Blocking Issues: 1 | Suggestions: 2 | 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

This PR adds an opt-in Unity Catalog catalog resource that hangs from metastore, keyed {metastore_id}::{name}. Each catalog gets static per-privilege entitlements plus a read-only owner. Grants and Grant/Revoke reuse the shared securable helpers. Metastore List stores each metastore's routes (running in-scope workspaces and owner) in the session store, and catalog List reads them back and walks one workspace per page-bag frame. main.go now enables the session store. I scanned the full PR diff for security and correctness, focusing on catalog.go, catalog_session.go, securable_helpers.go, metastore*.go, unity_catalog.go, and the wiring in connector.go, resource_types.go and main.go. I did not read the test files and docs line by line. No repo-local criteria loaded, so I applied the base prompt and the connector mixin.

The pagination, the 401/403/404 handling (no empty-list fallbacks, and the CATALOG_DOES_NOT_EXIST skip is documented), OptInRequired/SkipEntitlements on the type, ChildResourceType on the metastore, and the Grant/Revoke entity sources all look correct.

Security Issues

None found.

Correctness Issues

  • Prior — still present, pkg/databricks/unity_catalog.go:132: GetCatalog still joins the raw catalog name with JoinPath and skips the addressesSecurable guard that permissionsUrl (unity_catalog.go:342) applies. prepareRequest (request.go:267-272) unescapes the URL again, so a name containing ?, # or % reads a different catalog: sales?x reads sales. catalogThrough (catalog.go:405) checks only MetastoreID. routedEntitlement (catalog.go:704) then rebuilds the resource id from the returned catalog.Name, so resolveChange and the guarded permissionsUrl both see sales, and the PATCH lands on that catalog. Confidence: high on the code path, medium on how often such names occur. The existing open thread already covers this.

Suggestions

  • Prior — still present, pkg/connector/metastore_helpers.go:263: workspaceIsUsable still accepts CANCELLING, but the comments at lines 34-35 and 245-247 say such workspaces are out of scope. A metastore attached only to a cancelling workspace fails metastore and catalog List with FailedPrecondition on every sync. This fails closed. Confidence: high. These lines are no longer in this PR's diff, so I could not open a fresh inline thread; it is recorded here only.
  • New, pkg/databricks/unity_catalog.go:23-24,118-139 (B10, endpoint added): the PR adds the catalogs list/get endpoints, and the repo has no spec/openapi.json. Run the build-openapi-spec.md skill so the spec covers the Unity Catalog endpoints. Confidence: high. Not blocking.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `pkg/databricks/unity_catalog.go`:
- Around line 132: GetCatalog builds its URL with JoinPath(unityCatalogCatalogsEndpoint, name) and does not check that the name survives URL assembly. prepareRequest unescapes the URL again, so a name containing "?", "#" or "%" reads a different catalog, and Grant/Revoke then PATCH that catalog through routedEntitlement in catalog.go. Before sending the request, call addressesSecurable(u, "/"+name) and return invalidArgument when it fails, the same way permissionsUrl does. Also consider checking in catalogThrough (pkg/connector/catalog.go around line 405) that the returned catalog.Name equals the requested catalogName. Add a test with a name like "sales?x".

## Suggestions

In `pkg/connector/metastore_helpers.go`:
- Around line 261-268: workspaceIsUsable returns true for CANCELLING, but the comments at lines 34-35 and 245-247 say a cancelling workspace will not become RUNNING and is left out of scope. Either remove workspaceStatusCancelling from the true case or update the comments to match the behavior.

In `pkg/databricks/unity_catalog.go`:
- Around lines 23-24 and 118-139: new Unity Catalog catalog endpoints were added and the repo has no spec/openapi.json. Run the build-openapi-spec.md skill to generate it so it covers these endpoints.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Connector PR Review: [CXH-2721] - Add Unity Catalog catalog sync - Databricks Connector

Blocking Issues: 1 | Suggestions: 2 | 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

This PR adds an opt-in Unity Catalog catalog resource that hangs from metastore, keyed {metastore_id}::{name}. Each catalog gets static per-privilege entitlements plus a read-only owner. Grants and Grant/Revoke reuse the shared securable helpers. Metastore List stores each metastore's routes (running in-scope workspaces and owner) in the session store, and catalog List reads them back and walks one workspace per page-bag frame. main.go now enables the session store. I scanned the full PR diff for security and correctness, focusing on catalog.go, catalog_session.go, securable_helpers.go, metastore*.go, unity_catalog.go, and the wiring in connector.go, resource_types.go and main.go. I did not read the test files and docs line by line. No repo-local criteria loaded, so I applied the base prompt and the connector mixin.

The pagination, the 401/403/404 handling (no empty-list fallbacks, and the CATALOG_DOES_NOT_EXIST skip is documented), OptInRequired/SkipEntitlements on the type, ChildResourceType on the metastore, and the Grant/Revoke entity sources all look correct.

Security Issues

None found.

Correctness Issues

  • Prior — still present, pkg/databricks/unity_catalog.go:132: GetCatalog still joins the raw catalog name with JoinPath and skips the addressesSecurable guard that permissionsUrl (unity_catalog.go:342) applies. prepareRequest (request.go:267-272) unescapes the URL again, so a name containing ?, # or % reads a different catalog: sales?x reads sales. catalogThrough (catalog.go:405) checks only MetastoreID. routedEntitlement (catalog.go:704) then rebuilds the resource id from the returned catalog.Name, so resolveChange and the guarded permissionsUrl both see sales, and the PATCH lands on that catalog. Confidence: high on the code path, medium on how often such names occur. The existing open thread already covers this.

Suggestions

  • Prior — still present, pkg/connector/metastore_helpers.go:263: workspaceIsUsable still accepts CANCELLING, but the comments at lines 34-35 and 245-247 say such workspaces are out of scope. A metastore attached only to a cancelling workspace fails metastore and catalog List with FailedPrecondition on every sync. This fails closed. Confidence: high. These lines are no longer in this PR's diff, so I could not open a fresh inline thread; it is recorded here only.
  • New, pkg/databricks/unity_catalog.go:23-24,118-139 (B10, endpoint added): the PR adds the catalogs list/get endpoints, and the repo has no spec/openapi.json. Run the build-openapi-spec.md skill so the spec covers the Unity Catalog endpoints. Confidence: high. Not blocking.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `pkg/databricks/unity_catalog.go`:
- Around line 132: GetCatalog builds its URL with JoinPath(unityCatalogCatalogsEndpoint, name) and does not check that the name survives URL assembly. prepareRequest unescapes the URL again, so a name containing "?", "#" or "%" reads a different catalog, and Grant/Revoke then PATCH that catalog through routedEntitlement in catalog.go. Before sending the request, call addressesSecurable(u, "/"+name) and return invalidArgument when it fails, the same way permissionsUrl does. Also consider checking in catalogThrough (pkg/connector/catalog.go around line 405) that the returned catalog.Name equals the requested catalogName. Add a test with a name like "sales?x".

## Suggestions

In `pkg/connector/metastore_helpers.go`:
- Around line 261-268: workspaceIsUsable returns true for CANCELLING, but the comments at lines 34-35 and 245-247 say a cancelling workspace will not become RUNNING and is left out of scope. Either remove workspaceStatusCancelling from the true case or update the comments to match the behavior.

In `pkg/databricks/unity_catalog.go`:
- Around lines 23-24 and 118-139: new Unity Catalog catalog endpoints were added and the repo has no spec/openapi.json. Run the build-openapi-spec.md skill to generate it so it covers these endpoints.

Reviewed commit: a343b6888be7

@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.

Blocking issues found — see the full review report

mateoHernandez123 and others added 2 commits November 3, 2026 16:58
GetCatalog joined the raw catalog name into the path and skipped the
addressesSecurable guard that permissionsUrl applies. prepareRequest
unescapes the URL again before sending it, so a name holding "?", "#" or
"%" addressed a different catalog: GetCatalog("sales?x") read "sales" and
returned it.

That answer reached a write. catalogThrough checked only MetastoreID, and
routedEntitlement rebuilds the resource from the returned catalog.Name, so
Grant and Revoke sent their PATCH to the catalog Databricks answered with
rather than the one the entitlement named.

GetCatalog now runs the same guard permissionsUrl runs, and rejects an
answer whose name is not the name it asked for. catalogThrough passes
InvalidArgument through instead of folding it into catalogListingError,
which would have degraded it to Unknown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The profile workspace was reused whenever it was still attached, but it
was picked under the owner the last sync saw, and the owner decides which
workspaces read every privilege: with the connector owning the metastore
any of them does, otherwise only the ones it is a direct Admin of.

After a transfer the recorded workspace can be one the connector no
longer reads, while another attached workspace still satisfies the
documented rule. Grant and Revoke refused the write with PermissionDenied
and told the operator to grant Admin, on a metastore that was reachable.

The route is now kept only when it still reads under the live owner, so
the saved account round trip survives a transfer that does not move what
the connector can read, and otherwise pickReadableWorkspace runs again.

Also corrects the GetUncached comment: Cache-Control is part of the cache
key, so the no-cache read does not refresh the entry normal reads look up.
The behaviour is right and the comment described the opposite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mateoHernandez123
mateoHernandez123 dismissed github-actions[bot]’s stale review November 3, 2026 20:57

Dismissing: this review is on a343b68 and both findings have moved since.

The blocking one, GetCatalog addressing a different catalog, is fixed in 2855a2b. GetCatalog now runs the same addressesSecurable guard permissionsUrl runs and rejects an answer whose name is not the one it asked for, and catalogThrough passes the InvalidArgument through instead of degrading it to Unknown. Two tests cover it and both fail without the guard.

The CANCELLING suggestion is declined, with the reasoning in the thread. Dropping CANCELLING from workspaceIsUsable takes the workspace out of scope, which leaves the metastore that routed through it out of the sync, and ConductorOne reads that as a deletion and removes its grants. The status set is deliberate; the comments that described it as dropped were the wrong half and are corrected in #90.

The openapi spec note is tracked as a follow-up.

@FeliLucero1 FeliLucero1 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.

Pass on f8cd1917. CI is green and this looks close: the errMetastoreGone / %w fixes, the GetCatalog guard (I traced ?, #, %2F and ..; all rejected), the system/samples skip and the session-store routes all check out. There's one low inline, and I bumped the CATALOG_DOES_NOT_EXIST thread.

// it is a direct Admin of. A transfer can therefore leave the recorded
// workspace unreadable while another attached one still works, so the route
// is kept only when it still reads under the live owner.
if workspace != nil && profileOwner != liveOwner {

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.

[low] The readability re-check only runs when the owner string changed. With the owner unchanged and the connector not owning it, the route was picked because we're a direct Admin there. If that Admin assignment gets removed on this workspace but stays on another attached one, we keep this route and resolveChange fails with PermissionDenied (fatal in C1), even though the other workspace can read. It fails closed and fixes itself on the next sync, so it's non-blocking. Dropping the profileOwner != liveOwner part would cover it for one extra ListWorkspaceMembers.

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.

10 participants