Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// 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.OlapTable;
import org.apache.doris.catalog.TableIf;
import org.apache.doris.datasource.CatalogIf;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.mysql.privilege.InternalAuthorizationPlugin;
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.ImmutableSet;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/** Security dependencies of a reusable prepared point-query plan. */
public class SecurityDependencyContext {
private final UserIdentity planningUserIdentity;
private final Set<String> planningAuthenticatedRoles;
private final Env planningEnv;
private final boolean authorizationChecksEnabled;
private final List<CheckedPrivilege> checkedPrivileges = new ArrayList<>();
private boolean hasEffectiveRowPolicy;
private boolean hasDataMask;
private boolean complete;

/** Create an incomplete context for tests and callers without a connection. */
public SecurityDependencyContext() {
this(null, ImmutableSet.of(), null, false);
}

/** Capture the authorization subject before analysis starts. */
public SecurityDependencyContext(ConnectContext connectContext) {
this(connectContext == null ? null : connectContext.getCurrentUserIdentity(),
authenticatedRoles(connectContext),
connectContext == null ? null : connectContext.getEnv(),
usesAuthorizationChecks(connectContext));
}

private SecurityDependencyContext(UserIdentity planningUserIdentity, Set<String> planningAuthenticatedRoles,
Env planningEnv, boolean authorizationChecksEnabled) {
this.planningUserIdentity = planningUserIdentity;
this.planningAuthenticatedRoles = planningAuthenticatedRoles;
this.planningEnv = planningEnv;
this.authorizationChecksEnabled = authorizationChecksEnabled;
this.complete = authorizationChecksEnabled;
}

/** Record the exact SELECT check which must be repeated before direct reuse. */
public synchronized void addCheckedPrivilege(TableIf table, Set<String> usedColumns) {
if (table == null) {
complete = false;
return;
}
DatabaseIf<?> database = table.getDatabase();
CatalogIf<?> catalog = database == null ? null : database.getCatalog();
if (catalog == null
|| !(table instanceof OlapTable)
|| !InternalCatalog.INTERNAL_CATALOG_NAME.equals(catalog.getName())) {
complete = false;
return;
}
checkedPrivileges.add(new CheckedPrivilege(table, database, catalog, catalog.getName(),
database.getFullName(), table.getName(),
usedColumns == null ? ImmutableSet.of() : ImmutableSet.copyOf(usedColumns)));
}

/** Record mask presence so a masked plan is never reused without policy analysis. */
public synchronized void addDataMask(
String catalog, String database, String table, String column, Optional<DataMaskSpec> mask) {
hasDataMask |= mask.isPresent();
}

public synchronized boolean hasDataMask() {
return hasDataMask;
}

/** Record only whether external policy analysis produced a row filter; definitions are not retained. */
public synchronized void addRowPolicies(List<RowFilterSpec> policies) {
hasEffectiveRowPolicy |= policies != null && !policies.isEmpty();
}

public synchronized boolean hasEffectiveRowPolicy() {
return hasEffectiveRowPolicy;
}

/** Freeze the completed dependency set before storing it with the prepared plan. */
public synchronized SecurityDependencyContext snapshotForShortCircuit() {
SecurityDependencyContext snapshot = new SecurityDependencyContext(
planningUserIdentity, planningAuthenticatedRoles, planningEnv, authorizationChecksEnabled);
snapshot.checkedPrivileges.addAll(checkedPrivileges);
snapshot.hasEffectiveRowPolicy = hasEffectiveRowPolicy;
snapshot.hasDataMask = hasDataMask;
snapshot.complete = complete && !checkedPrivileges.isEmpty()
&& !hasEffectiveRowPolicy && !hasDataMask
&& checkedPrivileges.stream().allMatch(CheckedPrivilege::matchesNamespace);
return snapshot;
}

/**
* Recheck the small set of facts needed to bypass planning. Returning false only rejects direct reuse; normal
* planning then performs the authoritative check and reports its standard error.
*/
public boolean isValid(ConnectContext connectContext) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve the relation namespace across security refresh

For an unqualified prepared query, execute SELECT ... FROM t in db_a to publish this cache, then run USE db_b. While the security facts stay unchanged, this check still passes and direct execution remains on db_a.t. If a privilege, role, or row-policy change now makes the new security dependency fail, refreshPreparedPlan reparses the one-part name under the current connection and BindRelation binds db_b.t, permanently switching the same handle to a different table. Thus a security invalidation determines relation resolution. Capture the prepare/bound resolution namespace and use it during refresh (or make cached and normal executions consistently follow the chosen Doris semantics), and add a two-database USE plus policy/revoke regression.

if (!complete || !authorizationChecksEnabled || connectContext == null
|| planningEnv == null || planningEnv != connectContext.getEnv()
|| !Objects.equals(planningUserIdentity, connectContext.getCurrentUserIdentity())
|| !planningAuthenticatedRoles.equals(authenticatedRoles(connectContext))
|| !usesAuthorizationChecks(connectContext)
|| !usesBuiltInAuthorization(planningEnv)) {
return false;
}
try {
PolicyMgr policyMgr = planningEnv.getPolicyMgr();
if (policyMgr == null) {
return false;
}
for (CheckedPrivilege checkedPrivilege : checkedPrivileges) {
if (!checkedPrivilege.matchesNamespace()) {
return false;
}
if (policyMgr.hasRowPolicy(checkedPrivilege.catalog, checkedPrivilege.database,
checkedPrivilege.tableName)) {
return false;
}
UserAuthentication.checkPermission(
checkedPrivilege.table, connectContext, checkedPrivilege.usedColumns);
if (!checkedPrivilege.matchesNamespace()) {
return false;
}
}
return true;
} catch (Exception e) {
return false;
}
}

private static boolean usesBuiltInAuthorization(Env env) {
return env != null && env.getAccessManager() != null
&& env.getAccessManager().getAccessControllerOrDefault(InternalCatalog.INTERNAL_CATALOG_NAME)
instanceof InternalAuthorizationPlugin;
}

private static boolean usesAuthorizationChecks(ConnectContext connectContext) {
if (connectContext == null || connectContext.isSkipAuth()) {
return false;
}
SessionVariable sessionVariable = connectContext.getSessionVariable();
return sessionVariable != null && !sessionVariable.isPlayNereidsDump();
}

private static Set<String> authenticatedRoles(ConnectContext connectContext) {
if (connectContext == null) {
return ImmutableSet.of();
}
Set<String> roles = connectContext.getAuthenticatedRoles();
return roles == null || roles.isEmpty() ? ImmutableSet.of() : ImmutableSet.copyOf(roles);
}

private static class CheckedPrivilege {
private final TableIf table;
private final DatabaseIf<?> databaseObject;
private final CatalogIf<?> catalogObject;
private final String catalog;
private final String database;
private final String tableName;
private final Set<String> usedColumns;

private CheckedPrivilege(TableIf table, DatabaseIf<?> databaseObject, CatalogIf<?> catalogObject,
String catalog, String database, String tableName, Set<String> usedColumns) {
this.table = table;
this.databaseObject = databaseObject;
this.catalogObject = catalogObject;
this.catalog = catalog;
this.database = database;
this.tableName = tableName;
this.usedColumns = usedColumns;
}

private boolean matchesNamespace() {
DatabaseIf<?> currentDatabase = table.getDatabase();
CatalogIf<?> currentCatalog = currentDatabase == null ? null : currentDatabase.getCatalog();
return currentDatabase == databaseObject
&& currentCatalog == catalogObject
&& Objects.equals(database, currentDatabase == null ? null : currentDatabase.getFullName())
&& Objects.equals(catalog, currentCatalog == null ? null : currentCatalog.getName())
&& Objects.equals(tableName, table.getName());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ public enum TableFrom {
private final Map<CTEId, LogicalPlan> rewrittenCteConsumer = new HashMap<>();
private final Set<String> viewDdlSqlSet = Sets.newHashSet();
private final SqlCacheContext sqlCacheContext;
private final SecurityDependencyContext securityDependencyContext;

// generate for next id for prepared statement's placeholders, which is
// connection level
Expand Down Expand Up @@ -383,6 +384,7 @@ private StatementContext(ConnectContext connectContext, OriginStatement originSt
this.connectContext = connectContext;
this.originStatement = originStatement;
exprIdGenerator = ExprId.createGenerator(initialId);
this.securityDependencyContext = new SecurityDependencyContext(connectContext);
if (connectContext != null && connectContext.getSessionVariable() != null) {
if (CacheAnalyzer.canUseSqlCache(connectContext.getSessionVariable())) {
// cannot set the queryId here because the queryId for the current query is set
Expand Down Expand Up @@ -692,6 +694,10 @@ public Optional<SqlCacheContext> getSqlCacheContext() {
return Optional.ofNullable(sqlCacheContext);
}

public SecurityDependencyContext getSecurityDependencyContext() {
return securityDependencyContext;
}

public boolean isDpHyp() {
return isDpHyp;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import org.apache.doris.common.DdlException;
import org.apache.doris.common.Pair;
import org.apache.doris.common.util.Util;
import org.apache.doris.mysql.MysqlCommand;
import org.apache.doris.nereids.CascadesContext;
import org.apache.doris.nereids.SqlCacheContext;
import org.apache.doris.nereids.StatementContext;
Expand Down Expand Up @@ -92,6 +91,7 @@
import org.apache.doris.nereids.trees.expressions.typecoercion.ImplicitCastInputTypes;
import org.apache.doris.nereids.trees.plans.PlaceholderId;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
import org.apache.doris.nereids.types.ArrayType;
Expand Down Expand Up @@ -920,21 +920,34 @@ public Expression visitPlaceholder(Placeholder placeholder, ExpressionRewriteCon
return visit(realExpr, context);
}

// Register prepared statement placeholder id to related slot in comparison predicate.
// Used to replace expression in ShortCircuit plan
// Register point-query filter placeholders so cached conjuncts can be rebound on EXECUTE.
// Restrict this registry to LogicalFilter: placeholders in projections or other plan nodes
// cannot be updated by the short-circuit executor and must keep the statement on the normal path.
private void registerPlaceholderIdToSlot(ComparisonPredicate cp,
ExpressionRewriteContext context, Expression left, Expression right) {
if (ConnectContext.get() != null
&& ConnectContext.get().getCommand() == MysqlCommand.COM_STMT_EXECUTE) {
// Used to replace expression in ShortCircuit plan
if (cp.right() instanceof Placeholder && left instanceof SlotReference) {
PlaceholderId id = ((Placeholder) cp.right()).getPlaceholderId();
context.cascadesContext.getStatementContext().getIdToComparisonSlot().put(id, (SlotReference) left);
} else if (cp.left() instanceof Placeholder && right instanceof SlotReference) {
PlaceholderId id = ((Placeholder) cp.left()).getPlaceholderId();
context.cascadesContext.getStatementContext().getIdToComparisonSlot().put(id, (SlotReference) right);
}
if (context == null || !(currentPlan instanceof LogicalFilter)) {
return;
}
SlotReference leftSlot = extractInjectiveCastSlot(left);
SlotReference rightSlot = extractInjectiveCastSlot(right);
if (cp.right() instanceof Placeholder && leftSlot != null) {
PlaceholderId id = ((Placeholder) cp.right()).getPlaceholderId();
context.cascadesContext.getStatementContext().getIdToComparisonSlot().put(id, leftSlot);
} else if (cp.left() instanceof Placeholder && rightSlot != null) {
PlaceholderId id = ((Placeholder) cp.left()).getPlaceholderId();
context.cascadesContext.getStatementContext().getIdToComparisonSlot().put(id, rightSlot);
}
}

private SlotReference extractInjectiveCastSlot(Expression expression) {
if (expression instanceof SlotReference) {
return (SlotReference) expression;
}
if (expression instanceof Cast && expression.child(0) instanceof SlotReference
&& expression.child(0).getDataType().isInjectiveCastTo(expression.getDataType())) {
return (SlotReference) expression.child(0);
}
return null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ private void checkColumnPrivileges(TableIf table, Set<String> usedColumns) {
throw new AnalysisException(e.getMessage(), e);
}
StatementContext statementContext = cascadesContext.getStatementContext();
statementContext.getSecurityDependencyContext().addCheckedPrivilege(table, usedColumns);
Optional<SqlCacheContext> sqlCacheContext = statementContext.getSqlCacheContext();
if (sqlCacheContext.isPresent()) {
sqlCacheContext.get().addCheckPrivilegeTablesOrViews(table, usedColumns);
Expand Down
Loading
Loading