morrySnow commented on code in PR #67880: URL: https://github.com/apache/doris/pull/67880#discussion_r4011532991
########## regression-test/suites/prepared_stmt_p0/prepared_short_circuit_security_refresh.groovy: ########## @@ -0,0 +1,131 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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. + +import java.sql.DriverManager +import java.sql.SQLException + +suite("prepared_short_circuit_security_refresh", "nonConcurrent") { + def dbName = context.config.getDbNameByFile(context.file) + def policyName = "prepared_short_circuit_security_refresh_policy" + def testUser = "prepared_short_circuit_security_refresh_user" + def testPassword = "PreparedSecurity@123" + def adminUser = context.config.jdbcUser + def adminPassword = context.config.jdbcPassword + String serverPrepareUrl = getServerPrepareJdbcUrl(context.config.jdbcUrl, dbName) + + sql "DROP TABLE IF EXISTS prepared_short_circuit_security_refresh_tbl" + sql "DROP USER IF EXISTS ${testUser}" + sql "CREATE USER ${testUser} IDENTIFIED BY '${testPassword}'" + sql """ + CREATE TABLE prepared_short_circuit_security_refresh_tbl ( + k INT NOT NULL, + tenant_id INT NOT NULL, + payload VARCHAR(32) NULL + ) ENGINE=OLAP + UNIQUE KEY(k) + DISTRIBUTED BY HASH(k) BUCKETS 1 + PROPERTIES ( + "replication_num" = "1", + "enable_unique_key_merge_on_write" = "true", + "store_row_column" = "true" + ) + """ + sql """DROP ROW POLICY IF EXISTS ${policyName} + ON ${dbName}.prepared_short_circuit_security_refresh_tbl FOR ${testUser}""" + sql """INSERT INTO prepared_short_circuit_security_refresh_tbl + VALUES (1, 10, 'allowed'), (2, 20, 'restricted')""" + sql "GRANT SELECT_PRIV ON ${dbName}.prepared_short_circuit_security_refresh_tbl TO ${testUser}" + sql "SET GLOBAL enable_server_side_prepared_statement = true" + sql "SYNC" + + if (isCloudMode()) { + def clusters = sql "SHOW CLUSTERS" + assertTrue(!clusters.isEmpty()) + sql "GRANT USAGE_PRIV ON CLUSTER `${clusters[0][0]}` TO ${testUser}" + } + + def adminConnection = DriverManager.getConnection(context.config.jdbcUrl, adminUser, adminPassword) + def adminExecute = { String statement -> + adminConnection.createStatement().withCloseable { adminStatement -> + adminStatement.execute(statement) + } + } + + try { + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true, + enable_fallback_to_original_planner=false, + enable_short_circuit_query=true) */ + k, tenant_id, payload + FROM prepared_short_circuit_security_refresh_tbl + WHERE k = 2 + """ + contains "SHORT-CIRCUIT" + } + connect(testUser, testPassword, serverPrepareUrl) { + sql "SET enable_fallback_to_original_planner = false" + def prepared = prepareStatement( + """SELECT /*+ SET_VAR(enable_nereids_planner=true, + enable_fallback_to_original_planner=false, + enable_short_circuit_query=true) */ + k, tenant_id, payload + FROM prepared_short_circuit_security_refresh_tbl + WHERE k = ?""") + assertEquals(com.mysql.cj.jdbc.ServerPreparedStatement, prepared.class) + prepared.setInt(1, 2) + + // The second execution must use the cached point-query plan. + qe_before_policy prepared + qe_cached_before_policy prepared + + adminExecute(""" + CREATE ROW POLICY ${policyName} + ON ${dbName}.prepared_short_circuit_security_refresh_tbl + AS RESTRICTIVE TO ${testUser} USING (tenant_id = 10) + """) + qe_after_policy_added prepared + + adminExecute("""DROP ROW POLICY ${policyName} + ON ${dbName}.prepared_short_circuit_security_refresh_tbl FOR ${testUser}""") + qe_after_policy_dropped prepared + + adminExecute("""REVOKE SELECT_PRIV + ON ${dbName}.prepared_short_circuit_security_refresh_tbl FROM ${testUser}""") + boolean denied = false + try { + prepared.executeQuery().close() + } catch (SQLException e) { Review Comment: Fixed in #67885 (`d3ac58ea8a4`). `assertSelectDenied` now accepts only an error containing `permission denied`, `select_priv`, and the target table name, and rethrows every other `SQLException`. The sandbox regression verifies this for both `COM_CHANGE_USER` and SELECT revocation. ########## fe/fe-core/src/main/java/org/apache/doris/nereids/SecurityDependencyContext.java: ########## @@ -0,0 +1,303 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.doris.nereids; + +import org.apache.doris.analysis.UserIdentity; +import org.apache.doris.authorization.DataMaskSpec; +import org.apache.doris.authorization.RowFilterSpec; +import org.apache.doris.catalog.DatabaseIf; +import org.apache.doris.catalog.Env; +import org.apache.doris.catalog.TableIf; +import org.apache.doris.common.UserException; +import org.apache.doris.datasource.CatalogIf; +import org.apache.doris.datasource.InternalCatalog; +import org.apache.doris.mysql.privilege.Auth; +import org.apache.doris.mysql.privilege.InternalAuthorizationPlugin; +import org.apache.doris.nereids.SqlCacheContext.FullColumnName; +import org.apache.doris.nereids.SqlCacheContext.FullTableName; +import org.apache.doris.nereids.rules.analysis.UserAuthentication; +import org.apache.doris.policy.PolicyMgr; +import org.apache.doris.qe.ConnectContext; +import org.apache.doris.qe.SessionVariable; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * Security decisions which an analyzed plan depends on. + * + * <p>Unlike {@link SqlCacheContext}, this context exists independently of the SQL result-cache switch. A prepared + * short-circuit plan can otherwise outlive the privilege and data-policy decisions made while it was analyzed. + * Callers record both positive and negative policy answers so that adding a policy invalidates a plan which was + * built before that policy existed. + */ +public class SecurityDependencyContext { + private static final long UNKNOWN_VERSION = -1; + + private final Env planningEnv; + private final long authorizationVersion; + private final long rowPolicyVersion; + private final boolean versionValidationEligible; + private final Map<FullTableName, Set<String>> checkedPrivileges = Maps.newLinkedHashMap(); + private final Map<FullTableName, List<RowFilterSpec>> rowPolicies = Maps.newLinkedHashMap(); + private final Map<FullColumnName, Optional<DataMaskSpec>> dataMaskPolicies = Maps.newLinkedHashMap(); + private final Map<FullTableName, Set<String>> dataMaskColumnsByTable = Maps.newLinkedHashMap(); + private boolean useVersionValidation; + private boolean complete = true; + + /** Create a context which always uses full security revalidation. */ + public SecurityDependencyContext() { + this(null, UNKNOWN_VERSION, UNKNOWN_VERSION, false); + } + + /** Create a context and capture the security versions before analysis starts. */ + public SecurityDependencyContext(ConnectContext connectContext) { + this(connectContext == null ? null : connectContext.getEnv(), usesAuthorizationChecks(connectContext)); + } + + private SecurityDependencyContext(Env env, boolean versionValidationEligible) { + this(env, currentAuthorizationVersion(env), currentRowPolicyVersion(env), versionValidationEligible); + } + + private SecurityDependencyContext(Env planningEnv, long authorizationVersion, long rowPolicyVersion, + boolean versionValidationEligible) { + this.planningEnv = planningEnv; + this.authorizationVersion = authorizationVersion; + this.rowPolicyVersion = rowPolicyVersion; + this.versionValidationEligible = versionValidationEligible; + } + + /** Record the columns whose SELECT privilege was checked while the plan was analyzed. */ + public synchronized void addCheckedPrivilege(TableIf table, Set<String> usedColumns) { + Optional<FullTableName> tableName = qualifiedName(table); + if (!tableName.isPresent()) { + complete = false; + return; + } + Set<String> existing = checkedPrivileges.get(tableName.get()); + if (existing == null) { + checkedPrivileges.put(tableName.get(), ImmutableSet.copyOf(usedColumns)); + } else { + checkedPrivileges.put(tableName.get(), ImmutableSet.<String>builder() + .addAll(existing).addAll(usedColumns).build()); + } + } + + /** Record the complete row-filter answer, including an empty answer. */ + public synchronized void setRowPolicies( + String catalog, String database, String table, List<RowFilterSpec> policies) { + rowPolicies.put(new FullTableName(catalog, database, table), ImmutableList.copyOf(policies)); + } + + /** Record the mask answer for a column, including the absence of a mask. */ + public synchronized void addDataMask( + String catalog, String database, String table, String column, Optional<DataMaskSpec> mask) { + String normalizedColumn = column.toLowerCase(Locale.ROOT); + FullTableName tableName = new FullTableName(catalog, database, table); + dataMaskPolicies.put(new FullColumnName(catalog, database, table, normalizedColumn), mask); + dataMaskColumnsByTable.computeIfAbsent(tableName, ignored -> new LinkedHashSet<>()).add(normalizedColumn); + } + + /** Freeze the decisions used by a completed plan before storing them in a reusable context. */ + public synchronized SecurityDependencyContext snapshot() { Review Comment: Fixed in #67885 (`d3ac58ea8a4`). Plans with inlined view definitions are now explicitly ineligible for the short-circuit rewrite. `ShortCircuitPointQueryTest.testViewDoesNotUseShortCircuit` verifies that a simple eligible-looking view remains on the normal planning path. ########## fe/fe-core/src/main/java/org/apache/doris/nereids/SecurityDependencyContext.java: ########## @@ -0,0 +1,303 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.doris.nereids; + +import org.apache.doris.analysis.UserIdentity; +import org.apache.doris.authorization.DataMaskSpec; +import org.apache.doris.authorization.RowFilterSpec; +import org.apache.doris.catalog.DatabaseIf; +import org.apache.doris.catalog.Env; +import org.apache.doris.catalog.TableIf; +import org.apache.doris.common.UserException; +import org.apache.doris.datasource.CatalogIf; +import org.apache.doris.datasource.InternalCatalog; +import org.apache.doris.mysql.privilege.Auth; +import org.apache.doris.mysql.privilege.InternalAuthorizationPlugin; +import org.apache.doris.nereids.SqlCacheContext.FullColumnName; +import org.apache.doris.nereids.SqlCacheContext.FullTableName; +import org.apache.doris.nereids.rules.analysis.UserAuthentication; +import org.apache.doris.policy.PolicyMgr; +import org.apache.doris.qe.ConnectContext; +import org.apache.doris.qe.SessionVariable; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * Security decisions which an analyzed plan depends on. + * + * <p>Unlike {@link SqlCacheContext}, this context exists independently of the SQL result-cache switch. A prepared + * short-circuit plan can otherwise outlive the privilege and data-policy decisions made while it was analyzed. + * Callers record both positive and negative policy answers so that adding a policy invalidates a plan which was + * built before that policy existed. + */ +public class SecurityDependencyContext { + private static final long UNKNOWN_VERSION = -1; + + private final Env planningEnv; + private final long authorizationVersion; + private final long rowPolicyVersion; + private final boolean versionValidationEligible; + private final Map<FullTableName, Set<String>> checkedPrivileges = Maps.newLinkedHashMap(); + private final Map<FullTableName, List<RowFilterSpec>> rowPolicies = Maps.newLinkedHashMap(); + private final Map<FullColumnName, Optional<DataMaskSpec>> dataMaskPolicies = Maps.newLinkedHashMap(); + private final Map<FullTableName, Set<String>> dataMaskColumnsByTable = Maps.newLinkedHashMap(); + private boolean useVersionValidation; + private boolean complete = true; + + /** Create a context which always uses full security revalidation. */ + public SecurityDependencyContext() { + this(null, UNKNOWN_VERSION, UNKNOWN_VERSION, false); + } + + /** Create a context and capture the security versions before analysis starts. */ + public SecurityDependencyContext(ConnectContext connectContext) { + this(connectContext == null ? null : connectContext.getEnv(), usesAuthorizationChecks(connectContext)); + } + + private SecurityDependencyContext(Env env, boolean versionValidationEligible) { + this(env, currentAuthorizationVersion(env), currentRowPolicyVersion(env), versionValidationEligible); + } + + private SecurityDependencyContext(Env planningEnv, long authorizationVersion, long rowPolicyVersion, + boolean versionValidationEligible) { + this.planningEnv = planningEnv; + this.authorizationVersion = authorizationVersion; + this.rowPolicyVersion = rowPolicyVersion; + this.versionValidationEligible = versionValidationEligible; + } + + /** Record the columns whose SELECT privilege was checked while the plan was analyzed. */ + public synchronized void addCheckedPrivilege(TableIf table, Set<String> usedColumns) { + Optional<FullTableName> tableName = qualifiedName(table); + if (!tableName.isPresent()) { + complete = false; + return; + } + Set<String> existing = checkedPrivileges.get(tableName.get()); + if (existing == null) { + checkedPrivileges.put(tableName.get(), ImmutableSet.copyOf(usedColumns)); + } else { + checkedPrivileges.put(tableName.get(), ImmutableSet.<String>builder() + .addAll(existing).addAll(usedColumns).build()); + } + } + + /** Record the complete row-filter answer, including an empty answer. */ + public synchronized void setRowPolicies( + String catalog, String database, String table, List<RowFilterSpec> policies) { + rowPolicies.put(new FullTableName(catalog, database, table), ImmutableList.copyOf(policies)); + } + + /** Record the mask answer for a column, including the absence of a mask. */ + public synchronized void addDataMask( + String catalog, String database, String table, String column, Optional<DataMaskSpec> mask) { + String normalizedColumn = column.toLowerCase(Locale.ROOT); + FullTableName tableName = new FullTableName(catalog, database, table); + dataMaskPolicies.put(new FullColumnName(catalog, database, table, normalizedColumn), mask); + dataMaskColumnsByTable.computeIfAbsent(tableName, ignored -> new LinkedHashSet<>()).add(normalizedColumn); + } + + /** Freeze the decisions used by a completed plan before storing them in a reusable context. */ + public synchronized SecurityDependencyContext snapshot() { + SecurityDependencyContext snapshot = new SecurityDependencyContext( + planningEnv, authorizationVersion, rowPolicyVersion, versionValidationEligible); + snapshot.complete = complete; + for (Map.Entry<FullTableName, Set<String>> entry : checkedPrivileges.entrySet()) { + snapshot.checkedPrivileges.put(entry.getKey(), ImmutableSet.copyOf(entry.getValue())); + } + for (Map.Entry<FullTableName, List<RowFilterSpec>> entry : rowPolicies.entrySet()) { + snapshot.rowPolicies.put(entry.getKey(), ImmutableList.copyOf(entry.getValue())); + } + snapshot.dataMaskPolicies.putAll(dataMaskPolicies); + for (Map.Entry<FullTableName, Set<String>> entry : dataMaskColumnsByTable.entrySet()) { + snapshot.dataMaskColumnsByTable.put(entry.getKey(), ImmutableSet.copyOf(entry.getValue())); + } + snapshot.useVersionValidation = snapshot.canUseVersionValidation(); + return snapshot; + } + + /** Freeze the decisions for a prepared short-circuit plan, failing closed if authorization was not recorded. */ + public synchronized SecurityDependencyContext snapshotForShortCircuit() { + SecurityDependencyContext snapshot = snapshot(); + if (checkedPrivileges.isEmpty()) { + snapshot.complete = false; + } + return snapshot; + } + + /** + * Revalidate every security decision before a cached plan bypasses analysis. + * + * <p>A false result does not deny the statement itself. It rejects only the cached plan, after which the normal + * planning path performs the authoritative checks and returns the usual user-facing error when access was + * revoked. Authorization-source failures also reject reuse, so this fast path always fails closed. + */ + public boolean isValid(ConnectContext connectContext) { + if (!complete || connectContext == null) { + return false; + } + try { + Env env = connectContext.getEnv(); + if (useVersionValidation) { + return usesAuthorizationChecks(connectContext) && versionsAreCurrent(env); Review Comment: Fixed in #67885 (`d3ac58ea8a4`). The cached dependency snapshot now stores the planning `UserIdentity` and an immutable authenticated-role set, and compares both before epoch validation. The regression uses Connector/J `changeUser` on the same `ServerPreparedStatement`, verifies that user B reaches the expected SELECT denial, and then switches back to user A successfully. ########## fe/fe-core/src/main/java/org/apache/doris/nereids/SecurityDependencyContext.java: ########## @@ -0,0 +1,303 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.doris.nereids; + +import org.apache.doris.analysis.UserIdentity; +import org.apache.doris.authorization.DataMaskSpec; +import org.apache.doris.authorization.RowFilterSpec; +import org.apache.doris.catalog.DatabaseIf; +import org.apache.doris.catalog.Env; +import org.apache.doris.catalog.TableIf; +import org.apache.doris.common.UserException; +import org.apache.doris.datasource.CatalogIf; +import org.apache.doris.datasource.InternalCatalog; +import org.apache.doris.mysql.privilege.Auth; +import org.apache.doris.mysql.privilege.InternalAuthorizationPlugin; +import org.apache.doris.nereids.SqlCacheContext.FullColumnName; +import org.apache.doris.nereids.SqlCacheContext.FullTableName; +import org.apache.doris.nereids.rules.analysis.UserAuthentication; +import org.apache.doris.policy.PolicyMgr; +import org.apache.doris.qe.ConnectContext; +import org.apache.doris.qe.SessionVariable; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * Security decisions which an analyzed plan depends on. + * + * <p>Unlike {@link SqlCacheContext}, this context exists independently of the SQL result-cache switch. A prepared + * short-circuit plan can otherwise outlive the privilege and data-policy decisions made while it was analyzed. + * Callers record both positive and negative policy answers so that adding a policy invalidates a plan which was + * built before that policy existed. + */ +public class SecurityDependencyContext { + private static final long UNKNOWN_VERSION = -1; + + private final Env planningEnv; + private final long authorizationVersion; + private final long rowPolicyVersion; + private final boolean versionValidationEligible; + private final Map<FullTableName, Set<String>> checkedPrivileges = Maps.newLinkedHashMap(); + private final Map<FullTableName, List<RowFilterSpec>> rowPolicies = Maps.newLinkedHashMap(); + private final Map<FullColumnName, Optional<DataMaskSpec>> dataMaskPolicies = Maps.newLinkedHashMap(); + private final Map<FullTableName, Set<String>> dataMaskColumnsByTable = Maps.newLinkedHashMap(); + private boolean useVersionValidation; + private boolean complete = true; + + /** Create a context which always uses full security revalidation. */ + public SecurityDependencyContext() { + this(null, UNKNOWN_VERSION, UNKNOWN_VERSION, false); + } + + /** Create a context and capture the security versions before analysis starts. */ + public SecurityDependencyContext(ConnectContext connectContext) { + this(connectContext == null ? null : connectContext.getEnv(), usesAuthorizationChecks(connectContext)); + } + + private SecurityDependencyContext(Env env, boolean versionValidationEligible) { + this(env, currentAuthorizationVersion(env), currentRowPolicyVersion(env), versionValidationEligible); + } + + private SecurityDependencyContext(Env planningEnv, long authorizationVersion, long rowPolicyVersion, + boolean versionValidationEligible) { + this.planningEnv = planningEnv; + this.authorizationVersion = authorizationVersion; + this.rowPolicyVersion = rowPolicyVersion; + this.versionValidationEligible = versionValidationEligible; + } + + /** Record the columns whose SELECT privilege was checked while the plan was analyzed. */ + public synchronized void addCheckedPrivilege(TableIf table, Set<String> usedColumns) { + Optional<FullTableName> tableName = qualifiedName(table); + if (!tableName.isPresent()) { + complete = false; + return; + } + Set<String> existing = checkedPrivileges.get(tableName.get()); + if (existing == null) { + checkedPrivileges.put(tableName.get(), ImmutableSet.copyOf(usedColumns)); + } else { + checkedPrivileges.put(tableName.get(), ImmutableSet.<String>builder() + .addAll(existing).addAll(usedColumns).build()); + } + } + + /** Record the complete row-filter answer, including an empty answer. */ + public synchronized void setRowPolicies( + String catalog, String database, String table, List<RowFilterSpec> policies) { + rowPolicies.put(new FullTableName(catalog, database, table), ImmutableList.copyOf(policies)); + } + + /** Record the mask answer for a column, including the absence of a mask. */ + public synchronized void addDataMask( + String catalog, String database, String table, String column, Optional<DataMaskSpec> mask) { + String normalizedColumn = column.toLowerCase(Locale.ROOT); + FullTableName tableName = new FullTableName(catalog, database, table); + dataMaskPolicies.put(new FullColumnName(catalog, database, table, normalizedColumn), mask); + dataMaskColumnsByTable.computeIfAbsent(tableName, ignored -> new LinkedHashSet<>()).add(normalizedColumn); + } + + /** Freeze the decisions used by a completed plan before storing them in a reusable context. */ + public synchronized SecurityDependencyContext snapshot() { + SecurityDependencyContext snapshot = new SecurityDependencyContext( + planningEnv, authorizationVersion, rowPolicyVersion, versionValidationEligible); + snapshot.complete = complete; + for (Map.Entry<FullTableName, Set<String>> entry : checkedPrivileges.entrySet()) { + snapshot.checkedPrivileges.put(entry.getKey(), ImmutableSet.copyOf(entry.getValue())); + } + for (Map.Entry<FullTableName, List<RowFilterSpec>> entry : rowPolicies.entrySet()) { + snapshot.rowPolicies.put(entry.getKey(), ImmutableList.copyOf(entry.getValue())); + } + snapshot.dataMaskPolicies.putAll(dataMaskPolicies); + for (Map.Entry<FullTableName, Set<String>> entry : dataMaskColumnsByTable.entrySet()) { + snapshot.dataMaskColumnsByTable.put(entry.getKey(), ImmutableSet.copyOf(entry.getValue())); + } + snapshot.useVersionValidation = snapshot.canUseVersionValidation(); + return snapshot; + } + + /** Freeze the decisions for a prepared short-circuit plan, failing closed if authorization was not recorded. */ + public synchronized SecurityDependencyContext snapshotForShortCircuit() { + SecurityDependencyContext snapshot = snapshot(); + if (checkedPrivileges.isEmpty()) { + snapshot.complete = false; + } + return snapshot; + } + + /** + * Revalidate every security decision before a cached plan bypasses analysis. + * + * <p>A false result does not deny the statement itself. It rejects only the cached plan, after which the normal + * planning path performs the authoritative checks and returns the usual user-facing error when access was + * revoked. Authorization-source failures also reject reuse, so this fast path always fails closed. + */ + public boolean isValid(ConnectContext connectContext) { + if (!complete || connectContext == null) { + return false; + } + try { + Env env = connectContext.getEnv(); + if (useVersionValidation) { + return usesAuthorizationChecks(connectContext) && versionsAreCurrent(env); + } + UserIdentity currentUser = connectContext.getCurrentUserIdentity(); + if (currentUser == null) { + return false; + } + for (Map.Entry<FullTableName, Set<String>> entry : checkedPrivileges.entrySet()) { + TableIf table = findTable(env, entry.getKey()); + if (table == null) { + return false; + } + UserAuthentication.checkPermission(table, connectContext, entry.getValue()); + } + for (Map.Entry<FullTableName, List<RowFilterSpec>> entry : rowPolicies.entrySet()) { + FullTableName table = entry.getKey(); + List<RowFilterSpec> current = env.getAccessManager().evalRowFilterPolicies( + currentUser, table.catalog, table.db, table.table); + if (!CollectionUtils.isEqualCollection(entry.getValue(), current)) { + return false; + } + } + return dataMasksAreValid(env, currentUser); + } catch (UserException | RuntimeException e) { + return false; + } + } + + private boolean canUseVersionValidation() { + if (!complete || !versionValidationEligible || checkedPrivileges.isEmpty() + || authorizationVersion == UNKNOWN_VERSION || rowPolicyVersion == UNKNOWN_VERSION) { + return false; + } + return allDependenciesUseInternalCatalog() && usesVersionedBuiltInAuthorization(planningEnv); + } + + private boolean versionsAreCurrent(Env env) { + if (env == null || env != planningEnv) { + return false; + } + Auth auth = env.getAuth(); + PolicyMgr policyMgr = env.getPolicyMgr(); + return auth != null && policyMgr != null Review Comment: Fixed in #67885 (`d3ac58ea8a4`). `ShortCircuitQueryContext` now snapshots the database/catalog object identities and names and checks them in `isReusable`. A unit test mutates the database full name and verifies that cached reuse is rejected. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
