/*
* 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.ranger.plugin.model.validation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ranger.plugin.model.RangerPolicy;
import org.apache.ranger.plugin.model.RangerServiceDef;
import org.apache.ranger.plugin.model.RangerServiceDef.RangerResourceDef;
import com.google.common.collect.Lists;
import org.apache.ranger.plugin.resourcematcher.RangerAbstractResourceMatcher;
import org.apache.ranger.plugin.resourcematcher.RangerPathResourceMatcher;
public class RangerServiceDefHelper {
private static final Log LOG = LogFactory.getLog(RangerServiceDefHelper.class);
static final Map<String, Delegate> _Cache = new ConcurrentHashMap<>();
final Delegate _delegate;
static public RangerServiceDef getServiceDefForPolicyFiltering(RangerServiceDef serviceDef) {
List<RangerResourceDef> modifiedResourceDefs = new ArrayList<RangerResourceDef>();
for (RangerResourceDef resourceDef : serviceDef.getResources()) {
final RangerResourceDef modifiedResourceDef;
String matcherClassName = resourceDef.getMatcher();
if (RangerPathResourceMatcher.class.getName().equals(matcherClassName)) {
Map<String, String> modifiedMatcherOptions = new HashMap<String, String>(resourceDef.getMatcherOptions());
modifiedMatcherOptions.put(RangerAbstractResourceMatcher.OPTION_WILD_CARD, "false");
modifiedResourceDef = new RangerResourceDef(resourceDef);
modifiedResourceDef.setMatcherOptions(modifiedMatcherOptions);
modifiedResourceDef.setRecursiveSupported(false);
} else {
modifiedResourceDef = resourceDef;
}
modifiedResourceDefs.add(modifiedResourceDef);
}
return new RangerServiceDef(serviceDef.getName(), serviceDef.getImplClass(), serviceDef.getLabel(),
serviceDef.getDescription(), serviceDef.getOptions(), serviceDef.getConfigs(), modifiedResourceDefs, serviceDef.getAccessTypes(),
serviceDef.getPolicyConditions(), serviceDef.getContextEnrichers(), serviceDef.getEnums());
}
public static Map<String, String> getFilterResourcesForAncestorPolicyFiltering(RangerServiceDef serviceDef, Map<String, String> filterResources) {
Map<String, String> ret = null;
for (RangerResourceDef resourceDef : serviceDef.getResources()) {
String matcherClassName = resourceDef.getMatcher();
if (RangerPathResourceMatcher.class.getName().equals(matcherClassName)) {
String resourceDefName = resourceDef.getName();
final Map<String, String> resourceMatcherOptions = resourceDef.getMatcherOptions();
String delimiter = resourceMatcherOptions.get(RangerPathResourceMatcher.OPTION_PATH_SEPARATOR);
if (StringUtils.isBlank(delimiter)) {
delimiter = Character.toString(RangerPathResourceMatcher.DEFAULT_PATH_SEPARATOR_CHAR);
}
String resourceValue = filterResources.get(resourceDefName);
if (StringUtils.isNotBlank(resourceValue)) {
if (!resourceValue.endsWith(delimiter)) {
resourceValue += delimiter;
}
resourceValue += RangerAbstractResourceMatcher.WILDCARD_ASTERISK;
if (ret == null) {
ret = new HashMap<String, String>();
}
ret.put(resourceDefName, resourceValue);
}
}
}
return ret;
}
public RangerServiceDefHelper(RangerServiceDef serviceDef) {
this(serviceDef, true);
}
/**
* Intended for use when serviceDef object is not-trusted, e.g. when service-def is being created or updated.
* @param serviceDef
* @param useCache
*/
public RangerServiceDefHelper(RangerServiceDef serviceDef, boolean useCache) {
// NOTE: we assume serviceDef, its name and update time are can never by null.
if(LOG.isDebugEnabled()) {
LOG.debug(String.format("==> RangerPolicyValidator.isValidResourceNames(%s)", serviceDef));
}
String serviceName = serviceDef.getName();
Date serviceDefFreshnessDate = serviceDef.getUpdateTime();
Delegate delegate = null;
if (useCache && _Cache.containsKey(serviceName)) {
LOG.debug("RangerServiceDefHelper(): found delegate in cache with matching serviceName. Need to check date");
Delegate that = _Cache.get(serviceName);
if (Objects.equals(that.getServiceFreshnessDate(), serviceDefFreshnessDate)) {
delegate = that;
LOG.debug("RangerServiceDefHelper(): cached delegate matched in date, too! Will use it now.");
} else {
LOG.debug("RangerServiceDefHelper(): cached delegate date mismatch!");
}
}
if (delegate == null) { // either not found in cache or date didn't match
delegate = new Delegate(serviceDef);
if (useCache) {
LOG.debug("RangerServiceDefHelper(): Created new delegate and put in delegate cache!");
_Cache.put(serviceName, delegate);
}
}
_delegate = delegate;
}
/**
* for a resource definition as follows:
*
* /-> E -> F
* A -> B -> C -> D
* \-> G -> H
*
* It would return a set with following ordered entries in it
* { [A B C D], [A E F], [A B G H] }
*
* @return
*/
public Set<List<RangerResourceDef>> getResourceHierarchies(Integer policyType) {
return _delegate.getResourceHierarchies(policyType);
}
public Set<List<RangerResourceDef>> getResourceHierarchies(Integer policyType, Collection<String> keys) {
Set<List<RangerResourceDef>> ret = new HashSet<List<RangerResourceDef>>();
for (List<RangerResourceDef> hierarchy : getResourceHierarchies(policyType)) {
if (getAllResourceNames(hierarchy).containsAll(keys)) {
ret.add(hierarchy);
}
}
return ret;
}
public Set<String> getMandatoryResourceNames(List<RangerResourceDef> hierarchy) {
Set<String> result = new HashSet<String>(hierarchy.size());
for (RangerResourceDef resourceDef : hierarchy) {
if (Boolean.TRUE.equals(resourceDef.getMandatory())) {
result.add(resourceDef.getName());
}
}
return result;
}
/**
* Set view of a hierarchy's resource names for efficient searching
* @param hierarchy
* @return
*/
public Set<String> getAllResourceNames(List<RangerResourceDef> hierarchy) {
Set<String> result = new HashSet<String>(hierarchy.size());
for (RangerResourceDef resourceDef : hierarchy) {
result.add(resourceDef.getName());
}
return result;
}
/**
* Resources names matching the order of list of resource defs passed in.
* @param hierarchy
* @return
*/
public List<String> getAllResourceNamesOrdered(List<RangerResourceDef> hierarchy) {
List<String> result = new ArrayList<String>(hierarchy.size());
for (RangerResourceDef resourceDef : hierarchy) {
result.add(resourceDef.getName());
}
return result;
}
public boolean isResourceGraphValid() {
return _delegate.isResourceGraphValid();
}
/**
* Not designed for public access. Package level only for testability.
*/
static class Delegate {
final Map<Integer, Set<List<RangerResourceDef>>> _hierarchies = new HashMap<>();
final Date _serviceDefFreshnessDate;
final String _serviceName;
final boolean _valid;
final static Set<List<RangerResourceDef>> EMPTY_RESOURCE_HIERARCHY = Collections.unmodifiableSet(new HashSet<List<RangerResourceDef>>());
public Delegate(RangerServiceDef serviceDef) {
// NOTE: we assume serviceDef, its name and update time are can never by null.
_serviceName = serviceDef.getName();
_serviceDefFreshnessDate = serviceDef.getUpdateTime();
boolean isValid = true;
for(Integer policyType : RangerPolicy.POLICY_TYPES) {
List<RangerResourceDef> resources = getResourceDefs(serviceDef, policyType);
DirectedGraph graph = createGraph(resources);
if(graph != null) {
if (isValid(graph)) {
Set<List<String>> hierarchies = getHierarchies(graph);
_hierarchies.put(policyType, Collections.unmodifiableSet(convertHierarchies(hierarchies, getResourcesAsMap(resources))));
} else {
isValid = false;
_hierarchies.put(policyType, EMPTY_RESOURCE_HIERARCHY);
}
} else {
_hierarchies.put(policyType, EMPTY_RESOURCE_HIERARCHY);
}
}
_valid = isValid;
if (LOG.isDebugEnabled()) {
String message = String.format("Found [%d] resource hierarchies for service [%s] update-date[%s]: %s", _hierarchies.size(), _serviceName,
_serviceDefFreshnessDate == null ? null : _serviceDefFreshnessDate.toString(), _hierarchies);
LOG.debug(message);
}
}
public Set<List<RangerResourceDef>> getResourceHierarchies(Integer policyType) {
if(policyType == null) {
policyType = RangerPolicy.POLICY_TYPE_ACCESS;
}
Set<List<RangerResourceDef>> ret = _hierarchies.get(policyType);
if(ret == null) {
ret = EMPTY_RESOURCE_HIERARCHY;
}
return ret;
}
public String getServiceName() {
return _serviceName;
}
public Date getServiceFreshnessDate() {
return _serviceDefFreshnessDate;
}
public boolean isResourceGraphValid() {
return _valid;
}
/**
* Builds a directed graph where each resource is node and arc goes from parent level to child level
*
* @param resourceDefs
* @return
*/
DirectedGraph createGraph(List<RangerResourceDef> resourceDefs) {
DirectedGraph graph = null;
if(CollectionUtils.isNotEmpty(resourceDefs)) {
graph = new DirectedGraph();
for (RangerResourceDef resourceDef : resourceDefs) {
String name = resourceDef.getName();
graph.add(name);
String parent = resourceDef.getParent();
if (StringUtils.isNotEmpty(parent)) {
graph.addArc(parent, name);
}
}
}
if (LOG.isDebugEnabled()) {
LOG.debug("Created graph for resources: " + graph);
}
return graph;
}
List<RangerResourceDef> getResourceDefs(RangerServiceDef serviceDef, Integer policyType) {
final List<RangerResourceDef> resourceDefs;
if(policyType == null || policyType == RangerPolicy.POLICY_TYPE_ACCESS) {
resourceDefs = serviceDef.getResources();
} else if(policyType == RangerPolicy.POLICY_TYPE_DATAMASK) {
if(serviceDef.getDataMaskDef() != null) {
resourceDefs = serviceDef.getDataMaskDef().getResources();
} else {
resourceDefs = null;
}
} else if(policyType == RangerPolicy.POLICY_TYPE_ROWFILTER) {
if(serviceDef.getRowFilterDef() != null) {
resourceDefs = serviceDef.getRowFilterDef().getResources();
} else {
resourceDefs = null;
}
} else { // unknown policyType; use all resources
resourceDefs = serviceDef.getResources();
}
return resourceDefs;
}
/**
* A valid resource graph is a forest, i.e. a disjoint union of trees. In our case, given that node can have only one "parent" node, we can detect this validity simply by ensuring that
* the resource graph has:
* - at least one sink AND
* - and least one source.
*
* A more direct method would have been ensure that the resulting graph does not have any cycles.
*
* @param graph
*
* @return
*/
boolean isValid(DirectedGraph graph) {
return !graph.getSources().isEmpty() && !graph.getSinks().isEmpty();
}
/**
* Returns all valid resource hierarchies for the configured resource-defs. Behavior is undefined if it is called on and invalid graph. Use <code>isValid</code> to check validation first.
*
* @param graph
* @return
*/
Set<List<String>> getHierarchies(DirectedGraph graph) {
Set<List<String>> hierarchies = new HashSet<>();
Set<String> sources = graph.getSources();
Set<String> sinks = graph.getSinks();
for (String source : sources) {
/*
* A disconnected node, i.e. one that does not have any arc coming into or out of it is a hierarchy in itself!
* A source by definition does not have any arcs coming into it. So if it also doesn't have any neighbors then we know
* it is a disconnected node.
*/
if (!graph.hasNeighbors(source)) {
List<String> path = Lists.newArrayList(source);
hierarchies.add(path);
} else {
for (String sink : sinks) {
List<String> path = graph.getAPath(source, sink, new HashSet<String>());
if (!path.isEmpty()) {
hierarchies.add(path);
}
}
}
}
return hierarchies;
}
Set<List<RangerResourceDef>> convertHierarchies(Set<List<String>> hierarchies, Map<String, RangerResourceDef> resourceMap) {
Set<List<RangerResourceDef>> result = new HashSet<List<RangerResourceDef>>(hierarchies.size());
for (List<String> hierarchy : hierarchies) {
List<RangerResourceDef> resourceList = new ArrayList<RangerResourceDef>(hierarchy.size());
for (String name : hierarchy) {
RangerResourceDef def = resourceMap.get(name);
resourceList.add(def);
}
result.add(resourceList);
}
return result;
}
/**
* Converts resource list to resource map for efficient querying
*
* @param resourceList
* @return
*/
Map<String, RangerResourceDef> getResourcesAsMap(List<RangerResourceDef> resourceList) {
Map<String, RangerResourceDef> map = new HashMap<String, RangerResourceDef>(resourceList.size());
for (RangerResourceDef resourceDef : resourceList) {
map.put(resourceDef.getName(), resourceDef);
}
return map;
}
}
/**
* Limited DAG implementation to analyze resource graph for a service. Not designed for public access. Package level only for testability.
*/
static class DirectedGraph {
Map<String, Set<String>> _nodes = new HashMap<>();
/**
* Add a node to the graph
*
* @param node
*/
void add(String node) {
if (node == null) {
throw new IllegalArgumentException("Node can't be null!");
} else if (!_nodes.containsKey(node)) { // don't mess with a node's neighbors if it already exists in the graph
_nodes.put(node, new HashSet<String>());
}
}
/**
* Connects node "from" to node "to". Being a directed graph, after this call "to" will be in the list of neighbor's of "from". While the converse need not be true.
*
* @param from
* @param to
*/
void addArc(String from, String to) {
// connecting two nodes, implicitly adds nodes to the graph if they aren't already in it
if (!_nodes.containsKey(from)) {
add(from);
}
if (!_nodes.containsKey(to)) {
add(to);
}
_nodes.get(from).add(to);
}
/**
* Returns true if "to" is in the list of neighbors of "from"
*
* @param from
* @param to
* @return
*/
boolean hasArc(String from, String to) {
return _nodes.containsKey(from) && _nodes.containsKey(to) && _nodes.get(from).contains(to);
}
/**
* Returns true if the node "from" has any neighbor.
* @param from
* @return
*/
boolean hasNeighbors(String from) {
return _nodes.containsKey(from) && !_nodes.get(from).isEmpty();
}
/**
* Return the set of nodes with in degree of 0, i.e. those that are not in any other nodes' list of neighbors
*
* @return
*/
Set<String> getSources() {
Set<String> sources = new HashSet<>(_nodes.keySet());
for (Map.Entry<String, Set<String>> entry : _nodes.entrySet()) {
Set<String> nbrs = entry.getValue(); // can never be null
sources.removeAll(nbrs); // A source in a DAG can't be a neighbor of any other node
}
if (LOG.isDebugEnabled()) {
LOG.debug("Returning sources: " + sources);
}
return sources;
}
/**
* Returns the set of nodes with out-degree of 0, i.e. those nodes whose list of neighbors is empty
*
* @return
*/
Set<String> getSinks() {
Set<String> sinks = new HashSet<>();
for (Map.Entry<String, Set<String>> entry : _nodes.entrySet()) {
Set<String> nbrs = entry.getValue();
if (nbrs.isEmpty()) { // A sink in a DAG doesn't have any neighbor
String node = entry.getKey();
sinks.add(node);
}
}
if (LOG.isDebugEnabled()) {
LOG.debug("Returning sinks: " + sinks);
}
return sinks;
}
/**
* Attempts to do a depth first traversal of a graph and returns the resulting path. Note that there could be several paths that connect node "from" to node "to".
*
* @param from
* @param to
* @return
*/
List<String> getAPath(String from, String to, Set<String> alreadyVisited) {
List<String> path = new ArrayList<String>(_nodes.size());
if (_nodes.containsKey(from) && _nodes.containsKey(to)) { // one can never reach non-existent nodes
if (hasArc(from, to)) {
path.add(from);
path.add(to);
} else {
alreadyVisited.add(from);
Set<String> nbrs = _nodes.get(from);
for (String nbr : nbrs) {
if (!alreadyVisited.contains(nbr)) {
List<String> subPath = getAPath(nbr, to, alreadyVisited);
if (!subPath.isEmpty()) {
path.add(from);
path.addAll(subPath);
}
}
}
}
}
return path;
}
@Override
public boolean equals(Object object) {
if (object == this) {
return true;
}
if (object == null || object.getClass() != this.getClass()) {
return false;
}
DirectedGraph that = (DirectedGraph)object;
return Objects.equals(this._nodes, that._nodes);
}
@Override
public int hashCode() {
return Objects.hashCode(_nodes);
}
@Override
public String toString() {
return "_nodes=" + Objects.toString(_nodes);
}
}
}