sql: add security barrier views POC - #38562
Draft
jubrad wants to merge 1 commit into
Draft
Conversation
A view is currently not a boundary the optimizer respects. A predicate
supplied by a reader crosses into the view's own plan, propagates to the
base collection, and is scheduled ahead of the view's own filter, because
`MapFilterProject` orders predicates by the column position they first
reference. Since error messages embed the offending value, a reader with
`SELECT` on only the view can aim a fallible expression at rows the view
excludes and read the hidden values back out of the error.
Adopt PostgreSQL's model: mark the view, then decline to dissolve its
boundary.
CREATE VIEW my_orders WITH (SECURITY BARRIER) AS
SELECT * FROM orders WHERE tenant = current_user;
Two gates implement it. `inline_views` skips a barrier, so it stays a
distinct object referenced through a global `Get`, which every per-object
transform already treats as opaque, and no `Let` binding is formed for
`push_into_let_binding` to push into. `optimize_dataflow_filters_inner`
then applies only leakproof predicates to a barrier; a blocked predicate
never enters the view's plan, so it also never propagates onward to the
view's own inputs.
Leakproof is `!could_error()`. Materialize has no user-defined functions,
so every function in a predicate is a builtin whose only value-dependent
channel is its error, and `could_error` is already fail-safe: `true` by
default for a `LazyUnaryFunc`, otherwise derived from whether the Rust
signature returns a `Result`. This is a stronger footing than
`pg_proc.proleakproof`, which is a superuser assertion. It also keeps the
cost low, since `=`, `<`, `>`, and `AND` are all infallible and still
cross the barrier to reach persist pruning and index lookups.
The barrier set is optimizer-only state, so it lives on `TransformCtx`
rather than on `DataflowDescription`, which is part of the compute
protocol. `TransformCtx::global` takes it as a required argument so that
a caller which forgets it fails to compile rather than silently losing
the barrier.
Tests: `src/transform/tests/test_security_barrier.rs` asserts the blocked,
admitted, and unprotected cases at the `optimize_dataflow_filters_inner`
seam, including one test that pins the current exposure so a regression is
visible. `test/sqllogictest/security_barrier.slt` covers the SQL surface
and the resulting `EXPLAIN` plans.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Design doc: #38561. Alternative mechanism, same SQL surface: #38566.
Design doc: #38561. That PR carries the full problem statement, the
alternatives, and the open questions. This PR is the prototype it refers to.
In short: a view is not a boundary the optimizer respects. A reader's
predicate crosses into the view's own plan, propagates to the base collection,
and is scheduled ahead of the view's own filter. Because our error messages
embed the offending value, a reader holding
SELECTon only the view can aima fallible expression at rows the view excludes and read the hidden values out
of the resulting error.
Description
Adds an opt-in view option:
Most of the diff is option plumbing (lexer, AST, parser, planner, catalog).
The optimizer change proper is two gates and one predicate in
src/transform/src/dataflow.rs:inline_viewsskips a barrier view, so it stays a distinctobjects_to_buildentry referenced through a globalGet. Every per-objecttransform already treats that as opaque, and no
Letbinding is formed forPredicatePushdown::push_into_let_bindingto push into.optimize_dataflow_filters_innerapplies only leakproof predicates to abarrier. A blocked predicate never enters the view's plan, so it also never
propagates onward to the view's own inputs, which is what keeps it away from
persist filter pushdown.
No other cross-object pass needs a gate.
optimize_dataflow_demandprunescolumns rather than rows, which is safe and which PostgreSQL also permits.
Three decisions a reviewer may want to push on:
!could_error(). We have no user-defined functions, soevery function in a predicate is a builtin whose only value-dependent channel
is its error, and
could_erroris already fail-safe:trueby default for aLazyUnaryFunc, otherwise derived from whether the Rust signature returns aResult. This does promotecould_errorfrom an optimizer hint to a securityboundary, so a wrong
could_error = falsebecomes a vulnerability rather thana performance bug. Flagged as an open question on the design PR.
TransformCtx, notDataflowDescription. It isoptimizer-only state, and
DataflowDescriptionis part of the computeprotocol.
TransformCtx::globaltakes it as a required argument rather than abuilder method, so a caller that forgets it fails to compile instead of
silently losing the barrier. That is why the diff touches seven call sites.
Behavior is unchanged for views without the option: both gates are no-ops on an
empty barrier set.
Not included, and called out in the design doc: an
mz_viewscolumn reportingthe flag,
ALTER VIEW ... SET (SECURITY BARRIER), and user-facing docs. Thisis a prototype to validate the design, not a shippable feature.
Verification
src/transform/tests/test_security_barrier.rsasserts three behaviors at theoptimize_dataflow_filters_innerseam: a fallible predicate is blocked by abarrier, a leakproof one still crosses, and the unprotected case still leaks.
That last test pins the current exposure so a regression in the gate is visible
rather than silent.
test/sqllogictest/security_barrier.sltcovers the SQL surface, theSHOW CREATE VIEWround trip, and the resultingEXPLAINplans. Unprotected:With the barrier, the division stays above the view and only the tenant filter
reaches the base collection:
The existing
EXPLAINand privilege suites are unchanged, confirming thedefault path is untouched.
Note: code comments reference the design doc path, so #38561 should land first.
🤖 Generated with Claude Code