Repository navigation
[CXH-2723] - Add Unity Catalog table and volume sync - Databricks Connector - #73
Conversation
920df46 to
f9142b4
Compare
0ee9040 to
38e403e
Compare
f9142b4 to
29274ee
Compare
38e403e to
7c8e7a3
Compare
29274ee to
e74210e
Compare
7c8e7a3 to
a3bd512
Compare
e74210e to
7deff05
Compare
a3bd512 to
5dce9b1
Compare
7deff05 to
1253d7e
Compare
5dce9b1 to
5effb3f
Compare
FeliLucero1
left a comment
There was a problem hiding this comment.
Did a pass on this one. Main things: the per-table perf (the stack-wide one actually bites here), the List/Revoke 404 handling copied from #72, and the table_type mapping dropping REFRESH / ALL_PRIVILEGES grants from the sync. The rest are nits.
Comments tagged (out of scope) are inherited or stack-wide, so treat them as FYI, not blocking for this PR. Build/vet/tests pass locally.
Co-authored-by: Cursor <[email protected]>
… sync Carries the main merge from the parent branches: the uhttp cache now keys on both the host scope header and Cache-Control, and the workspace assignment read-before-write uses GetUncached. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
… stack The tree is unchanged; this only propagates the parent link so GitHub sees main as merged. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
FeliLucero1
left a comment
There was a problem hiding this comment.
Round 4. Nice rebuild: the generic securable flow + route in the page token fixes most of what we flagged (view ALL_PRIVILEGES, table_type sets, perf on the Grants side). Two things below I'd want before merge: the children completeness proof (:186) and the empty list when usable[0] doesn't see the catalog (:163). Also the revoke-on-dropped mapping got lost in the rebuild (replied on that thread).
(out of scope, #71/#72) CI test is red here, on #71 and on #72: cannot list every schema in catalog system (built-in catalog, exists on every tenant), and on #71 cannot read every privilege on catalog test_workspace. Fail-closed is the right call per c1, but right now any SP that isn't metastore owner/admin aborts the whole sync, including the types that already worked. Probably worth skipping system and fixing the CI tenant perms down in #71/#72.
…schema-sync' into mateoHernandez123/unity-catalog-table-and-volume-sync
…Hernandez123/unity-catalog-table-and-volume-sync Co-authored-by: Cursor <[email protected]> # Conflicts: # docs/connector.mdx # pkg/connector/schema.go
…he schema listing USE_SCHEMA and MANAGE make ListSchemas return every schema. Neither makes ListTables or ListVolumes return every child, so reusing the schema proof synced the subset Databricks chose to show and ConductorOne deleted the rest. The child listing now needs ownership of the metastore, the catalog or the schema, the workspace-admins-group path, or ALL_PRIVILEGES on the catalog. pushSchemaChildRoute also walks every running in-scope workspace instead of taking the first one and emitting nothing when it does not list the catalog. MODIFY on a streaming table is reported when already held and refused on Grant: the streaming-table privilege reference lists SELECT and REFRESH only. Co-authored-by: Cursor <[email protected]>
…Hernandez123/unity-catalog-table-and-volume-sync Co-authored-by: Cursor <[email protected]> # Conflicts: # docs/docs-info.md
… as already revoked The gone signal landed with the catalog level only, so a revoke on a table, volume or schema that Databricks had dropped still failed on every retry. Each level now declares the error_code values that mean it is gone, parents included: dropping a catalog drops its schemas, and dropping a schema drops its tables and volumes, so any of those codes leaves nothing to revoke. A 404 carrying no code, or one naming something else, still fails. That is a workspace the securable is not reachable through, where the privilege is still held. Co-authored-by: Cursor <[email protected]>
…t is partial The sync stops on system. It is built in, owned by System user, and Databricks refuses a MANAGE grant on it, same as samples. This connector has never synced either, so leaving them out deletes nothing. Any other catalog still fails the sync, which keeps what ConductorOne already has. Co-authored-by: Cursor <[email protected]>
sergiocorral-conductorone
left a comment
There was a problem hiding this comment.
Automated review (follow-up/first pass) — drafted 3 → confirmed 3, low-confidence 0, dropped 0, added 0 (panel: opus-5.5 / gpt-6-astra / gemini-4-argon, all high).
…Hernandez123/unity-catalog-table-and-volume-sync Co-authored-by: Cursor <[email protected]> # Conflicts: # docs/connector.mdx # docs/docs-info.md # pkg/connector/catalog.go # pkg/connector/catalog_test.go # pkg/connector/schema.go
Co-authored-by: Cursor <[email protected]>
FeliLucero1
left a comment
There was a problem hiding this comment.
Round 5. Looks good, the main asks are fixed (children completeness proof, no silent empty list, revoke on dropped targets, route docs) and CI is green. Approvable from my side; the comments below are small follow-ups.
(out of scope, #71 catalog.go:270) built-in catalog skip:
__databricks_internal(INTERNAL_CATALOG, Lakeflow pipelines) isn't covered, so it'd fail the sync the same waysystemdid- the docs don't say
samplesisSYSTEM_CATALOG(could be Delta Sharing), so worth confirming - this commit duplicates logic already on #71's tip (
abee6af5), so watch the rebase
nits (outside the diff): schema.go:195 still says the schema List skips system (only the catalog syncer does), catalog_test.go:356 still says "never synced / deletes nothing", and the README schema-listing rule leaves out metastore owner + the admin path that docs-info has.
…schema-sync' into mateoHernandez123/unity-catalog-table-and-volume-sync
…efore the trust check Co-authored-by: Cursor <[email protected]>
…code Co-authored-by: Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
…lume sync The conflicts were two layers adding to the same places: unityCatalogVars gained include_unbound below and the table payload vars here, and the client test file gained one test on each side. Both resolved as a union. holdsPrivilegeOn now takes the gone error codes of the level that calls it. The table and volume trust rules name none, so their reads are unchanged. The workspace admins group that owns a catalog is checked against the workspace its name carries, here as well. Co-Authored-By: Claude Opus 5 <[email protected]>
| volumeResourceType = &v2.ResourceType{ | ||
| Id: "volume", | ||
| DisplayName: "Volume", | ||
| Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_APP}, |
There was a problem hiding this comment.
Are all the new resource types apps? I think it's fine if they have no trait. A profile can go on any resource now.
| Id: "volume", | ||
| DisplayName: "Volume", | ||
| Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_APP}, | ||
| Annotations: annotationsForStaticSecurableResourceType(securablePermissions(permissionUseCatalog, permissionUseSchema)...), |
There was a problem hiding this comment.
This was a little hard for me to follow, but it might be simpler to change these to:
volumeResourceType = &v2.ResourceType{
Id: "volume",
DisplayName: "Volume",
Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_APP},
Annotations: annotations.New(
&v2.OptInRequired{},
&v2.SkipEntitlements{},
capabilityPermissions(securablePermissions(permissionUseCatalog, permissionUseSchema)...),
),
}That way anyone reading the code can quickly tell which resource types are opt in, which ones skip entitlements/grants, etc.
| rs.WithAnnotation(&v2.RawId{Id: resourceID}), | ||
| rs.WithAnnotation( | ||
| // Without these annotations the table and volume syncers' List is | ||
| // never invoked. |
There was a problem hiding this comment.
Nitpick: I don't think this comment is necessary. Child resource types always work this way.
FeliLucero1
left a comment
There was a problem hiding this comment.
Round 6. Looks good, CI green, and thanks for the walk/comment fixes. One thing I'd fix before merge: the RESOURCE_DOES_NOT_EXIST mapping on volumes (inline). I'm fine leaving the non-owner revoke path out as you said.
Leftovers outside the diff: schema.go:195 still says the schema List leaves system out (only the catalog syncer does). (out of scope, #71) the built-in skip still doesn't cover __databricks_internal (INTERNAL_CATALOG).
| // errorCodeVolumeGone is the code the volume reference uses. The permissions | ||
| // endpoint on a missing volume answers RESOURCE_DOES_NOT_EXIST instead. | ||
| errorCodeVolumeGone = "VOLUME_DOES_NOT_EXIST" | ||
| errorCodeVolumePermissionsGone = "RESOURCE_DOES_NOT_EXIST" |
There was a problem hiding this comment.
[medium] careful with this one. RESOURCE_DOES_NOT_EXIST is the generic 404 code (error codes), and our own tests treat it as a route miss that must still fail: table_test.go:417 ("a bare 404 also comes back from a workspace the table is not reachable through, where the privilege is still held") and catalog_test.go:781. For volumes it now means GrantAlreadyRevoked with no PATCH. So if a stale/unreachable workspace answers with it, the revoke "succeeds" and the access stays. Since f2fca90 there's also no volume revoke test. Suggest: on RESOURCE_DOES_NOT_EXIST, confirm with GET /unity-catalog/volumes/{full_name} through the same workspace and only treat it as gone if that 404s too (200 = route problem → error). Plus a test for both cases.
|
|
||
| // errorCodeVolumeGone is the code the volume reference uses. The permissions | ||
| // endpoint on a missing volume answers RESOURCE_DOES_NOT_EXIST instead. | ||
| errorCodeVolumeGone = "VOLUME_DOES_NOT_EXIST" |
There was a problem hiding this comment.
nit: VOLUME_DOES_NOT_EXIST isn't in the documented error_code enum (TABLE/SCHEMA/CATALOG/METASTORE_DOES_NOT_EXIST are, and so is generic RESOURCE_DOES_NOT_EXIST), so this is probably never returned. Harmless, but the comment makes it sound real.
sergiocorral-conductorone
left a comment
There was a problem hiding this comment.
Automated review (follow-up round on new commits).
Stats: drafted 3 → confirmed 3, low-confidence 0, dropped 0, added 0 (panel: opus-5.5 / gpt-6-astra / gemini-4-argon, all high).
Findings not anchorable to a diff line:
pkg/connector/catalog.go:419— [major]catalogThroughused to answer(nil, nil)for bothCATALOG_DOES_NOT_EXISTand a 401/403/404 deny, and every caller's walk treatedcatalog == nilas "try the next workspace". Now it returnserrCatalogGoneor the raw deny error, but not every caller was updated:pushSchemaChildRoute(schema_child.go:169-172) still doesif err != nil { return err }. A deny or a gone code onusable[0]ends the table/volume walk there.- schema
pushRoute(schema.go:305-311) continues on a deny, but returnserrCatalogGoneas a plain error.
This PR's own logic (workspaceForCatalog's allGone and confirmCatalogGone's "still exists but no workspace in sync scope can list it") assumes a workspace the catalog is not bound to answers CATALOG_DOES_NOT_EXIST. So for an ISOLATED catalog bound only to usable[1], the catalog syncer emits it through workspace B. Then the schema List and every table/volume List under it fail on workspace A, and the sync never completes on multi-workspace accounts. That undoes the "walk usable until one lists it" fix from the schema_child.go:163 thread. Suggest treating errors.Is(err, errCatalogGone) || deniesTrust(err) as continue in both walks, keeping lastDeny for the final error message the way schema.go now does. Please add a two-workspace test where wsa answers 404 CATALOG_DOES_NOT_EXIST and wsb serves the catalog, for both schemas and tables.
pkg/connector/catalog.go:316— [minor, perf] With theseendedupe gone, a catalog bound to W in-scope workspaces is emitted W times, and every OPEN catalog is bound to all of them. The store does collapse the catalog row on upsert. But the SDK syncer queues a child-resource sync for each resource a List returns, so the catalog's whole subtree is listed W times: schemas, then tables and volumes per schema. That subtree includes the uncached per-schema completeness proof (schemaListingIsComplete/childListingIsComplete) that the listSchemaChildren comment already calls quadratic. The live measurement here was a single-workspace tenant, so W=1 hides it. If the oldseenmap was dropped because it outlived a sync, a sync-safe alternative would be to emit an OPEN catalog only from the first route that serves it, or to carry the names already emitted in the bag state.
| trusts: volumeGrantsAreTrustworthy, | ||
| // Dropping a parent drops the volume with it, so any of the three codes | ||
| // means there is no volume left to revoke on. | ||
| goneErrorCodes: []string{errorCodeVolumeGone, errorCodeVolumePermissionsGone, errorCodeSchemaGone, errorCodeCatalogGone}, |
There was a problem hiding this comment.
[major] RESOURCE_DOES_NOT_EXIST is Databricks' generic 404 code, not a volume-specific one. Adding it to volumeLevel.goneErrorCodes now has two effects. In revokeSecurable, any 404 carrying it answers GrantAlreadyRevoked. In the new syncSecurableGrants branch (securable_helpers.go:168-175), it returns zero grants with a nil error. The PR's stated rule is "any other 404 still fails, because an unbound or unprivileged workspace answers 404 too". This code is the one most likely to be shared with those non-deletion 404s, such as a volume read through a workspace that cannot serve it, or a routing miss. When that happens, Revoke tells C1 the access is gone while it stays in Databricks, and Grants silently reports an empty volume. Measuring "missing volume → RESOURCE_DOES_NOT_EXIST" on one tenant shows the code is necessary, not that it is sufficient. Suggest: on RESOURCE_DOES_NOT_EXIST, confirm with GET /api/2.1/unity-catalog/volumes/{full_name}, and treat the volume as gone only when that answers VOLUME_DOES_NOT_EXIST (or the parent codes). Otherwise refuse the read. Please also add a volume Revoke test and a Grants test for this code. Neither path is tested in this diff.




Description
Last of four stacked PRs for Unity Catalog (CXH-2723). Tables and volumes are the data assets an access review is about. They are siblings under a schema.
#70 is merged. #71 and #72 merge before this one.
What the connector syncs
Before, with #72 applied. The schema is a leaf.
After this PR. The tree reaches the data assets.
stagingstagingSync:
table, NEW) — child ofschema. Unity Catalog uses onetablesecurable for tables, views, materialized views, metric views and streaming tables.table_typepicks the privilege set, so entitlements are per instance.volume, NEW) — child ofschema, read withGET /api/2.1/unity-catalog/volumes. The privilege set is static, plus a read-onlyowner.table_typedecides which privileges the connector offers on each table:Provisioning:
table_type, so ConductorOne does not offer an entitlement the grants API rejects.ALL_PRIVILEGESis emitted when Databricks already holds it and is not requestable. Grant returnsInvalidArgumentbefore any PATCH. Revoke still removes it.REFRESH.REFRESHstays requestable.SELECT,READ_METADATA,APPLY_TAG,MANAGE,ALL_PRIVILEGES,REFRESHandMODIFY.REFRESHstays requestable. The table DML trio (DELETE,INSERT,UPDATE) is not offered.MANAGED,EXTERNAL,MANAGED_SHALLOW_CLONEandEXTERNAL_SHALLOW_CLONE: the table set, which does not includeREFRESH.FOREIGN: read-only.DELETE,INSERT,UPDATEandMODIFYare not offered.table_type: the union of every variant, so a heldREFRESHis not dropped. ThatREFRESHis not requestable, and Grant returnsInvalidArgumentbefore any PATCH.PATCHpath and the same echo-confirmed idempotency:GrantAlreadyExistsandGrantAlreadyRevoked.TABLE_DOES_NOT_EXIST,VOLUME_DOES_NOT_EXIST,SCHEMA_DOES_NOT_EXIST,CATALOG_DOES_NOT_EXIST) isGrantAlreadyRevoked. Any other 404 still fails, because an unbound or unprivileged workspace answers 404 too.Auth:
Credentials are unchanged. There is no new config field.
The schema listing from #72 decides what this PR can read. Schema List fails with
PermissionDeniedunless that listing is complete, and tables and volumes are read through the same workspace.information_schemais skipped.A direct Admin is enough when the catalog is owned by that workspace's admins group. The connector reads the workspace id off the group name
_workspace_admins_<workspace>_<id>and checks that workspace, whatever route the securable is listed through. That rule arrived from #71 and applies to the table and volume trust rules here as well.Architecture highlights:
omit_columnsandomit_properties. It does not sendomit_username, because that one also dropsowner, and every securable's profile and owner grant depend on it. The client test asserts the exact query set, so adding it fails there.max_resultsabove 50. The client clamps to 50.holdsPrivilegeOnis the one uncached read of the connector's own effective privileges. Each level passes the privileges it accepts and the error codes that prove the securable is gone. The table and volume levels name none, so every 404 there leaves the privilege unproven rather than claiming an absence.Find*filters. A 400 on the owner SCIM filter continues to the next principal kind: user, then group, then service principal. A 500 fails Grants. The rejection is logged at Debug.Useful links: