/*
* JBoss, Home of Professional Open Source.
* See the COPYRIGHT.txt file distributed with this work for information
* regarding copyright ownership. Some portions may be licensed
* to Red Hat, Inc. under one or more contributor license agreements.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*/
package org.teiid.query.optimizer.relational.rules;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.teiid.api.exception.query.QueryMetadataException;
import org.teiid.api.exception.query.QueryPlannerException;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.TeiidProcessingException;
import org.teiid.query.analysis.AnalysisRecord;
import org.teiid.query.metadata.QueryMetadataInterface;
import org.teiid.query.optimizer.capabilities.CapabilitiesFinder;
import org.teiid.query.optimizer.relational.ColumnMaskingHelper;
import org.teiid.query.optimizer.relational.OptimizerRule;
import org.teiid.query.optimizer.relational.RelationalPlanner;
import org.teiid.query.optimizer.relational.RowBasedSecurityHelper;
import org.teiid.query.optimizer.relational.RuleStack;
import org.teiid.query.optimizer.relational.plantree.NodeConstants;
import org.teiid.query.optimizer.relational.plantree.NodeConstants.Info;
import org.teiid.query.optimizer.relational.plantree.NodeEditor;
import org.teiid.query.optimizer.relational.plantree.PlanNode;
import org.teiid.query.resolver.util.ResolverUtil;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.lang.Criteria;
import org.teiid.query.sql.symbol.ElementSymbol;
import org.teiid.query.sql.symbol.Expression;
import org.teiid.query.sql.symbol.GroupSymbol;
import org.teiid.query.sql.symbol.WindowFunction;
import org.teiid.query.sql.util.SymbolMap;
import org.teiid.query.sql.visitor.AggregateSymbolCollectorVisitor;
import org.teiid.query.sql.visitor.GroupsUsedByElementsVisitor;
import org.teiid.query.util.CommandContext;
/**
* Applies row/column security to a non-update plan
*
* Should be run after rule assign output elements
*/
public class RuleApplySecurity implements OptimizerRule {
@Override
public PlanNode execute(PlanNode plan, QueryMetadataInterface metadata,
CapabilitiesFinder capabilitiesFinder, RuleStack rules,
AnalysisRecord analysisRecord, CommandContext context)
throws QueryPlannerException, QueryMetadataException,
TeiidComponentException {
try {
for (PlanNode sourceNode : NodeEditor.findAllNodes(plan, NodeConstants.Types.SOURCE)) {
GroupSymbol group = sourceNode.getGroups().iterator().next();
if (!RowBasedSecurityHelper.applyRowSecurity(metadata, group, context)) {
continue;
}
List<ElementSymbol> cols = null;
Command command = (Command) sourceNode.getProperty(Info.VIRTUAL_COMMAND);
if (group.isProcedure()) {
if (command == null) {
continue; //proc relational, will instead apply at the proc level
}
if (cols == null) {
cols = (List)command.getProjectedSymbols();
}
} else if (command != null && !command.returnsResultSet()) {
continue; //should be handled in the planner
}
if (cols == null) {
cols = ResolverUtil.resolveElementsInGroup(group, metadata);
}
//apply masks first
List<? extends Expression> masked = ColumnMaskingHelper.maskColumns(cols, group, metadata, context);
Map<ElementSymbol, Expression> mapping = null;
//TODO: we don't actually allow window function masks yet becuase they won't pass
//validation. but if we do, we need to check for them here
List<WindowFunction> windowFunctions = new ArrayList<WindowFunction>(2);
for (int i = 0; i < masked.size(); i++) {
Expression maskedCol = masked.get(i);
AggregateSymbolCollectorVisitor.getAggregates(maskedCol, null, null, null, windowFunctions, null);
if (maskedCol.equals(cols.get(i))) {
continue;
}
if (mapping == null) {
mapping = new HashMap<ElementSymbol, Expression>();
}
mapping.put(cols.get(i), maskedCol);
}
PlanNode parentJoin = NodeEditor.findParent(sourceNode.getParent(), NodeConstants.Types.JOIN, NodeConstants.Types.SOURCE);
if (mapping != null) {
//some element symbol has been replaced
PlanNode project = null;
if (group.isProcedure()) {
project = NodeEditor.findParent(sourceNode, NodeConstants.Types.PROJECT);
project.setProperty(NodeConstants.Info.PROJECT_COLS, masked);
}
if (windowFunctions.isEmpty() && RuleMergeVirtual.checkProjectedSymbols(group, parentJoin, metadata, masked, Collections.singleton(group), true)){
if (!group.isProcedure()) {
//just upwardly project - TODO: we could also handle some subquery simple projection situations here
FrameUtil.convertFrame(sourceNode.getParent(), group, Collections.singleton(group), mapping, metadata);
}
} else {
if (!group.isProcedure()) {
project = RelationalPlanner.createProjectNode(masked);
}
rules.getPlanner().planSubqueries(null, sourceNode.getGroups(), project, project.getSubqueryContainers(), true);
project.addGroups(GroupsUsedByElementsVisitor.getGroups(project.getCorrelatedReferenceElements()));
if (!group.isProcedure()) {
//we need to insert a view to give a single place to evaluate the subqueries
PlanNode root = sourceNode;
if (sourceNode.getParent().getType() == NodeConstants.Types.ACCESS) {
root = sourceNode.getParent();
}
root.addAsParent(project);
addView(metadata, context, group, cols, masked, project);
parentJoin = null;
}
}
if (!windowFunctions.isEmpty() && project != null) {
project.setProperty(Info.HAS_WINDOW_FUNCTIONS, true);
}
}
//logically filters are applied below masking
Criteria filter = RowBasedSecurityHelper.getRowBasedFilters(metadata, group, context, false);
if (filter == null) {
continue;
}
List<Criteria> crits = Criteria.separateCriteriaByAnd(filter);
PlanNode root = sourceNode;
if (sourceNode.getParent().getType() == NodeConstants.Types.ACCESS) {
root = sourceNode.getParent();
}
PlanNode parent = null;
for (Criteria crit : crits) {
PlanNode critNode = RelationalPlanner.createSelectNode(crit, false);
if (parent == null) {
parent = critNode;
}
rules.getPlanner().planSubqueries(null, sourceNode.getGroups(), critNode, critNode.getSubqueryContainers(), true);
critNode.addGroups(GroupsUsedByElementsVisitor.getGroups(critNode.getCorrelatedReferenceElements()));
root.addAsParent(critNode);
}
if (!RuleMergeVirtual.checkJoinCriteria(parent, group, parentJoin)) {
PlanNode project = RelationalPlanner.createProjectNode(cols);
parent.addAsParent(project);
//a view is needed to keep the logical placement of the criteria
addView(metadata, context, group, cols, cols, project);
}
}
} catch (TeiidProcessingException e) {
throw new QueryPlannerException(e);
}
return plan;
}
private void addView(QueryMetadataInterface metadata,
CommandContext context, GroupSymbol group,
List<ElementSymbol> cols, List<? extends Expression> old,
PlanNode viewRoot) throws TeiidComponentException,
QueryMetadataException, QueryPlannerException {
GroupSymbol securityVeiw = new GroupSymbol("sec"); //$NON-NLS-1$
Set<String> groups = context.getGroups();
securityVeiw = RulePlaceAccess.recontextSymbol(securityVeiw, groups);
List<ElementSymbol> newCols = RulePushAggregates.defineNewGroup(securityVeiw, old, metadata);
PlanNode newSourceNode = RuleDecomposeJoin.createSource(securityVeiw, viewRoot, newCols);
Map<ElementSymbol, Expression> upperMapping = SymbolMap.createSymbolMap(cols, newCols).asMap();
FrameUtil.convertFrame(newSourceNode.getParent(), group, Collections.singleton(securityVeiw), upperMapping, metadata);
}
@Override
public String toString() {
return "ApplySecurity"; //$NON-NLS-1$
}
}