diff --git a/presto-hive/src/main/java/io/prestosql/plugin/hive/security/LegacyAccessControl.java b/presto-hive/src/main/java/io/prestosql/plugin/hive/security/LegacyAccessControl.java index b0105d277f61..732509832388 100644 --- a/presto-hive/src/main/java/io/prestosql/plugin/hive/security/LegacyAccessControl.java +++ b/presto-hive/src/main/java/io/prestosql/plugin/hive/security/LegacyAccessControl.java @@ -23,6 +23,7 @@ import io.prestosql.spi.connector.SchemaTableName; import io.prestosql.spi.security.PrestoPrincipal; import io.prestosql.spi.security.Privilege; +import io.prestosql.spi.security.ViewExpression; import javax.inject.Inject; @@ -267,4 +268,10 @@ public void checkCanShowCurrentRoles(ConnectorSecurityContext context, String ca public void checkCanShowRoleGrants(ConnectorSecurityContext context, String catalogName) { } + + @Override + public Optional getRowFilter(ConnectorSecurityContext context, SchemaTableName tableName) + { + return Optional.empty(); + } } diff --git a/presto-hive/src/main/java/io/prestosql/plugin/hive/security/SqlStandardAccessControl.java b/presto-hive/src/main/java/io/prestosql/plugin/hive/security/SqlStandardAccessControl.java index 0c130f53f5f5..49842675cf5e 100644 --- a/presto-hive/src/main/java/io/prestosql/plugin/hive/security/SqlStandardAccessControl.java +++ b/presto-hive/src/main/java/io/prestosql/plugin/hive/security/SqlStandardAccessControl.java @@ -30,6 +30,7 @@ import io.prestosql.spi.security.PrestoPrincipal; import io.prestosql.spi.security.Privilege; import io.prestosql.spi.security.RoleGrant; +import io.prestosql.spi.security.ViewExpression; import javax.inject.Inject; @@ -389,6 +390,12 @@ public void checkCanShowRoleGrants(ConnectorSecurityContext context, String cata { } + @Override + public Optional getRowFilter(ConnectorSecurityContext context, SchemaTableName tableName) + { + return Optional.empty(); + } + private boolean isAdmin(ConnectorSecurityContext context) { SemiTransactionalHiveMetastore metastore = metastoreProvider.apply(((HiveTransactionHandle) context.getTransactionHandle())); diff --git a/presto-main/src/main/java/io/prestosql/security/AccessControl.java b/presto-main/src/main/java/io/prestosql/security/AccessControl.java index 0eeb15cc6ba8..3de24e23e948 100644 --- a/presto-main/src/main/java/io/prestosql/security/AccessControl.java +++ b/presto-main/src/main/java/io/prestosql/security/AccessControl.java @@ -13,6 +13,7 @@ */ package io.prestosql.security; +import com.google.common.collect.ImmutableList; import io.prestosql.metadata.QualifiedObjectName; import io.prestosql.spi.connector.CatalogSchemaName; import io.prestosql.spi.connector.CatalogSchemaTableName; @@ -22,6 +23,7 @@ import io.prestosql.spi.security.Identity; import io.prestosql.spi.security.PrestoPrincipal; import io.prestosql.spi.security.Privilege; +import io.prestosql.spi.security.ViewExpression; import java.security.Principal; import java.util.List; @@ -322,4 +324,9 @@ public interface AccessControl * @throws io.prestosql.spi.security.AccessDeniedException if not allowed */ void checkCanShowRoleGrants(SecurityContext context, String catalogName); + + default List getRowFilters(SecurityContext context, QualifiedObjectName tableName) + { + return ImmutableList.of(); + } } diff --git a/presto-main/src/main/java/io/prestosql/security/AccessControlManager.java b/presto-main/src/main/java/io/prestosql/security/AccessControlManager.java index 8afa59a4b2d8..20ca26eb375a 100644 --- a/presto-main/src/main/java/io/prestosql/security/AccessControlManager.java +++ b/presto-main/src/main/java/io/prestosql/security/AccessControlManager.java @@ -39,6 +39,7 @@ import io.prestosql.spi.security.SystemAccessControl; import io.prestosql.spi.security.SystemAccessControlFactory; import io.prestosql.spi.security.SystemSecurityContext; +import io.prestosql.spi.security.ViewExpression; import io.prestosql.transaction.TransactionId; import io.prestosql.transaction.TransactionManager; import org.weakref.jmx.Managed; @@ -721,6 +722,28 @@ public void checkCanShowRoleGrants(SecurityContext securityContext, String catal catalogAuthorizationCheck(catalogName, securityContext, (control, context) -> control.checkCanShowRoleGrants(context, catalogName)); } + @Override + public List getRowFilters(SecurityContext context, QualifiedObjectName tableName) + { + requireNonNull(context, "securityContext is null"); + requireNonNull(tableName, "catalogName is null"); + + ImmutableList.Builder filters = ImmutableList.builder(); + CatalogAccessControlEntry entry = getConnectorAccessControl(context.getTransactionId(), tableName.getCatalogName()); + + if (entry != null) { + entry.getAccessControl().getRowFilter(entry.toConnectorSecurityContext(context), tableName.asSchemaTableName()) + .ifPresent(filters::add); + } + + for (SystemAccessControl systemAccessControl : systemAccessControls.get()) { + systemAccessControl.getRowFilter(context.toSystemSecurityContext(), tableName.asCatalogSchemaTableName()) + .ifPresent(filters::add); + } + + return filters.build(); + } + private CatalogAccessControlEntry getConnectorAccessControl(TransactionId transactionId, String catalogName) { return transactionManager.getOptionalCatalogMetadata(transactionId, catalogName) diff --git a/presto-main/src/main/java/io/prestosql/security/ForwardingAccessControl.java b/presto-main/src/main/java/io/prestosql/security/ForwardingAccessControl.java index 47441b11f595..0ac8fc3e26b5 100644 --- a/presto-main/src/main/java/io/prestosql/security/ForwardingAccessControl.java +++ b/presto-main/src/main/java/io/prestosql/security/ForwardingAccessControl.java @@ -21,6 +21,7 @@ import io.prestosql.spi.security.Identity; import io.prestosql.spi.security.PrestoPrincipal; import io.prestosql.spi.security.Privilege; +import io.prestosql.spi.security.ViewExpression; import java.security.Principal; import java.util.List; @@ -300,4 +301,10 @@ public void checkCanShowRoleGrants(SecurityContext context, String catalogName) { delegate().checkCanShowRoleGrants(context, catalogName); } + + @Override + public List getRowFilters(SecurityContext context, QualifiedObjectName tableName) + { + return delegate().getRowFilters(context, tableName); + } } diff --git a/presto-main/src/main/java/io/prestosql/sql/analyzer/Analysis.java b/presto-main/src/main/java/io/prestosql/sql/analyzer/Analysis.java index df498b85f031..7e24fa9430ec 100644 --- a/presto-main/src/main/java/io/prestosql/sql/analyzer/Analysis.java +++ b/presto-main/src/main/java/io/prestosql/sql/analyzer/Analysis.java @@ -14,10 +14,12 @@ package io.prestosql.sql.analyzer; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimap; +import com.google.common.collect.Multiset; import io.prestosql.metadata.NewTableLayout; import io.prestosql.metadata.QualifiedObjectName; import io.prestosql.metadata.ResolvedFunction; @@ -56,6 +58,7 @@ import javax.annotation.concurrent.Immutable; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.HashSet; @@ -133,6 +136,9 @@ public class Analysis private final Map, List> groupingOperations = new LinkedHashMap<>(); + private final Multiset rowFilterScopes = HashMultiset.create(); + private final Map, List> rowFilters = new LinkedHashMap<>(); + private Optional create = Optional.empty(); private Optional insert = Optional.empty(); private Optional analyzeTarget = Optional.empty(); @@ -663,6 +669,32 @@ public boolean isOrderByRedundant(OrderBy orderBy) return redundantOrderBy.contains(NodeRef.of(orderBy)); } + public boolean hasRowFilter(QualifiedObjectName table, String identity) + { + return rowFilterScopes.contains(new RowFilterScopeEntry(table, identity)); + } + + public void registerTableForRowFiltering(QualifiedObjectName table, String identity) + { + rowFilterScopes.add(new RowFilterScopeEntry(table, identity)); + } + + public void unregisterTableForRowFiltering(QualifiedObjectName table, String identity) + { + rowFilterScopes.remove(new RowFilterScopeEntry(table, identity)); + } + + public void addRowFilter(Table table, Expression filter) + { + rowFilters.computeIfAbsent(NodeRef.of(table), node -> new ArrayList<>()) + .add(filter); + } + + public List getRowFilters(Table node) + { + return rowFilters.getOrDefault(NodeRef.of(node), ImmutableList.of()); + } + @Immutable public static final class SelectExpression { @@ -895,4 +927,36 @@ public String toString() return format("AccessControl: %s, Identity: %s", accessControl.getClass(), identity); } } + + private static class RowFilterScopeEntry + { + private final QualifiedObjectName table; + private final String identity; + + public RowFilterScopeEntry(QualifiedObjectName table, String identity) + { + this.table = requireNonNull(table, "table is null"); + this.identity = requireNonNull(identity, "identity is null"); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RowFilterScopeEntry that = (RowFilterScopeEntry) o; + return table.equals(that.table) && + identity.equals(that.identity); + } + + @Override + public int hashCode() + { + return Objects.hash(table, identity); + } + } } diff --git a/presto-main/src/main/java/io/prestosql/sql/analyzer/StatementAnalyzer.java b/presto-main/src/main/java/io/prestosql/sql/analyzer/StatementAnalyzer.java index dfa57e962440..797cde0ab314 100644 --- a/presto-main/src/main/java/io/prestosql/sql/analyzer/StatementAnalyzer.java +++ b/presto-main/src/main/java/io/prestosql/sql/analyzer/StatementAnalyzer.java @@ -46,6 +46,7 @@ import io.prestosql.spi.function.OperatorType; import io.prestosql.spi.security.AccessDeniedException; import io.prestosql.spi.security.Identity; +import io.prestosql.spi.security.ViewExpression; import io.prestosql.spi.type.ArrayType; import io.prestosql.spi.type.CharType; import io.prestosql.spi.type.MapType; @@ -53,6 +54,7 @@ import io.prestosql.spi.type.Type; import io.prestosql.spi.type.TypeNotFoundException; import io.prestosql.spi.type.VarcharType; +import io.prestosql.sql.SqlPath; import io.prestosql.sql.analyzer.Analysis.SelectExpression; import io.prestosql.sql.analyzer.Scope.AsteriskedIdentifierChainBasis; import io.prestosql.sql.parser.ParsingException; @@ -184,6 +186,7 @@ import static io.prestosql.spi.StandardErrorCode.FUNCTION_NOT_WINDOW; import static io.prestosql.spi.StandardErrorCode.INVALID_COLUMN_REFERENCE; import static io.prestosql.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static io.prestosql.spi.StandardErrorCode.INVALID_ROW_FILTER; import static io.prestosql.spi.StandardErrorCode.INVALID_VIEW; import static io.prestosql.spi.StandardErrorCode.INVALID_WINDOW_FRAME; import static io.prestosql.spi.StandardErrorCode.MISMATCHED_COLUMN_ALIASES; @@ -216,6 +219,7 @@ import static io.prestosql.sql.analyzer.ExpressionTreeUtils.asQualifiedName; import static io.prestosql.sql.analyzer.ExpressionTreeUtils.extractAggregateFunctions; import static io.prestosql.sql.analyzer.ExpressionTreeUtils.extractExpressions; +import static io.prestosql.sql.analyzer.ExpressionTreeUtils.extractLocation; import static io.prestosql.sql.analyzer.ExpressionTreeUtils.extractWindowFunctions; import static io.prestosql.sql.analyzer.Scope.BasisType.TABLE; import static io.prestosql.sql.analyzer.ScopeReferenceExtractor.getReferencesToScope; @@ -479,6 +483,10 @@ protected Scope visitDelete(Delete node, Optional scope) accessControl.checkCanDeleteFromTable(session.toSecurityContext(), tableName); + if (!accessControl.getRowFilters(session.toSecurityContext(), tableName).isEmpty()) { + throw semanticException(NOT_SUPPORTED, node, "Delete from table with row filter"); + } + return createAndAssignScope(node, scope, Field.newUnqualified("rows", BIGINT)); } @@ -991,9 +999,18 @@ protected Scope visitTable(Table table, Optional scope) analysis.setColumn(field, columnHandle); } + List outputFields = fields.build(); + + Scope accessControlScope = Scope.builder() + .withRelationType(RelationId.anonymous(), new RelationType(outputFields)) + .build(); + + accessControl.getRowFilters(session.toSecurityContext(), name) + .forEach(filter -> analyzeRowFilter(session.getIdentity().getUser(), table, name, accessControlScope, filter)); + analysis.registerTable(table, tableHandle.get()); - return createAndAssignScope(table, scope, fields.build()); + return createAndAssignScope(table, scope, outputFields); } private Scope createScopeForCommonTableExpression(Table table, Optional scope, WithQuery withQuery) @@ -1083,6 +1100,11 @@ private Scope createScopeForView(Table table, QualifiedObjectName name, Optional analysis.addRelationCoercion(table, outputFields.stream().map(Field::getType).toArray(Type[]::new)); + Scope accessControlScope = createAndAssignScope(table, Optional.empty(), outputFields); + + accessControl.getRowFilters(session.toSecurityContext(), name) + .forEach(filter -> analyzeRowFilter(session.getIdentity().getUser(), table, name, accessControlScope, filter)); + return createAndAssignScope(table, scope, outputFields); } @@ -2279,21 +2301,7 @@ private RelationType analyzeView(Query query, QualifiedObjectName name, Optional } // TODO: record path in view definition (?) (check spec) and feed it into the session object we use to evaluate the query defined by the view - Session viewSession = Session.builder(metadata.getSessionPropertyManager()) - .setQueryId(session.getQueryId()) - .setTransactionId(session.getTransactionId().orElse(null)) - .setIdentity(identity) - .setSource(session.getSource().orElse(null)) - .setCatalog(catalog.orElse(null)) - .setSchema(schema.orElse(null)) - .setPath(session.getPath()) - .setTimeZoneKey(session.getTimeZoneKey()) - .setLocale(session.getLocale()) - .setRemoteUserAddress(session.getRemoteUserAddress().orElse(null)) - .setUserAgent(session.getUserAgent().orElse(null)) - .setClientInfo(session.getClientInfo().orElse(null)) - .setStartTime(session.getStartTime()) - .build(); + Session viewSession = createViewSession(catalog, schema, identity, session.getPath()); StatementAnalyzer analyzer = new StatementAnalyzer(analysis, metadata, sqlParser, viewAccessControl, viewSession, warningCollector); Scope queryScope = analyzer.analyze(query, Scope.create()); @@ -2357,6 +2365,58 @@ private ExpressionAnalysis analyzeExpression(Expression expression, Scope scope) warningCollector); } + private void analyzeRowFilter(String currentIdentity, Table table, QualifiedObjectName name, Scope scope, ViewExpression filter) + { + if (analysis.hasRowFilter(name, currentIdentity)) { + throw new PrestoException(INVALID_ROW_FILTER, extractLocation(table), format("Row filter for '%s' is recursive", name), null); + } + + Expression expression; + try { + expression = sqlParser.createExpression(filter.getExpression(), createParsingOptions(session)); + } + catch (ParsingException e) { + throw new PrestoException(INVALID_ROW_FILTER, extractLocation(table), format("Invalid row filter for '%s': %s", name, e.getErrorMessage()), e); + } + + analysis.registerTableForRowFiltering(name, currentIdentity); + ExpressionAnalysis expressionAnalysis; + try { + expressionAnalysis = ExpressionAnalyzer.analyzeExpression( + createViewSession(filter.getCatalog(), filter.getSchema(), Identity.forUser(filter.getIdentity()).build(), session.getPath()), // TODO: path should be included in row filter + metadata, + accessControl, + sqlParser, + scope, + analysis, + expression, + warningCollector); + } + catch (PrestoException e) { + throw new PrestoException(e::getErrorCode, extractLocation(table), format("Invalid row filter for '%s': %s", name, e.getRawMessage()), e); + } + finally { + analysis.unregisterTableForRowFiltering(name, currentIdentity); + } + + verifyNoAggregateWindowOrGroupingFunctions(metadata, expression, format("Row filter for '%s'", name)); + + analysis.recordSubqueries(expression, expressionAnalysis); + + Type actualType = expressionAnalysis.getType(expression); + if (!actualType.equals(BOOLEAN)) { + TypeCoercion coercion = new TypeCoercion(metadata::getType); + + if (!coercion.canCoerce(actualType, BOOLEAN)) { + throw new PrestoException(TYPE_MISMATCH, extractLocation(table), format("Expected row filter for '%s' to be of type BOOLEAN, but was %s", name, actualType), null); + } + + analysis.addCoercion(expression, BOOLEAN, coercion.isTypeOnlyCoercion(actualType, BOOLEAN)); + } + + analysis.addRowFilter(table, expression); + } + private List descriptorToFields(Scope scope) { ImmutableList.Builder builder = ImmutableList.builder(); @@ -2596,6 +2656,25 @@ private Scope.Builder scopeBuilder(Optional parentScope) } } + private Session createViewSession(Optional catalog, Optional schema, Identity identity, SqlPath path) + { + return Session.builder(metadata.getSessionPropertyManager()) + .setQueryId(session.getQueryId()) + .setTransactionId(session.getTransactionId().orElse(null)) + .setIdentity(identity) + .setSource(session.getSource().orElse(null)) + .setCatalog(catalog.orElse(null)) + .setSchema(schema.orElse(null)) + .setPath(path) + .setTimeZoneKey(session.getTimeZoneKey()) + .setLocale(session.getLocale()) + .setRemoteUserAddress(session.getRemoteUserAddress().orElse(null)) + .setUserAgent(session.getUserAgent().orElse(null)) + .setClientInfo(session.getClientInfo().orElse(null)) + .setStartTime(session.getStartTime()) + .build(); + } + private static boolean hasScopeAsLocalParent(Scope root, Scope parent) { Scope scope = root; diff --git a/presto-main/src/main/java/io/prestosql/sql/planner/RelationPlanner.java b/presto-main/src/main/java/io/prestosql/sql/planner/RelationPlanner.java index ee031a6247cf..03db6ad72faa 100644 --- a/presto-main/src/main/java/io/prestosql/sql/planner/RelationPlanner.java +++ b/presto-main/src/main/java/io/prestosql/sql/planner/RelationPlanner.java @@ -170,7 +170,26 @@ protected RelationPlan visitTable(Table node, Void context) List outputSymbols = outputSymbolsBuilder.build(); PlanNode root = TableScanNode.newInstance(idAllocator.getNextId(), handle, outputSymbols, columns.build()); - return new RelationPlan(root, scope, outputSymbols); + + RelationPlan tableScan = new RelationPlan(root, scope, outputSymbols); + tableScan = addRowFilters(node, tableScan); + return tableScan; + } + + private RelationPlan addRowFilters(Table node, RelationPlan plan) + { + PlanBuilder planBuilder = new PlanBuilder(new TranslationMap(plan, analysis, ImmutableMap.of()), plan.getRoot()); + + for (Expression filter : analysis.getRowFilters(node)) { + planBuilder = subqueryPlanner.handleSubqueries(planBuilder, filter, filter); + + planBuilder = planBuilder.withNewRoot(new FilterNode( + idAllocator.getNextId(), + planBuilder.getRoot(), + planBuilder.rewrite(filter))); + } + + return new RelationPlan(planBuilder.getRoot(), plan.getScope(), plan.getFieldMappings()); } @Override diff --git a/presto-main/src/main/java/io/prestosql/testing/TestingAccessControlManager.java b/presto-main/src/main/java/io/prestosql/testing/TestingAccessControlManager.java index 5c28cb23c028..0408152e2f9a 100644 --- a/presto-main/src/main/java/io/prestosql/testing/TestingAccessControlManager.java +++ b/presto-main/src/main/java/io/prestosql/testing/TestingAccessControlManager.java @@ -13,6 +13,7 @@ */ package io.prestosql.testing; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import io.prestosql.metadata.QualifiedObjectName; @@ -23,13 +24,18 @@ import io.prestosql.spi.connector.CatalogSchemaName; import io.prestosql.spi.connector.CatalogSchemaTableName; import io.prestosql.spi.security.Identity; +import io.prestosql.spi.security.ViewExpression; import io.prestosql.transaction.TransactionManager; import javax.inject.Inject; import java.security.Principal; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -94,6 +100,7 @@ public class TestingAccessControlManager extends AccessControlManager { private final Set denyPrivileges = new HashSet<>(); + private final Map> rowFilters = new HashMap<>(); private Predicate deniedCatalogs = s -> true; @Inject @@ -118,10 +125,17 @@ public void deny(TestingPrivilege... deniedPrivileges) Collections.addAll(this.denyPrivileges, deniedPrivileges); } + public void rowFilter(QualifiedObjectName table, String identity, ViewExpression filter) + { + rowFilters.computeIfAbsent(new RowFilterKey(identity, table), key -> new ArrayList<>()) + .add(filter); + } + public void reset() { denyPrivileges.clear(); deniedCatalogs = s -> true; + rowFilters.clear(); } public void denyCatalogs(Predicate deniedCatalogs) @@ -437,6 +451,12 @@ public void checkCanSelectFromColumns(SecurityContext context, QualifiedObjectNa } } + @Override + public List getRowFilters(SecurityContext context, QualifiedObjectName tableName) + { + return rowFilters.getOrDefault(new RowFilterKey(context.getIdentity().getUser(), tableName), ImmutableList.of()); + } + private boolean shouldDenyPrivilege(String userName, String entityName, TestingPrivilegeType type) { return shouldDenyPrivilege(privilege(userName, entityName, type)); @@ -514,4 +534,36 @@ public String toString() .toString(); } } + + private static class RowFilterKey + { + private final String identity; + private final QualifiedObjectName table; + + public RowFilterKey(String identity, QualifiedObjectName table) + { + this.identity = requireNonNull(identity, "identity is null"); + this.table = requireNonNull(table, "table is null"); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RowFilterKey that = (RowFilterKey) o; + return identity.equals(that.identity) && + table.equals(that.table); + } + + @Override + public int hashCode() + { + return Objects.hash(identity, table); + } + } } diff --git a/presto-main/src/test/java/io/prestosql/sql/query/QueryAssertions.java b/presto-main/src/test/java/io/prestosql/sql/query/QueryAssertions.java index 883dbeb3a8c9..3f3dcd76acca 100644 --- a/presto-main/src/test/java/io/prestosql/sql/query/QueryAssertions.java +++ b/presto-main/src/test/java/io/prestosql/sql/query/QueryAssertions.java @@ -34,6 +34,7 @@ import static io.airlift.testing.Assertions.assertEqualsIgnoreOrder; import static io.prestosql.testing.TestingSession.testSessionBuilder; import static java.lang.String.format; +import static java.util.Objects.requireNonNull; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; @@ -52,7 +53,12 @@ public QueryAssertions() public QueryAssertions(Session session) { - runner = LocalQueryRunner.create(session); + this(LocalQueryRunner.create(session)); + } + + public QueryAssertions(QueryRunner runner) + { + this.runner = requireNonNull(runner, "runner is null"); } public void assertFails(@Language("SQL") String sql, @Language("RegExp") String expectedMessageRegExp) diff --git a/presto-main/src/test/java/io/prestosql/sql/query/TestRowFilter.java b/presto-main/src/test/java/io/prestosql/sql/query/TestRowFilter.java new file mode 100644 index 000000000000..9a6a77c3040e --- /dev/null +++ b/presto-main/src/test/java/io/prestosql/sql/query/TestRowFilter.java @@ -0,0 +1,306 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.prestosql.sql.query; + +import com.google.common.collect.ImmutableMap; +import io.prestosql.Session; +import io.prestosql.metadata.QualifiedObjectName; +import io.prestosql.plugin.tpch.TpchConnectorFactory; +import io.prestosql.spi.security.Identity; +import io.prestosql.spi.security.ViewExpression; +import io.prestosql.testing.LocalQueryRunner; +import io.prestosql.testing.TestingAccessControlManager; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static io.prestosql.plugin.tpch.TpchMetadata.TINY_SCHEMA_NAME; +import static io.prestosql.testing.TestingSession.testSessionBuilder; + +public class TestRowFilter +{ + private static final String CATALOG = "local"; + private static final String USER = "user"; + private static final String RUN_AS_USER = "run-as-user"; + + private QueryAssertions assertions; + private TestingAccessControlManager accessControl; + + @BeforeClass + public void init() + { + Session session = testSessionBuilder() + .setCatalog(CATALOG) + .setSchema(TINY_SCHEMA_NAME) + .setIdentity(Identity.forUser(USER).build()) + .build(); + + LocalQueryRunner runner = LocalQueryRunner.builder(session) + .build(); + + runner.createCatalog(CATALOG, new TpchConnectorFactory(1), ImmutableMap.of()); + + assertions = new QueryAssertions(runner); + accessControl = assertions.getQueryRunner().getAccessControl(); + } + + @AfterClass(alwaysRun = true) + public void teardown() + { + assertions.close(); + assertions = null; + } + + @Test + public void testSimpleFilter() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.empty(), Optional.empty(), "orderkey < 10")); + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '7'"); + }); + + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.empty(), Optional.empty(), "NULL")); + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '0'"); + }); + } + + @Test + public void testMultipleFilters() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.empty(), Optional.empty(), "orderkey < 10")); + + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.empty(), Optional.empty(), "orderkey > 5")); + + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '2'"); + }); + } + + @Test + public void testCorrelatedSubquery() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "EXISTS (SELECT 1 FROM nation WHERE nationkey = orderkey)")); + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '7'"); + }); + } + + @Test + public void testTableReferenceInWithClause() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.empty(), Optional.empty(), "orderkey = 1")); + assertions.assertQuery("WITH t AS (SELECT count(*) FROM orders) SELECT * FROM t", "VALUES BIGINT '1'"); + }); + } + + @Test + public void testOtherSchema() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("sf1"), "(SELECT count(*) FROM customer) = 150000")); // Filter is TRUE only if evaluating against sf1.customer + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '15000'"); + }); + } + + @Test + public void testDifferentIdentity() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + RUN_AS_USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey = 1")); + + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey IN (SELECT orderkey FROM orders)")); + + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '1'"); + }); + } + + @Test + public void testRecursion() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey IN (SELECT orderkey FROM orders)")); + + assertions.assertFails("SELECT count(*) FROM orders", ".*\\QRow filter for 'local.tiny.orders' is recursive\\E.*"); + }); + + // different reference style to same table + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey IN (SELECT local.tiny.orderkey FROM orders)")); + assertions.assertFails("SELECT count(*) FROM orders", ".*\\QRow filter for 'local.tiny.orders' is recursive\\E.*"); + }); + + // mutual recursion + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + RUN_AS_USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey IN (SELECT orderkey FROM orders)")); + + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey IN (SELECT orderkey FROM orders)")); + + assertions.assertFails("SELECT count(*) FROM orders", ".*\\QRow filter for 'local.tiny.orders' is recursive\\E.*"); + }); + } + + @Test + public void testLimitedScope() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "customer"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey = 1")); + assertions.assertFails( + "SELECT (SELECT min(name) FROM customer WHERE customer.custkey = orders.custkey) FROM orders", + "\\Qline 1:31: Invalid row filter for 'local.tiny.customer': Column 'orderkey' cannot be resolved\\E"); + }); + } + + @Test + public void testSqlInjection() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "nation"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "regionkey IN (SELECT regionkey FROM region WHERE name = 'ASIA')")); + assertions.assertQuery( + "WITH region(regionkey, name) AS (VALUES (0, 'ASIA'), (1, 'ASIA'), (2, 'ASIA'), (3, 'ASIA'), (4, 'ASIA'))" + + "SELECT name FROM nation ORDER BY name LIMIT 1", + "VALUES CAST('CHINA' AS VARCHAR(25))"); // if sql-injection would work then query would return ALGERIA + }); + } + + @Test + public void testInvalidFilter() + { + // parse error + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "$$$")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\Qline 1:22: Invalid row filter for 'local.tiny.orders': mismatched input '$'. Expecting: \\E"); + }); + + // unknown column + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "unknown_column")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\Qline 1:22: Invalid row filter for 'local.tiny.orders': Column 'unknown_column' cannot be resolved\\E"); + }); + + // invalid type + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "1")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\Qline 1:22: Expected row filter for 'local.tiny.orders' to be of type BOOLEAN, but was integer\\E"); + }); + + // aggregation + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "count(*) > 0")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\Qline 1:10: Row filter for 'local.tiny.orders' cannot contain aggregations, window functions or grouping operations: [count(*)]\\E"); + }); + + // window function + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "row_number() OVER () > 0")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\Qline 1:22: Row filter for 'local.tiny.orders' cannot contain aggregations, window functions or grouping operations: [row_number() OVER ()]\\E"); + }); + + // window function + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "grouping(orderkey) = 0")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\Qline 1:20: Row filter for 'local.tiny.orders' cannot contain aggregations, window functions or grouping operations: [GROUPING (orderkey)]\\E"); + }); + } +} diff --git a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/classloader/ClassLoaderSafeConnectorAccessControl.java b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/classloader/ClassLoaderSafeConnectorAccessControl.java index 07662e9eb783..16767369cccb 100644 --- a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/classloader/ClassLoaderSafeConnectorAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/classloader/ClassLoaderSafeConnectorAccessControl.java @@ -20,6 +20,7 @@ import io.prestosql.spi.connector.SchemaTableName; import io.prestosql.spi.security.PrestoPrincipal; import io.prestosql.spi.security.Privilege; +import io.prestosql.spi.security.ViewExpression; import javax.inject.Inject; @@ -313,4 +314,12 @@ public void checkCanShowRoleGrants(ConnectorSecurityContext context, String cata delegate.checkCanShowRoleGrants(context, catalogName); } } + + @Override + public Optional getRowFilter(ConnectorSecurityContext context, SchemaTableName tableName) + { + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { + return delegate.getRowFilter(context, tableName); + } + } } diff --git a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/AllowAllAccessControl.java b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/AllowAllAccessControl.java index 9fe73cdf31e4..7c18e5e260be 100644 --- a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/AllowAllAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/AllowAllAccessControl.java @@ -19,6 +19,7 @@ import io.prestosql.spi.connector.SchemaTableName; import io.prestosql.spi.security.PrestoPrincipal; import io.prestosql.spi.security.Privilege; +import io.prestosql.spi.security.ViewExpression; import java.util.List; import java.util.Optional; @@ -199,4 +200,10 @@ public void checkCanShowCurrentRoles(ConnectorSecurityContext context, String ca public void checkCanShowRoleGrants(ConnectorSecurityContext context, String catalogName) { } + + @Override + public Optional getRowFilter(ConnectorSecurityContext context, SchemaTableName tableName) + { + return Optional.empty(); + } } diff --git a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/AllowAllSystemAccessControl.java b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/AllowAllSystemAccessControl.java index 1e9bd3c468a2..3ed72f26ba61 100644 --- a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/AllowAllSystemAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/AllowAllSystemAccessControl.java @@ -22,6 +22,7 @@ import io.prestosql.spi.security.SystemAccessControl; import io.prestosql.spi.security.SystemAccessControlFactory; import io.prestosql.spi.security.SystemSecurityContext; +import io.prestosql.spi.security.ViewExpression; import java.security.Principal; import java.util.List; @@ -241,4 +242,10 @@ public void checkCanRevokeTablePrivilege(SystemSecurityContext context, Privileg public void checkCanShowRoles(SystemSecurityContext context, String catalogName) { } + + @Override + public Optional getRowFilter(SystemSecurityContext context, CatalogSchemaTableName tableName) + { + return Optional.empty(); + } } diff --git a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/FileBasedAccessControl.java b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/FileBasedAccessControl.java index fa5c492dbea8..a170c0fed4fa 100644 --- a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/FileBasedAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/FileBasedAccessControl.java @@ -21,6 +21,7 @@ import io.prestosql.spi.connector.SchemaTableName; import io.prestosql.spi.security.PrestoPrincipal; import io.prestosql.spi.security.Privilege; +import io.prestosql.spi.security.ViewExpression; import javax.inject.Inject; @@ -317,6 +318,12 @@ public void checkCanShowRoleGrants(ConnectorSecurityContext context, String cata { } + @Override + public Optional getRowFilter(ConnectorSecurityContext context, SchemaTableName tableName) + { + return Optional.empty(); + } + private boolean canSetSessionProperty(ConnectorSecurityContext context, String property) { for (SessionPropertyAccessControlRule rule : sessionPropertyRules) { diff --git a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/FileBasedSystemAccessControl.java b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/FileBasedSystemAccessControl.java index dceda6a37e0e..135ca9c4c7da 100644 --- a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/FileBasedSystemAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/FileBasedSystemAccessControl.java @@ -29,6 +29,7 @@ import io.prestosql.spi.security.SystemAccessControl; import io.prestosql.spi.security.SystemAccessControlFactory; import io.prestosql.spi.security.SystemSecurityContext; +import io.prestosql.spi.security.ViewExpression; import java.nio.file.Paths; import java.security.Principal; @@ -520,4 +521,10 @@ public void checkCanRevokeTablePrivilege(SystemSecurityContext context, Privileg public void checkCanShowRoles(SystemSecurityContext context, String catalogName) { } + + @Override + public Optional getRowFilter(SystemSecurityContext context, CatalogSchemaTableName tableName) + { + return Optional.empty(); + } } diff --git a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/ForwardingConnectorAccessControl.java b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/ForwardingConnectorAccessControl.java index 38e309a7bd91..7a28ca84d5bb 100644 --- a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/ForwardingConnectorAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/ForwardingConnectorAccessControl.java @@ -19,6 +19,7 @@ import io.prestosql.spi.connector.SchemaTableName; import io.prestosql.spi.security.PrestoPrincipal; import io.prestosql.spi.security.Privilege; +import io.prestosql.spi.security.ViewExpression; import java.util.List; import java.util.Optional; @@ -248,4 +249,10 @@ public void checkCanShowRoleGrants(ConnectorSecurityContext context, String cata { delegate().checkCanShowRoleGrants(context, catalogName); } + + @Override + public Optional getRowFilter(ConnectorSecurityContext context, SchemaTableName tableName) + { + return delegate().getRowFilter(context, tableName); + } } diff --git a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/ForwardingSystemAccessControl.java b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/ForwardingSystemAccessControl.java index 3bb35537237a..1fe806885573 100644 --- a/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/ForwardingSystemAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/io/prestosql/plugin/base/security/ForwardingSystemAccessControl.java @@ -21,6 +21,7 @@ import io.prestosql.spi.security.Privilege; import io.prestosql.spi.security.SystemAccessControl; import io.prestosql.spi.security.SystemSecurityContext; +import io.prestosql.spi.security.ViewExpression; import java.security.Principal; import java.util.List; @@ -263,4 +264,10 @@ public void checkCanShowRoles(SystemSecurityContext context, String catalogName) { delegate().checkCanShowRoles(context, catalogName); } + + @Override + public Optional getRowFilter(SystemSecurityContext context, CatalogSchemaTableName tableName) + { + return delegate().getRowFilter(context, tableName); + } } diff --git a/presto-spi/src/main/java/io/prestosql/spi/StandardErrorCode.java b/presto-spi/src/main/java/io/prestosql/spi/StandardErrorCode.java index a1e775eb1cdc..4393983b1ea8 100644 --- a/presto-spi/src/main/java/io/prestosql/spi/StandardErrorCode.java +++ b/presto-spi/src/main/java/io/prestosql/spi/StandardErrorCode.java @@ -104,6 +104,7 @@ public enum StandardErrorCode VIEW_IS_STALE(81, USER_ERROR), VIEW_IS_RECURSIVE(82, USER_ERROR), NULL_TREATMENT_NOT_ALLOWED(83, USER_ERROR), + INVALID_ROW_FILTER(84, USER_ERROR), GENERIC_INTERNAL_ERROR(65536, INTERNAL_ERROR), TOO_MANY_REQUESTS_FAILED(65537, INTERNAL_ERROR), diff --git a/presto-spi/src/main/java/io/prestosql/spi/connector/ConnectorAccessControl.java b/presto-spi/src/main/java/io/prestosql/spi/connector/ConnectorAccessControl.java index f328d33ccd1b..ad6ab0c5099d 100644 --- a/presto-spi/src/main/java/io/prestosql/spi/connector/ConnectorAccessControl.java +++ b/presto-spi/src/main/java/io/prestosql/spi/connector/ConnectorAccessControl.java @@ -15,6 +15,7 @@ import io.prestosql.spi.security.PrestoPrincipal; import io.prestosql.spi.security.Privilege; +import io.prestosql.spi.security.ViewExpression; import java.util.Collections; import java.util.List; @@ -368,4 +369,16 @@ default void checkCanShowRoleGrants(ConnectorSecurityContext context, String cat { denyShowRoleGrants(catalogName); } + + /** + * Get a row filter associated with the given table and identity. + * + * The filter must be a scalar SQL expression of boolean type over the columns in the table. + * + * @return the filter, or {@link Optional#empty()} if not applicable + */ + default Optional getRowFilter(ConnectorSecurityContext context, SchemaTableName tableName) + { + return Optional.empty(); + } } diff --git a/presto-spi/src/main/java/io/prestosql/spi/security/SystemAccessControl.java b/presto-spi/src/main/java/io/prestosql/spi/security/SystemAccessControl.java index 39d4c094ec0a..2286f82b8a15 100644 --- a/presto-spi/src/main/java/io/prestosql/spi/security/SystemAccessControl.java +++ b/presto-spi/src/main/java/io/prestosql/spi/security/SystemAccessControl.java @@ -415,4 +415,16 @@ default void checkCanShowRoles(SystemSecurityContext context, String catalogName { denyShowRoles(catalogName); } + + /** + * Get a row filter associated with the given table and identity. + * + * The filter must be a scalar SQL expression of boolean type over the columns in the table. + * + * @return the filter, or {@link Optional#empty()} if not applicable + */ + default Optional getRowFilter(SystemSecurityContext context, CatalogSchemaTableName tableName) + { + return Optional.empty(); + } } diff --git a/presto-spi/src/main/java/io/prestosql/spi/security/ViewExpression.java b/presto-spi/src/main/java/io/prestosql/spi/security/ViewExpression.java new file mode 100644 index 000000000000..cef670b998f8 --- /dev/null +++ b/presto-spi/src/main/java/io/prestosql/spi/security/ViewExpression.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.prestosql.spi.security; + +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class ViewExpression +{ + private final String identity; + private final Optional catalog; + private final Optional schema; + private final String expression; + + public ViewExpression(String identity, Optional catalog, Optional schema, String expression) + { + this.identity = requireNonNull(identity, "identity is null"); + this.catalog = requireNonNull(catalog, "catalog is null"); + this.schema = requireNonNull(schema, "schema is null"); + this.expression = requireNonNull(expression, "expression is null"); + + if (!catalog.isPresent() && schema.isPresent()) { + throw new IllegalArgumentException("catalog must be present if schema is present"); + } + } + + public String getIdentity() + { + return identity; + } + + public Optional getCatalog() + { + return catalog; + } + + public Optional getSchema() + { + return schema; + } + + public String getExpression() + { + return expression; + } +}