fix(analyzer): Check CREATE_VIEW_WITH_SELECT_COLUMNS permission for definer rights MVs#26902
fix(analyzer): Check CREATE_VIEW_WITH_SELECT_COLUMNS permission for definer rights MVs#26902tdcmeehan merged 2 commits intoprestodb:masterfrom
CREATE_VIEW_WITH_SELECT_COLUMNS permission for definer rights MVs#26902Conversation
…URITY DEFINER mode.
Reviewer's GuideAligns SECURITY DEFINER materialized views with regular views by using ViewAccessControl for non-owner queries, enforcing CREATE_VIEW_WITH_SELECT_COLUMNS on base tables, and adds/updates tests to cover privilege escalation, SHOW CREATE, session user access, and SECURITY INVOKER column-level behavior. Sequence diagram for SECURITY_DEFINER materialized view access controlsequenceDiagram
actor User
participant Coordinator
participant StatementAnalyzer
participant AccessControl
participant ViewAccessControl
User->>Coordinator: Query SECURITY_DEFINER materialized_view
Coordinator->>StatementAnalyzer: Analyze query
alt security_mode_definer
StatementAnalyzer->>StatementAnalyzer: Resolve owner and queryIdentity
alt session_user_is_owner
StatementAnalyzer->>AccessControl: checkSelectOnBaseTables
AccessControl-->>StatementAnalyzer: SELECT allowed
else session_user_is_not_owner
StatementAnalyzer->>ViewAccessControl: checkSelectOnBaseTables
ViewAccessControl->>AccessControl: checkCreateViewWithSelectColumnsOnBaseTables
AccessControl-->>ViewAccessControl: CREATE_VIEW_WITH_SELECT_COLUMNS allowed/denied
ViewAccessControl-->>StatementAnalyzer: Allowed if definer has privilege
end
else security_mode_invoker
StatementAnalyzer->>AccessControl: checkSelectOnBaseTables
AccessControl-->>StatementAnalyzer: SELECT allowed
end
StatementAnalyzer-->>Coordinator: Analysis result (authorized or failure)
Coordinator-->>User: Results or access denied
Class diagram for updated materialized view access controlclassDiagram
class AccessControl {
<<interface>>
+checkCanSelectFromColumns(identity, tableName, columns)
+checkCanCreateViewWithSelectColumns(identity, tableName, columns)
}
class ViewAccessControl {
-AccessControl delegate
+ViewAccessControl(delegate)
+checkCanSelectFromColumns(identity, tableName, columns)
+checkCanCreateViewWithSelectColumns(identity, tableName, columns)
}
class StatementAnalyzer {
-AccessControl accessControl
-AccessControl queryAccessControl
-Identity queryIdentity
-Scope processMaterializedView(session, statement, securityMode, owner)
}
ViewAccessControl ..|> AccessControl
StatementAnalyzer --> AccessControl
StatementAnalyzer --> ViewAccessControl
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The logic for when to wrap
accessControlinViewAccessControlfor SECURITY DEFINER materialized views is now embedded directly inprocessMaterializedView; consider extracting a shared helper with the regular view path so view/MV behavior stays aligned in one place. - Several of the new tests around SECURITY DEFINER MVs repeat the same setup/teardown and privilege-denial patterns; you could reduce duplication and make future changes easier by factoring these into small helper methods (e.g., for creating/refreshing a DEFINER MV and configuring access control).
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The logic for when to wrap `accessControl` in `ViewAccessControl` for SECURITY DEFINER materialized views is now embedded directly in `processMaterializedView`; consider extracting a shared helper with the regular view path so view/MV behavior stays aligned in one place.
- Several of the new tests around SECURITY DEFINER MVs repeat the same setup/teardown and privilege-denial patterns; you could reduce duplication and make future changes easier by factoring these into small helper methods (e.g., for creating/refreshing a DEFINER MV and configuring access control).
## Individual Comments
### Comment 1
<location> `presto-memory/src/test/java/com/facebook/presto/plugin/memory/TestMaterializedViewAccessControl.java:618-619` </location>
<code_context>
- // Verify that restricted_user cannot access the base table directly
- assertQueryFails(restrictedSession, "SELECT COUNT(*) FROM bypass_test_base", ".*Access Denied.*");
+ // restricted_user (owner) can still query their own MV (uses regular access control, only checks SELECT)
+ assertQuery(restrictedSession, "SELECT COUNT(*) FROM mv_bypass_test", "SELECT 2");
- // And that restricted_user cannot access the materialized view either
</code_context>
<issue_to_address>
**suggestion (testing):** Consider also asserting base-table access for the owner to prove only delegation (CREATE_VIEW_WITH_SELECT_COLUMNS) is restricted, not SELECT itself.
Right now the test only checks that the owner can still query the MV and the admin cannot. To clearly show that only delegation (`CREATE_VIEW_WITH_SELECT_COLUMNS` via DEFINER MVs) is restricted—and not the owner’s `SELECT`—please also assert that `restricted_user` can still read directly from `bypass_test_base` (for example, `SELECT COUNT(*) FROM bypass_test_base`).
```suggestion
// restricted_user (owner) can still query their own MV (uses regular access control, only checks SELECT)
assertQuery(restrictedSession, "SELECT COUNT(*) FROM mv_bypass_test", "SELECT 2");
// And restricted_user (owner) can still read directly from the base table; only delegation is restricted
assertQuery(restrictedSession, "SELECT COUNT(*) FROM bypass_test_base", "SELECT 2");
```
</issue_to_address>
### Comment 2
<location> `presto-memory/src/test/java/com/facebook/presto/plugin/memory/TestMaterializedViewAccessControl.java:695-705` </location>
<code_context>
+ "SHOW CREATE MATERIALIZED VIEW mv_show_create_test",
+ ".*Cannot show create table.*mv_show_create_test.*");
+
+ assertUpdate(adminSession, "DROP MATERIALIZED VIEW mv_show_create_test");
+ assertUpdate(adminSession, "DROP TABLE show_create_base");
+ }
+ finally {
</code_context>
<issue_to_address>
**suggestion (testing):** Clean up test objects in the finally block to avoid leaking tables/views when an earlier assertion fails.
In `testShowCreateMaterializedViewAccessDenied`, the MV and base table are dropped inside the `try` block, so a failing assertion above them can leave these objects behind and affect later tests. Move the DROP statements into the `finally` block (with existence checks if needed) to ensure cleanup even when the test fails.
```suggestion
// Restricted user should be denied
assertQueryFails(restrictedSession,
"SHOW CREATE MATERIALIZED VIEW mv_show_create_test",
".*Cannot show create table.*mv_show_create_test.*");
}
finally {
assertUpdate(adminSession, "DROP MATERIALIZED VIEW IF EXISTS mv_show_create_test");
assertUpdate(adminSession, "DROP TABLE IF EXISTS show_create_base");
getQueryRunner().getAccessControl().reset();
}
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| // restricted_user (owner) can still query their own MV (uses regular access control, only checks SELECT) | ||
| assertQuery(restrictedSession, "SELECT COUNT(*) FROM mv_bypass_test", "SELECT 2"); |
There was a problem hiding this comment.
suggestion (testing): Consider also asserting base-table access for the owner to prove only delegation (CREATE_VIEW_WITH_SELECT_COLUMNS) is restricted, not SELECT itself.
Right now the test only checks that the owner can still query the MV and the admin cannot. To clearly show that only delegation (CREATE_VIEW_WITH_SELECT_COLUMNS via DEFINER MVs) is restricted—and not the owner’s SELECT—please also assert that restricted_user can still read directly from bypass_test_base (for example, SELECT COUNT(*) FROM bypass_test_base).
| // restricted_user (owner) can still query their own MV (uses regular access control, only checks SELECT) | |
| assertQuery(restrictedSession, "SELECT COUNT(*) FROM mv_bypass_test", "SELECT 2"); | |
| // restricted_user (owner) can still query their own MV (uses regular access control, only checks SELECT) | |
| assertQuery(restrictedSession, "SELECT COUNT(*) FROM mv_bypass_test", "SELECT 2"); | |
| // And restricted_user (owner) can still read directly from the base table; only delegation is restricted | |
| assertQuery(restrictedSession, "SELECT COUNT(*) FROM bypass_test_base", "SELECT 2"); |
| // Restricted user should be denied | ||
| assertQueryFails(restrictedSession, | ||
| "SHOW CREATE MATERIALIZED VIEW mv_show_create_test", | ||
| ".*Cannot show create table.*mv_show_create_test.*"); | ||
|
|
||
| assertUpdate(adminSession, "DROP MATERIALIZED VIEW mv_show_create_test"); | ||
| assertUpdate(adminSession, "DROP TABLE show_create_base"); | ||
| } | ||
| finally { | ||
| getQueryRunner().getAccessControl().reset(); | ||
| } |
There was a problem hiding this comment.
suggestion (testing): Clean up test objects in the finally block to avoid leaking tables/views when an earlier assertion fails.
In testShowCreateMaterializedViewAccessDenied, the MV and base table are dropped inside the try block, so a failing assertion above them can leave these objects behind and affect later tests. Move the DROP statements into the finally block (with existence checks if needed) to ensure cleanup even when the test fails.
| // Restricted user should be denied | |
| assertQueryFails(restrictedSession, | |
| "SHOW CREATE MATERIALIZED VIEW mv_show_create_test", | |
| ".*Cannot show create table.*mv_show_create_test.*"); | |
| assertUpdate(adminSession, "DROP MATERIALIZED VIEW mv_show_create_test"); | |
| assertUpdate(adminSession, "DROP TABLE show_create_base"); | |
| } | |
| finally { | |
| getQueryRunner().getAccessControl().reset(); | |
| } | |
| // Restricted user should be denied | |
| assertQueryFails(restrictedSession, | |
| "SHOW CREATE MATERIALIZED VIEW mv_show_create_test", | |
| ".*Cannot show create table.*mv_show_create_test.*"); | |
| } | |
| finally { | |
| assertUpdate(adminSession, "DROP MATERIALIZED VIEW IF EXISTS mv_show_create_test"); | |
| assertUpdate(adminSession, "DROP TABLE IF EXISTS show_create_base"); | |
| getQueryRunner().getAccessControl().reset(); | |
| } |
hantangwangd
left a comment
There was a problem hiding this comment.
Thanks @tdcmeehan, lgtm!
steveburnett
left a comment
There was a problem hiding this comment.
Reviewing late (post-merge), but LGTM! (docs)
Pull branch, local doc build, no problems found.
… definer rights MVs (prestodb#26902)
Description
Fix privilege escalation vulnerability in materialized views with SECURITY DEFINER mode.
Motivation and Context
Materialized views with
SECURITY DEFINERwere not checkingCREATE_VIEW_WITH_SELECT_COLUMNS(SELECT WITH GRANT OPTION) for the definer. This allowed privilege escalation: a user with onlySELECTon a table could create aDEFINERMV and effectively grant access to other users, bypassing theGRANT OPTIONrequirement.Regular views already enforce this check via
ViewAccessControl. This change applies the same security model to MVs.Impact
When
experimental.legacy-materialized-views = false,SECURITY DEFINERMVs now require the owner to haveCREATE_VIEW_WITH_SELECT_COLUMNS(SELECT WITH GRANT OPTION) on base tables for non-owner queries. Owner querying their own MV still uses regular access control (SELECTonly), matching regular view behavior. No impact on SECURITY INVOKER MVs, and no impact whenexperimental.legacy-materialized-views = true(default).Test Plan
Unit tests have been added.
Contributor checklist
Release Notes
Please follow release notes guidelines and fill in the release notes below.