Skip to content

Authorize source access before purification - #38564

Draft
tonydu-mz wants to merge 1 commit into
mainfrom
tonydu/sql-660-create-table-from-source-and-alter-source-drive-the-sources
Draft

Authorize source access before purification#38564
tonydu-mz wants to merge 1 commit into
mainfrom
tonydu/sql-660-create-table-from-source-and-alter-source-drive-the-sources

Conversation

@tonydu-mz

@tonydu-mz tonydu-mz commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Authorize source access before purification

Fixes SQL-660.

Motivation

CREATE TABLE ... FROM SOURCE and ALTER SOURCE are purified off-thread
before they are planned. The only privilege check that ran before purification
was rbac::check_usage(.., &CREATE_ITEM_USAGE), which requires USAGE on the
Secret, Connection and Type items the statement names. These statements
name neither a secret nor a connection: they reach the upstream through an
existing source, whose connection comes from its own source_desc() rather
than from the statement. So nothing was required.

Purification then opens that connection with the source owner's credentials and
enumerates upstream objects. A role holding no privilege on the source could
therefore make Materialize dial the source's upstream, and read upstream schema,
table and column names out of the resulting purification errors. SQL-655
(#38480) closed the plan-time bypass, so no rows are readable; this is the
residual that fix named.

CREATE SOURCE ... FROM CONNECTION and CREATE SINK ... INTO are unaffected:
they name their connection, so the existing usage requirement gates them.

Description

Replace the pre-purification check_usage call with rbac::check_purification,
which builds a single RbacRequirements for the statement and delegates to the
same validation path check_plan uses:

  • the existing CREATE_ITEM_USAGE usage requirements, unchanged;
  • for ALTER SOURCE: ownership of the named source;
  • for CREATE TABLE ... FROM SOURCE: read privileges on the source (SELECT
    plus schema USAGE), required of the owner too, since an owner's SELECT is
    an ordinary revocable grant and schema USAGE is separate from ownership.

Both mirror what planning requires later, so a statement that passes here can
still be rejected by check_plan, never the reverse.

The source is resolved from the statement rather than from resolved_ids.
AlterSourceStatement::source_name is an UnresolvedItemName, so name
resolution never records it and a resolved_ids-based check is silently a no-op
for ALTER SOURCE. Resolution mirrors purification exactly, so the check gates
the item that would be dialed.

The resolved source id is also added to the purified statement's dependency set.
It was previously absent for ALTER SOURCE, so a source dropped concurrently
with off-thread purification passed the validity check and then panicked the
coordinator on the missing catalog entry ("catalog out of sync") during
planning. With the id tracked, the drop is detected and the statement is
repurified, ending in a clean unknown-item error.

Verification

test/sqllogictest/rbac_create_table_from_source.slt gains cases that pin the
ordering, not just the denial. Each uses a statement whose purification fails
for a non-permission reason, so a permission error can only come from the
pre-purification check:

  • CREATE TABLE ... FROM SOURCE with an unresolvable reference: purification
    reports whether a reference exists upstream, so without the gate this leaks
    reference existence.
  • The same statement run by the source's owner after their SELECT is revoked,
    pinning that ownership does not stand in for read privileges.
  • ALTER SOURCE ... ADD SUBSOURCE on a load generator, which purification
    rejects with "does not support ALTER SOURCE": an ownership error can only
    come from the earlier gate.

Plus: an owner-succeeds case, so the rule gates the caller rather than the
syntax; a schema-USAGE denial, pinning the other half of the read
requirement; and pass-through cases for a superuser and for an RBAC-disabled
deployment, each reaching purification's own error on a source they do not
own, pinning that the gate filters requirements the same way check_plan
does.

@linear-code

linear-code Bot commented Aug 29, 2026

Copy link
Copy Markdown

SQL-660

@tonydu-mz tonydu-mz self-assigned this Sep 1, 2026
@tonydu-mz
tonydu-mz force-pushed the tonydu/sql-660-create-table-from-source-and-alter-source-drive-the-sources branch 2 times, most recently from 6dbe163 to c8dd2ba Compare September 2, 2026 18:39
Fixes SQL-660.

### Motivation

`CREATE TABLE ... FROM SOURCE` and `ALTER SOURCE` are purified off-thread
before they are planned. The only privilege check that ran before purification
was `rbac::check_usage(.., &CREATE_ITEM_USAGE)`, which requires `USAGE` on the
`Secret`, `Connection` and `Type` items the statement names. These statements
name neither a secret nor a connection: they reach the upstream through an
existing source, whose connection comes from its own `source_desc()` rather
than from the statement. So nothing was required.

Purification then opens that connection with the source owner's credentials and
enumerates upstream objects. A role holding no privilege on the source could
therefore make Materialize dial the source's upstream, and read upstream schema,
table and column names out of the resulting purification errors. SQL-655
(#38480) closed the plan-time bypass, so no rows are readable; this is the
residual that fix named.

`CREATE SOURCE ... FROM CONNECTION` and `CREATE SINK ... INTO` are unaffected:
they name their connection, so the existing usage requirement gates them.

### Description

Replace the pre-purification `check_usage` call with `rbac::check_purification`,
which builds a single `RbacRequirements` for the statement and delegates to the
same validation path `check_plan` uses:

* the existing `CREATE_ITEM_USAGE` usage requirements, unchanged;
* for `ALTER SOURCE`: ownership of the named source;
* for `CREATE TABLE ... FROM SOURCE`: read privileges on the source (`SELECT`
  plus schema `USAGE`), required of the owner too, since an owner's `SELECT` is
  an ordinary revocable grant and schema `USAGE` is separate from ownership.

Both mirror what planning requires later, so a statement that passes here can
still be rejected by `check_plan`, never the reverse.

The source is resolved from the statement rather than from `resolved_ids`.
`AlterSourceStatement::source_name` is an `UnresolvedItemName`, so name
resolution never records it and a `resolved_ids`-based check is silently a no-op
for `ALTER SOURCE`. Resolution mirrors purification exactly, so the check gates
the item that would be dialed.

The resolved source id is also added to the purified statement's dependency set.
It was previously absent for `ALTER SOURCE`, so a source dropped concurrently
with off-thread purification passed the validity check and then panicked the
coordinator on the missing catalog entry ("catalog out of sync") during
planning. With the id tracked, the drop is detected and the statement is
repurified, ending in a clean unknown-item error.

### Verification

`test/sqllogictest/rbac_create_table_from_source.slt` gains cases that pin the
ordering, not just the denial. Each uses a statement whose purification fails
for a non-permission reason, so a permission error can only come from the
pre-purification check:

* `CREATE TABLE ... FROM SOURCE` with an unresolvable reference: purification
  reports whether a reference exists upstream, so without the gate this leaks
  reference existence.
* The same statement run by the source's owner after their `SELECT` is revoked,
  pinning that ownership does not stand in for read privileges.
* `ALTER SOURCE ... ADD SUBSOURCE` on a load generator, which purification
  rejects with "does not support ALTER SOURCE": an ownership error can only
  come from the earlier gate.

Plus: an owner-succeeds case, so the rule gates the caller rather than the
syntax; a schema-`USAGE` denial, pinning the other half of the read
requirement; and pass-through cases for a superuser and for an RBAC-disabled
deployment, each reaching purification's own error on a source they do not
own, pinning that the gate filters requirements the same way `check_plan`
does.
@tonydu-mz
tonydu-mz force-pushed the tonydu/sql-660-create-table-from-source-and-alter-source-drive-the-sources branch from c8dd2ba to 4e17335 Compare September 3, 2026 01:55
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.

1 participant