Skip to content

Commit 24727b0

Browse files
committed
Only check access control on underlying MV tables on refresh
1 parent feb4cfe commit 24727b0

File tree

3 files changed

+90
-18
lines changed

3 files changed

+90
-18
lines changed

core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ protected Scope visitRefreshMaterializedView(RefreshMaterializedView refreshMate
714714

715715
// analyze the query that creates the data
716716
Query query = parseView(view.getOriginalSql(), name, refreshMaterializedView);
717-
Scope queryScope = process(query, scope);
717+
Scope queryScope = analyzeView(query, name, view.getCatalog(), view.getSchema(), view.getRunAsIdentity(), view.getPath(), refreshMaterializedView.getTable(), false);
718718

719719
// verify the insert destination columns match the query
720720
TableHandle targetTableHandle = metadata.getTableHandle(session, targetTable)
@@ -2260,10 +2260,10 @@ protected Scope visitTable(Table table, Optional<Scope> scope)
22602260
checkStorageTableNotRedirected(storageTableName);
22612261
TableHandle tableHandle = metadata.getTableHandle(session, storageTableName)
22622262
.orElseThrow(() -> semanticException(INVALID_VIEW, table, "Storage table '%s' does not exist", storageTableName));
2263-
return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.of(tableHandle));
2263+
return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.of(tableHandle), true);
22642264
}
22652265
// This is a stale materialized view and should be expanded like a logical view
2266-
return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.empty());
2266+
return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.empty(), true);
22672267
}
22682268

22692269
// This could be a reference to a logical view or a table
@@ -2474,7 +2474,7 @@ private Scope createScopeForCommonTableExpression(Table table, Optional<Scope> s
24742474
return createAndAssignScope(table, scope, fields);
24752475
}
24762476

2477-
private Scope createScopeForMaterializedView(Table table, QualifiedObjectName name, Optional<Scope> scope, MaterializedViewDefinition view, Optional<TableHandle> storageTable)
2477+
private Scope createScopeForMaterializedView(Table table, QualifiedObjectName name, Optional<Scope> scope, MaterializedViewDefinition view, Optional<TableHandle> storageTable, boolean bypassViewAccessControl)
24782478
{
24792479
return createScopeForView(
24802480
table,
@@ -2487,7 +2487,8 @@ private Scope createScopeForMaterializedView(Table table, QualifiedObjectName na
24872487
view.getPath(),
24882488
view.getColumns(),
24892489
storageTable,
2490-
true);
2490+
true,
2491+
bypassViewAccessControl);
24912492
}
24922493

24932494
private Scope createScopeForView(Table table, QualifiedObjectName name, Optional<Scope> scope, ViewDefinition view)
@@ -2502,6 +2503,7 @@ private Scope createScopeForView(Table table, QualifiedObjectName name, Optional
25022503
view.getPath(),
25032504
view.getColumns(),
25042505
Optional.empty(),
2506+
false,
25052507
false);
25062508
}
25072509

@@ -2516,7 +2518,8 @@ private Scope createScopeForView(
25162518
List<CatalogSchemaName> path,
25172519
List<ViewColumn> columns,
25182520
Optional<TableHandle> storageTable,
2519-
boolean isMaterializedView)
2521+
boolean isMaterializedView,
2522+
boolean bypassViewAccessControl)
25202523
{
25212524
Statement statement = analysis.getStatement();
25222525
if (statement instanceof CreateView viewStatement) {
@@ -2542,7 +2545,8 @@ private Scope createScopeForView(
25422545
}
25432546

25442547
analysis.registerTableForView(table, name, isMaterializedView);
2545-
RelationType descriptor = analyzeView(query, name, catalog, schema, owner, path, table);
2548+
RelationType descriptor = analyzeView(query, name, catalog, schema, owner, path, table, bypassViewAccessControl)
2549+
.getRelationType().withAlias(name.objectName(), null);
25462550
analysis.unregisterTableForView();
25472551

25482552
checkViewStaleness(columns, descriptor.getVisibleFields(), name, table)
@@ -5041,43 +5045,46 @@ private void analyzeAggregations(
50415045
}
50425046
}
50435047

5044-
private RelationType analyzeView(
5048+
private Scope analyzeView(
50455049
Query query,
50465050
QualifiedObjectName name,
50475051
Optional<String> catalog,
50485052
Optional<String> schema,
50495053
Optional<Identity> owner,
50505054
List<CatalogSchemaName> path,
5051-
Table node)
5055+
Table node,
5056+
boolean bypassAccessControl)
50525057
{
50535058
try {
50545059
// run view as view owner if set; otherwise, run as session user
50555060
Identity identity;
5056-
AccessControl viewAccessControl;
5061+
AccessControl viewAccessControl = accessControl;
50575062
if (owner.isPresent()) {
50585063
identity = Identity.from(owner.get())
50595064
.withGroups(groupProvider.getGroups(owner.get().getUser()))
50605065
.build();
5061-
if (owner.get().getUser().equals(session.getIdentity().getUser())) {
5062-
// View owner does not need GRANT OPTION to grant access themselves
5063-
viewAccessControl = accessControl;
5066+
if (bypassAccessControl) {
5067+
viewAccessControl = new AllowAllAccessControl();
50645068
}
5065-
else {
5069+
// View owner does not need GRANT OPTION to grant access themselves
5070+
// All others do need GRANT OPTION
5071+
else if (!owner.get().getUser().equals(session.getIdentity().getUser())) {
50665072
viewAccessControl = new ViewAccessControl(accessControl);
50675073
}
50685074
}
50695075
else {
50705076
identity = session.getIdentity();
5071-
viewAccessControl = accessControl;
5077+
if (bypassAccessControl) {
5078+
viewAccessControl = new AllowAllAccessControl();
5079+
}
50725080
}
50735081

50745082
Session viewSession = session.createViewSession(catalog, schema, identity, path);
50755083

50765084
StatementAnalyzer analyzer = statementAnalyzerFactory
50775085
.withSpecializedAccessControl(viewAccessControl)
50785086
.createStatementAnalyzer(analysis, viewSession, warningCollector, CorrelationSupport.ALLOWED);
5079-
Scope queryScope = analyzer.analyze(query);
5080-
return queryScope.getRelationType().withAlias(name.objectName(), null);
5087+
return analyzer.analyze(query);
50815088
}
50825089
catch (RuntimeException e) {
50835090
throw semanticException(INVALID_VIEW, node, e, "Failed analyzing stored view '%s': %s", name, e.getMessage());

testing/trino-tests/src/test/java/io/trino/execution/TestEventListenerBasic.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ public void testReferencedTablesInRefreshMaterializedView()
630630
TableInfo table = tables.get(0);
631631
assertThat(table)
632632
.hasCatalogSchemaTable("tpch", "tiny", "nation")
633-
.hasAuthorization("user")
633+
.hasAuthorization("alice")
634634
.isDirectlyReferenced()
635635
.hasColumnsWithoutMasking("nationkey")
636636
.hasNoRowFilters()

testing/trino-tests/src/test/java/io/trino/security/TestAccessControl.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import io.trino.spi.security.SystemSecurityContext;
5959
import io.trino.spi.security.TrinoPrincipal;
6060
import io.trino.spi.security.ViewExpression;
61+
import io.trino.spi.type.VarcharType;
6162
import io.trino.sql.SqlPath;
6263
import io.trino.testing.AbstractTestQueryFramework;
6364
import io.trino.testing.DistributedQueryRunner;
@@ -210,6 +211,19 @@ protected SystemAccessControl delegate()
210211
@Override
211212
public Map<SchemaTableName, ConnectorMaterializedViewDefinition> apply(ConnectorSession session, SchemaTablePrefix schemaTablePrefix)
212213
{
214+
ConnectorMaterializedViewDefinition selectFromUnderlyingTableDefinition = new ConnectorMaterializedViewDefinition(
215+
"SELECT * FROM tpch.sf1.region",
216+
Optional.of(new CatalogSchemaTableName("tpch", "sf1", "region")),
217+
Optional.empty(),
218+
Optional.empty(),
219+
ImmutableList.of(
220+
new ConnectorMaterializedViewDefinition.Column("regionkey", BIGINT.getTypeId(), Optional.empty()),
221+
new ConnectorMaterializedViewDefinition.Column("name", VarcharType.createVarcharType(25).getTypeId(), Optional.empty()),
222+
new ConnectorMaterializedViewDefinition.Column("comment", VarcharType.createVarcharType(152).getTypeId(), Optional.empty())),
223+
Optional.of(Duration.ZERO),
224+
Optional.of("comment"),
225+
Optional.of("test_mv_access_owner"),
226+
ImmutableList.of());
213227
ConnectorMaterializedViewDefinition materializedViewDefinition = new ConnectorMaterializedViewDefinition(
214228
"SELECT 1 AS test",
215229
Optional.empty(),
@@ -221,6 +235,7 @@ public Map<SchemaTableName, ConnectorMaterializedViewDefinition> apply(Connector
221235
Optional.of("owner"),
222236
ImmutableList.of());
223237
return ImmutableMap.of(
238+
new SchemaTableName("default", "test_materialized_view_select_from_region"), selectFromUnderlyingTableDefinition,
224239
new SchemaTableName("default", "test_materialized_view"), materializedViewDefinition);
225240
}
226241
})
@@ -860,6 +875,56 @@ public void testCommentOnRedirectedTable()
860875
assertQueryReturnsEmptyResult(query);
861876
}
862877

878+
@Test
879+
public void testMaterializedViewAccessControl()
880+
{
881+
reset();
882+
883+
Session viewOwnerSession = TestingSession.testSessionBuilder()
884+
.setIdentity(Identity.ofUser("test_mv_access_owner"))
885+
.setCatalog(getSession().getCatalog())
886+
.setSchema(getSession().getSchema())
887+
.build();
888+
889+
// Verify MV owner can access
890+
assertAccessAllowed(
891+
viewOwnerSession,
892+
"SELECT * FROM mock.default.test_materialized_view_select_from_region");
893+
894+
// Verify it's accessible to non-owner session as well if nothing's denied
895+
assertAccessAllowed(
896+
"SELECT * FROM mock.default.test_materialized_view_select_from_region");
897+
898+
// Verify non-owner cannot access if it doesn't have SELECT
899+
assertAccessDenied("SELECT * FROM mock.default.test_materialized_view_select_from_region",
900+
"Cannot select from columns .* in table or view mock.default.test_materialized_view_select_from_region",
901+
privilege(getSession().getUser(), "test_materialized_view_select_from_region", SELECT_COLUMN));
902+
903+
// Verify an MV is treated as a table, not a view, for the purposes of selecting from it
904+
assertAccessAllowed(
905+
viewOwnerSession,
906+
"SELECT * FROM mock.default.test_materialized_view_select_from_region",
907+
privilege(viewOwnerSession.getUser(), "region", CREATE_VIEW_WITH_SELECT_COLUMNS));
908+
assertAccessAllowed(
909+
"SELECT * FROM mock.default.test_materialized_view_select_from_region",
910+
privilege(viewOwnerSession.getUser(), "region", CREATE_VIEW_WITH_SELECT_COLUMNS));
911+
assertAccessAllowed(
912+
viewOwnerSession,
913+
"SELECT * FROM mock.default.test_materialized_view_select_from_region",
914+
privilege(viewOwnerSession.getUser(), "region", SELECT_COLUMN));
915+
assertAccessAllowed(
916+
"SELECT * FROM mock.default.test_materialized_view_select_from_region",
917+
privilege(viewOwnerSession.getUser(), "region", SELECT_COLUMN));
918+
919+
// Verify only the owner can refresh the MV and the permissions on the underlying table are checked
920+
assertAccessAllowed(
921+
viewOwnerSession,
922+
"REFRESH MATERIALIZED VIEW mock.default.test_materialized_view_select_from_region");
923+
assertAccessDenied("REFRESH MATERIALIZED VIEW mock.default.test_materialized_view_select_from_region",
924+
"View owner does not have sufficient privileges: View owner 'test_mv_access_owner' cannot create view that selects from tpch.sf1.region",
925+
privilege(viewOwnerSession.getUser(), "region", CREATE_VIEW_WITH_SELECT_COLUMNS));
926+
}
927+
863928
@Test
864929
public void testViewWithTableFunction()
865930
{

0 commit comments

Comments
 (0)