Uh oh!
There was an error while loading. Please reload this page.
Require read privileges for CREATE TABLE ... FROM SOURCE - #38480
Require read privileges for CREATE TABLE ... FROM SOURCE#38480tonydu-mz wants to merge 1 commit into
Conversation
598170c to
6d29cb0Compare6d29cb0 to
1d929f7Comparedef-
commented
Aug 26, 2026
QA LLM Review1. MEDIUM -- Requiring read on every export of the reference lets one tenant's table block all other roles, including the source owner
Because the requirement is the conjunction over all cataloged exports of the reference, a single export sitting in a schema other roles cannot see denies the attach to everyone else, the source owner included, even when they can already read that reference through an export they are authorized for. In the deployment shape the PR description targets (platform team owns sources, app teams attach tables into their own schemas), the first app team to attach a reference locks out every other team and the platform team. Details
Concretely: the platform team owns source That is stricter than the stated goal of requiring "the privileges a direct read of that data requires" — reading The sound criterion is disjunctive rather than conjunctive: ownership of the parent, or 2. MEDIUM -- |
ff68319 to
4254e51Comparedef-
commented
Aug 26, 2026
QA LLM Review1. MEDIUM -- Column-name coverage is not a read-authorization proxy on Kafka sources, where the caller picks the column names
The coverage rule compares column name sets, but Details
Concretely, on the shape in CREATETABLEapp_schema.raw (key, f1, f2) FROM SOURCE avro_source (REFERENCE 'topic')
FORMAT BYTES ENVELOPE NONE INCLUDE HEADERS, PARTITION;The desc is Column names carry meaning on the connectors where they come from the upstream schema, and none on the connectors where the caller supplies them. Restricting the coverage branch to the reference types whose names are purification-derived, and falling through to the ownership requirement otherwise, would keep the rule sound; comparing the planned table's |
fb5c503 to
5c440d0Comparetonydu-mz
commented
Aug 26, 2026
Thanks Dennis! I address all the comments |
There was a problem hiding this comment.
On reading this change, at a high level, it feel like it's a bit hacky/not principled. Although the motivation is clear, I don't like the implicitness of allowing access based on existing privileges of adjacent/similar looking tables.
Take the following example:
Team B has SELECT on source pg_src and on source table safe.customers, and their dbt run attaches source table public.customers fine on Monday. On Tuesday the platform team drops or narrows safe.customers during their own migration. Team B's identical dbt run now fails with must be owner of SOURCE pg_src.
Nothing else in our RBAC model follows this pattern. For the time being, I think I'd rather make the RBAC policy a bit dumb and either:
- Assert that the table creator must be an owner of the source (means separate teams can't export source tables separate from the platform team. Might be a breaking change)
- Assert that the table creator must also have SELECT privilege on the source, similar to sinks (means sub team A can access everything from sub team B). This would be my pick (also don't forget to update docs!!)
In the longer term, I think something really nice could be PostgreSQL column level SELECT grants where you can grant SELECT on certain columns of a relation. Syntax could look like:
GRANT SELECT (name, department)
ON pg_src (REFERENCE "public"."customers")
TO analyst;
but this would be a larger feature.
tonydu-mz
commented
Aug 26, 2026
Thanks Jun. That's a fair point. I'll take a look at this more tonight |
Fixes SQL-655 (2026 Refactor penetration test, finding H02, High).
### Motivation
`CREATE TABLE ... FROM SOURCE` bypassed source authorization. The statement
plans to a generic `Plan::CreateTable`, whose RBAC arm required only `CREATE`
on the destination schema and never inspected `table.data_source`. Because the
resulting table is owned by whoever creates it, `CREATE` on any schema a role
controls was enough to read a source that role had been explicitly denied.
The assessment read 4,076 rows from an Auction load generator through an
attached table while direct reads of the same export stayed denied. The
behaviour has been reachable by default since v26.25.0 and was still present in
v26.35.0.
### Description
The `Plan::CreateTable` arm now inspects `table.data_source` and, for an
`IngestionExport`, requires the privileges reading that source requires:
`SELECT` on the source plus `USAGE` on its schema, via `generate_read_privileges`.
This is the same shape as `CREATE SINK`, which requires read on the relation it
reads.
**The source is the authorization boundary.** `SELECT` on a source permits
attaching any reference that source ingests, including references that have no
cataloged export yet, and including columns that some existing export omitted.
If a reference should not be reachable, it should not be in the source's
publication or topic set.
An earlier revision of this change tried to be finer-grained, authorizing an
attach when the role could already read an existing export of the same reference
whose columns covered the new table's. That is not implemented, deliberately.
It made the outcome depend on unrelated catalog state: narrowing or dropping one
team's table could revoke another team's ability to run an unchanged statement,
with an error naming the source rather than the table that actually changed. No
other part of the RBAC model works that way, and per-column granularity belongs
in real column-level grants rather than being inferred from which exports happen
to exist.
This means pentest remediation item 2 ("require `SELECT` on the selected
cataloged subsource or source export when one exists") is intentionally not
implemented, since it is that same catalog-dependent rule. Item 1 is what closes
the finding, and it is implemented here.
### User-visible effect
This tightens an existing privilege requirement, and
`enable_create_table_from_source` is on by default, so it applies to everyone on
upgrade. A role that previously needed only `CREATE` on its own schema plus
`USAGE` on the source's schema now also needs `SELECT` on the source.
Deployments where a platform team owns sources and application teams attach
tables into their own schemas will need those `SELECT` grants added. The
user-facing privileges include for `CREATE TABLE` is updated in a follow-up PR,
kept separate so this fix is not gated on a second CODEOWNERS scope.
### Verification
New `test/sqllogictest/rbac_create_table_from_source.slt` pins both halves of the
requirement and the stability property: a direct read is denied; the attach is
denied without `SELECT` on the source; the attach is denied for a role that has
`SELECT` on the source but no `CREATE` on the destination schema; it succeeds
with both; a second role with the same grants can attach the same reference after
another team has already exported it; and a reference with no existing export is
authorized identically, so the rule does not vary with catalog shape.
Registered in `tests_without_views` alongside the other RBAC suites, since
`--auto-index-selects` view-wrapping would change privilege semantics.
### Residual, not fixed here
Purification runs before authorization. `must_spawn_purification` gates it on
`CREATE_ITEM_USAGE`, which covers secrets, connections and types but not
sources, and the upstream connection is taken from the source's own
`source_desc()` rather than from the statement's resolved ids. An unauthorized
role can therefore still cause an outbound connection to the source's upstream
using the source owner's credentials through a connection it holds no `USAGE`
on, and use purification errors as an upstream existence and column-name oracle.
This is shared with `CREATE SOURCE` and `CREATE SINK` rather than introduced
here, and closing it needs a pre-purification privilege gate. Filed separately.5c440d0 to
1699d8aCompare### Motivation `CREATE TABLE ... FROM SOURCE` populates the new table from an existing ingestion, so creating it reads that source's data. #38480 makes the privilege requirements match that, fixing a case where `CREATE` on the destination schema alone was enough to read a source the role had been denied (SQL-655, 2026 penetration test finding H02). The privileges include for `CREATE TABLE` still lists only the schema and type requirements, so it understates what the statement needs. ### Description Add the read requirement to `create-table.md`, the single include used by all five `CREATE TABLE` pages and by the generated privileges appendix: `SELECT` on the source plus `USAGE` on its schema. The note about scope is the part worth reading. The source is the authorization boundary, so `SELECT` on it permits attaching any reference that source ingests, including references with no existing table and columns some existing table omitted. An admin deciding whether to grant it needs that sentence. Split out from #38480 so that fix, which is an urgent security finding, is not gated on a second CODEOWNERS scope. It is accurate to merge this either before or after #38480: before, it documents a requirement that is about to exist; after, it closes a gap where the docs understate what is enforced. ### Verification Prose only. Rendering is unchanged in shape, four bullets where there were three.
tonydu-mz
commented
Aug 27, 2026
Thanks @SangJunBak ! Your instability point make a lot of sense. Authorization that depends on which other exports happen to exist means one team narrowing its own table silently breaks another team's unchanged dbt run, with an error naming the source rather than the table that actually changed. That's not a tradeoff worth the finer granularity. |
Require read privileges for CREATE TABLE ... FROM SOURCE
Fixes SQL-655 (2026 Refactor penetration test, finding H02, High).
Motivation
CREATE TABLE ... FROM SOURCEbypassed source authorization. The statementplans to a generic
Plan::CreateTable, whose RBAC arm required onlyCREATEon the destination schema and never inspected
table.data_source. Because theresulting table is owned by whoever creates it,
CREATEon any schema a rolecontrols was enough to read a source that role had been explicitly denied.
The assessment read 4,076 rows from an Auction load generator through an
attached table while direct reads of the same export stayed denied. The
behaviour has been reachable by default since v26.25.0 and was still present in
v26.35.0.
Description
The
Plan::CreateTablearm now inspectstable.data_sourceand, for anIngestionExport, requires the privileges reading that source requires:SELECTon the source plusUSAGEon its schema, viagenerate_read_privileges.This is the same shape as
CREATE SINK, which requires read on the relation itreads.
The source is the authorization boundary.
SELECTon a source permitsattaching any reference that source ingests, including references that have no
cataloged export yet, and including columns that some existing export omitted.
If a reference should not be reachable, it should not be in the source's
publication or topic set.
An earlier revision of this change tried to be finer-grained, authorizing an
attach when the role could already read an existing export of the same reference
whose columns covered the new table's. That is not implemented, deliberately.
It made the outcome depend on unrelated catalog state: narrowing or dropping one
team's table could revoke another team's ability to run an unchanged statement,
with an error naming the source rather than the table that actually changed. No
other part of the RBAC model works that way, and per-column granularity belongs
in real column-level grants rather than being inferred from which exports happen
to exist.
This means pentest remediation item 2 ("require
SELECTon the selectedcataloged subsource or source export when one exists") is intentionally not
implemented, since it is that same catalog-dependent rule. Item 1 is what closes
the finding, and it is implemented here.
User-visible effect
This tightens an existing privilege requirement, and
enable_create_table_from_sourceis on by default, so it applies to everyone onupgrade. A role that previously needed only
CREATEon its own schema plusUSAGEon the source's schema now also needsSELECTon the source.Deployments where a platform team owns sources and application teams attach
tables into their own schemas will need those
SELECTgrants added. Theuser-facing privileges include for
CREATE TABLEis updated in a follow-up PR,kept separate so this fix is not gated on a second CODEOWNERS scope.
Verification
New
test/sqllogictest/rbac_create_table_from_source.sltpins both halves of therequirement and the stability property: a direct read is denied; the attach is
denied without
SELECTon the source; the attach is denied for a role that hasSELECTon the source but noCREATEon the destination schema; it succeedswith both; a second role with the same grants can attach the same reference after
another team has already exported it; and a reference with no existing export is
authorized identically, so the rule does not vary with catalog shape.
Registered in
tests_without_viewsalongside the other RBAC suites, since--auto-index-selectsview-wrapping would change privilege semantics.Residual, not fixed here
Purification runs before authorization.
must_spawn_purificationgates it onCREATE_ITEM_USAGE, which covers secrets, connections and types but notsources, and the upstream connection is taken from the source's own
source_desc()rather than from the statement's resolved ids. An unauthorizedrole can therefore still cause an outbound connection to the source's upstream
using the source owner's credentials through a connection it holds no
USAGEon, and use purification errors as an upstream existence and column-name oracle.
This is shared with
CREATE SOURCEandCREATE SINKrather than introducedhere, and closing it needs a pre-purification privilege gate. Filed separately.