CreatePolicyCommand.java
// 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.trees.plans.commands;
import org.apache.doris.analysis.Expr;
import org.apache.doris.analysis.SlotRef;
import org.apache.doris.analysis.StmtType;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.TableIf;
import org.apache.doris.catalog.info.TableNameInfo;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.nereids.analyzer.UnboundSlot;
import org.apache.doris.nereids.glue.translator.ExpressionTranslator;
import org.apache.doris.nereids.glue.translator.PlanTranslatorContext;
import org.apache.doris.nereids.parser.NereidsParser;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.plans.PlanType;
import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
import org.apache.doris.policy.FilterType;
import org.apache.doris.policy.Policy;
import org.apache.doris.policy.PolicyTypeEnum;
import org.apache.doris.policy.RowPolicy;
import org.apache.doris.policy.StoragePolicy;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.SqlModeHelper;
import org.apache.doris.qe.StmtExecutor;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Map;
import java.util.Optional;
/**
* Create policy command use for row policy and storage policy.
*/
public class CreatePolicyCommand extends Command implements ForwardWithSync {
private static final Logger LOG = LogManager.getLogger(CreatePolicyCommand.class);
private final PolicyTypeEnum policyType;
private final String policyName;
private final boolean ifNotExists;
private final TableNameInfo tableNameInfo;
private final Optional<FilterType> filterType;
private final UserIdentity user;
private final String roleName;
private final Optional<Expression> wherePredicate;
// The row predicate exactly as written, null for a storage policy.
private final String wherePredicateSql;
private final Map<String, String> properties;
/**
* The predicate as it will be understood, which is what this statement accepts, stores and shows.
*
* <p>Not the one the parser handed over: that one was read under the creator's {@code sql_mode}, and the
* text is re-read under {@link SqlModeHelper#MODE_FOR_POLICY_TEXT} every time the policy is applied to a
* query. Two bits of {@code sql_mode} change what the same text means, so accepting it under one mode and
* enforcing it under another lets a policy mean one thing in {@code SHOW ROW POLICY} and another in the
* queries it restricts - or, with {@code NO_BACKSLASH_ESCAPES}, be accepted here and fail to parse on
* every query it governs from then on. Settled once, in {@link #validate}.
*/
private Optional<Expression> policyPredicate = Optional.empty();
/**
* What the creator wrote that this statement is not storing, when the two readings differ.
*
* <p>Reported to the client rather than only to {@code fe.log}: the statement succeeds, and the one thing
* it drops is the creator's own reading of their own text. Learning that from a query behaving in a way
* {@code SHOW ROW POLICY} does not explain is the failure this exists to prevent.
*/
private String droppedReading = null;
/**
* ctor of this command.
*/
public CreatePolicyCommand(PolicyTypeEnum policyType, String policyName, boolean ifNotExists,
TableNameInfo tableNameInfo, Optional<FilterType> filterType, UserIdentity user, String roleName,
Optional<Expression> wherePredicate, String wherePredicateSql, Map<String, String> properties) {
super(PlanType.CREATE_POLICY_COMMAND);
this.policyType = policyType;
this.policyName = policyName;
this.ifNotExists = ifNotExists;
this.tableNameInfo = tableNameInfo;
this.filterType = filterType;
this.user = user;
this.roleName = roleName;
this.wherePredicate = wherePredicate;
this.wherePredicateSql = wherePredicateSql;
this.properties = properties;
}
public Optional<Expression> getWherePredicate() {
return wherePredicate;
}
public String getWherePredicateSql() {
return wherePredicateSql;
}
public Map<String, String> getProperties() {
return properties;
}
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
return visitor.visitCreatePolicyCommand(this, context);
}
@Override
public void run(ConnectContext ctx, StmtExecutor executor) throws Exception {
validate(ctx);
Policy policy = createPolicy(ctx, executor);
Env.getCurrentEnv().getPolicyMgr().createPolicy(policy, ifNotExists);
if (droppedReading != null) {
// Set here rather than left to the executor's plain setOk: this is the only chance to tell the
// client that the policy it just created does not say what its session read the text as. Counted as
// a warning so a client that only looks at the counters still sees that something was reported.
ctx.getState().setOk(0, 1, "this policy's predicate is stored as ["
+ policyPredicate.get().toSql() + "], which is how it is read on every query it restricts."
+ " Your session's sql_mode reads the same text as [" + droppedReading + "]; policy text is"
+ " read under the default sql_mode, not the session's");
}
}
@Override
public StmtType stmtType() {
return StmtType.CREATE;
}
private void validate(ConnectContext ctx) throws AnalysisException {
switch (policyType) {
case STORAGE:
if (!Config.enable_storage_policy) {
throw new AnalysisException("storage policy feature is disabled by default. "
+ "Enable it by setting 'enable_storage_policy=true' in fe.conf");
}
// check auth
// check if can create policy and use storage_resource
if (!Env.getCurrentEnv().getAccessManager()
.checkGlobalPriv(ConnectContext.get(), PrivPredicate.ADMIN)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
PrivPredicate.ADMIN.getPrivs().toString());
}
break;
case ROW:
default:
// check auth
if (!Env.getCurrentEnv().getAccessManager()
.checkGlobalPriv(ConnectContext.get(), PrivPredicate.GRANT)) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR,
PrivPredicate.GRANT.getPrivs().toString());
}
tableNameInfo.analyze(ctx.getNameSpaceContext());
if (user != null) {
user.analyze();
if (user.isRootUser() || user.isAdminUser()) {
throw new AnalysisException("not allow add row policy for system user");
}
if (!Env.getCurrentEnv().getAuth().doesUserExist(user)) {
throw new AnalysisException("user not exist: " + user);
}
}
if (!StringUtils.isEmpty(roleName)) {
if (!Env.getCurrentEnv().getAuth().doesRoleExist(roleName)) {
throw new AnalysisException("role not exist: " + roleName);
}
}
if (!wherePredicate.isPresent()) {
throw new AnalysisException("wherePredicate can not be null");
}
policyPredicate = Optional.of(predicateUnderPolicyMode());
TableIf tableIf = Env.getCurrentEnv().getCatalogMgr()
.getCatalogOrAnalysisException(tableNameInfo.getCtl())
.getDbOrAnalysisException(tableNameInfo.getDb())
.getTableOrAnalysisException(tableNameInfo.getTbl());
policyPredicate.get().foreach(expr -> {
if (expr instanceof UnboundSlot) {
UnboundSlot slot = (UnboundSlot) expr;
if (tableIf.getColumn(slot.getName()) == null) {
throw new org.apache.doris.nereids.exceptions.AnalysisException(
"column not exist: " + slot.getName());
}
}
});
}
}
/**
* The stored predicate text read under the mode a security policy's text is read under.
*
* <p>Re-parsed rather than reused because {@code sql_mode} is a session variable any account may set with
* no privilege at all, and the account creating a row policy is not the one it restricts: taking the
* creator's reading of {@code ||} or of a backslash and enforcing a different one is a policy that does not
* say what it was accepted as saying. Text the fixed mode cannot read is refused here, which is the whole
* point of doing this at creation time - the alternative is a policy that stores fine and then fails every
* single query it governs.
*/
private Expression predicateUnderPolicyMode() throws AnalysisException {
if (StringUtils.isEmpty(wherePredicateSql)) {
// Nothing to re-read: no statement text was recorded, so the parse that produced the predicate is
// all there is. Not reachable from CREATE ROW POLICY, which always records it.
return wherePredicate.get();
}
try {
Expression underPolicyMode = SqlModeHelper.withSqlMode(SqlModeHelper.MODE_FOR_POLICY_TEXT,
() -> new NereidsParser().parseExpression(wherePredicateSql));
if (!underPolicyMode.equals(wherePredicate.get())) {
// Both modes read the text, and they read it differently. This statement is the only place
// that holds both readings, so it is the only place that can say so; the creator's is the one
// being dropped, and the operator would otherwise learn that only from a query behaving in a
// way SHOW ROW POLICY does not explain. Also handed back to the client, in run().
droppedReading = wherePredicate.get().toSql();
LOG.warn("row policy {} on {} was written under a sql_mode that reads it as [{}], and is"
+ " stored as [{}], which is how it is read on every query it restricts:"
+ " predicate text [{}]", policyName, tableNameInfo,
wherePredicate.get().toSql(), underPolicyMode.toSql(), wherePredicateSql);
}
return underPolicyMode;
} catch (Exception e) {
throw new AnalysisException("the predicate of a row policy is read under the default sql_mode"
+ " rather than this session's, because it is read again on every query the policy"
+ " restricts, on the thread of the very user it restricts. Under that mode this predicate"
+ " cannot be parsed: " + wherePredicateSql, e);
}
}
private Policy createPolicy(ConnectContext ctx, StmtExecutor executor) throws AnalysisException {
long policyId = Env.getCurrentEnv().getNextId();
switch (policyType) {
case STORAGE:
StoragePolicy storagePolicy = new StoragePolicy(policyId, policyName);
storagePolicy.init(properties, ifNotExists);
return storagePolicy;
case ROW:
// The predicate text goes in with the policy: this parse already has it, and recovering it
// later means parsing the request text again - a request that may hold several statements.
// The predicate stored alongside it is the one read under the policy-text mode, so that what
// SHOW ROW POLICY renders is what the queries this policy restricts will actually be filtered
// by. See predicateUnderPolicyMode().
return new RowPolicy(policyId, policyName, tableNameInfo.getCtl(),
tableNameInfo.getDb(), tableNameInfo.getTbl(), user, roleName,
executor.getOriginStmt().originStmt, executor.getOriginStmt().idx, filterType.get(),
policyPredicate.get(), wherePredicateSql);
default:
throw new AnalysisException("Unknown policy type: " + policyType);
}
}
private static class ExpressionToExpr extends ExpressionTranslator {
@Override
public Expr visitUnboundSlot(UnboundSlot unboundSlot, PlanTranslatorContext context) {
String inputCol = unboundSlot.getName();
return new SlotRef(null, inputCol);
}
}
}