/*
* 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.resourcematcher;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOCase;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ranger.plugin.util.ServiceDefUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class RangerPathResourceMatcher extends RangerDefaultResourceMatcher {
private static final Log LOG = LogFactory.getLog(RangerPathResourceMatcher.class);
public static final String OPTION_PATH_SEPARATOR = "pathSeparatorChar";
public static final char DEFAULT_PATH_SEPARATOR_CHAR = org.apache.hadoop.fs.Path.SEPARATOR_CHAR;
private boolean policyIsRecursive;
private char pathSeparatorChar = '/';
@Override
public void init() {
if(LOG.isDebugEnabled()) {
LOG.debug("==> RangerPathResourceMatcher.init()");
}
Map<String, String> options = resourceDef == null ? null : resourceDef.getMatcherOptions();
policyIsRecursive = policyResource != null && policyResource.getIsRecursive();
pathSeparatorChar = ServiceDefUtil.getCharOption(options, OPTION_PATH_SEPARATOR, DEFAULT_PATH_SEPARATOR_CHAR);
super.init();
if(LOG.isDebugEnabled()) {
LOG.debug("<== RangerPathResourceMatcher.init()");
}
}
@Override
protected ResourceMatcherWrapper buildResourceMatchers() {
List<ResourceMatcher> resourceMatchers = new ArrayList<>();
boolean needsDynamicEval = false;
for (String policyValue : policyValues) {
if (optWildCard && policyIsRecursive) {
if (policyValue.charAt(policyValue.length() - 1) == pathSeparatorChar) {
policyValue += WILDCARD_ASTERISK;
}
}
ResourceMatcher matcher = getMatcher(policyValue);
if (matcher != null) {
if (matcher.isMatchAny()) {
resourceMatchers.clear();
break;
}
if (!needsDynamicEval && matcher.getNeedsDynamicEval()) {
needsDynamicEval = true;
}
resourceMatchers.add(matcher);
}
}
Collections.sort(resourceMatchers);
return CollectionUtils.isNotEmpty(resourceMatchers) ?
new ResourceMatcherWrapper(needsDynamicEval, resourceMatchers) : null;
}
@Override
ResourceMatcher getMatcher(String policyValue) {
if(! policyIsRecursive) {
return super.getMatcher(policyValue);
}
final int len = policyValue != null ? policyValue.length() : 0;
if (len == 0) {
return null;
}
// To ensure that when policyValue is single '*', ResourceMatcher created here returns true for isMatchAny()
if (optWildCard && WILDCARD_ASTERISK.equals(policyValue)) {
return new CaseInsensitiveStringMatcher("");
}
boolean isWildcardPresent = false;
if (optWildCard) {
for (int i = 0; i < len; i++) {
final char c = policyValue.charAt(i);
if (c == '?' || c == '*') {
isWildcardPresent = true;
break;
}
}
}
final ResourceMatcher ret;
if (isWildcardPresent) {
ret = optIgnoreCase ? new CaseInsensitiveRecursiveWildcardMatcher(policyValue, pathSeparatorChar)
: new CaseSensitiveRecursiveWildcardMatcher(policyValue, pathSeparatorChar);
} else {
ret = optIgnoreCase ? new CaseInsensitiveRecursiveMatcher(policyValue, pathSeparatorChar) : new CaseSensitiveRecursiveMatcher(policyValue, pathSeparatorChar);
}
if (optReplaceTokens) {
ret.setDelimiters(startDelimiterChar, endDelimiterChar, escapeChar, tokenPrefix);
}
return ret;
}
static boolean isRecursiveWildCardMatch(String pathToCheck, String wildcardPath, char pathSeparatorChar, IOCase caseSensitivity) {
boolean ret = false;
if (! StringUtils.isEmpty(pathToCheck)) {
String[] pathElements = StringUtils.split(pathToCheck, pathSeparatorChar);
if(! ArrayUtils.isEmpty(pathElements)) {
StringBuilder sb = new StringBuilder();
if(pathToCheck.charAt(0) == pathSeparatorChar) {
sb.append(pathSeparatorChar); // preserve the initial pathSeparatorChar
}
for(String p : pathElements) {
sb.append(p);
ret = FilenameUtils.wildcardMatch(sb.toString(), wildcardPath, caseSensitivity);
if (ret) {
break;
}
sb.append(pathSeparatorChar);
}
sb = null;
} else { // pathToCheck consists of only pathSeparatorChar
ret = FilenameUtils.wildcardMatch(pathToCheck, wildcardPath, caseSensitivity);
}
}
return ret;
}
public StringBuilder toString(StringBuilder sb) {
sb.append("RangerPathResourceMatcher={");
super.toString(sb);
sb.append("policyIsRecursive={").append(policyIsRecursive).append("} ");
sb.append("}");
return sb;
}
}
final class CaseSensitiveRecursiveWildcardMatcher extends ResourceMatcher {
private final char levelSeparatorChar;
CaseSensitiveRecursiveWildcardMatcher(String value, char levelSeparatorChar) {
super(value);
this.levelSeparatorChar = levelSeparatorChar;
}
@Override
boolean isMatch(String resourceValue, Map<String, Object> evalContext) {
return RangerPathResourceMatcher.isRecursiveWildCardMatch(resourceValue, getExpandedValue(evalContext), levelSeparatorChar, IOCase.SENSITIVE);
}
int getPriority() { return 7 + (getNeedsDynamicEval() ? DYNAMIC_EVALUATION_PENALTY : 0);}
}
final class CaseInsensitiveRecursiveWildcardMatcher extends ResourceMatcher {
private final char levelSeparatorChar;
CaseInsensitiveRecursiveWildcardMatcher(String value, char levelSeparatorChar) {
super(value);
this.levelSeparatorChar = levelSeparatorChar;
}
@Override
boolean isMatch(String resourceValue, Map<String, Object> evalContext) {
return RangerPathResourceMatcher.isRecursiveWildCardMatch(resourceValue, getExpandedValue(evalContext), levelSeparatorChar, IOCase.INSENSITIVE);
}
int getPriority() { return 8 + (getNeedsDynamicEval() ? DYNAMIC_EVALUATION_PENALTY : 0);}
}
abstract class RecursiveMatcher extends ResourceMatcher {
final char levelSeparatorChar;
String valueWithoutSeparator;
String valueWithSeparator;
RecursiveMatcher(String value, char levelSeparatorChar) {
super(value);
this.levelSeparatorChar = levelSeparatorChar;
}
String getStringToCompare(String policyValue) {
return (policyValue.lastIndexOf(levelSeparatorChar) == policyValue.length()-1) ?
policyValue.substring(0, policyValue.length()-1) : policyValue;
}
}
final class CaseSensitiveRecursiveMatcher extends RecursiveMatcher {
CaseSensitiveRecursiveMatcher(String value, char levelSeparatorChar) {
super(value, levelSeparatorChar);
}
@Override
boolean isMatch(String resourceValue, Map<String, Object> evalContext) {
final String noSeparator;
if (getNeedsDynamicEval()) {
noSeparator = getStringToCompare(getExpandedValue(evalContext));
} else {
if (valueWithoutSeparator == null) {
valueWithoutSeparator = getStringToCompare(value);
valueWithSeparator = valueWithoutSeparator + Character.toString(levelSeparatorChar);
}
noSeparator = valueWithoutSeparator;
}
boolean ret = StringUtils.equals(resourceValue, noSeparator);
if (!ret) {
final String withSeparator = getNeedsDynamicEval() ? noSeparator + Character.toString(levelSeparatorChar) : valueWithSeparator;
ret = StringUtils.startsWith(resourceValue, withSeparator);
}
return ret;
}
int getPriority() { return 7 + (getNeedsDynamicEval() ? DYNAMIC_EVALUATION_PENALTY : 0);}
}
final class CaseInsensitiveRecursiveMatcher extends RecursiveMatcher {
CaseInsensitiveRecursiveMatcher(String value, char levelSeparatorChar) {
super(value, levelSeparatorChar);
}
@Override
boolean isMatch(String resourceValue, Map<String, Object> evalContext) {
final String noSeparator;
if (getNeedsDynamicEval()) {
noSeparator = getStringToCompare(getExpandedValue(evalContext));
} else {
if (valueWithoutSeparator == null) {
valueWithoutSeparator = getStringToCompare(value);
valueWithSeparator = valueWithoutSeparator + Character.toString(levelSeparatorChar);
}
noSeparator = valueWithoutSeparator;
}
boolean ret = StringUtils.equalsIgnoreCase(resourceValue, noSeparator);
if (!ret) {
final String withSeparator = getNeedsDynamicEval() ? noSeparator + Character.toString(levelSeparatorChar) : valueWithSeparator;
ret = StringUtils.startsWithIgnoreCase(resourceValue, withSeparator);
}
return ret;
}
int getPriority() { return 8 + (getNeedsDynamicEval() ? DYNAMIC_EVALUATION_PENALTY : 0);}
}