| /* |
| * 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.jackrabbit.oak.security.authorization.restriction; |
| |
| import javax.jcr.security.AccessControlException; |
| |
| import com.google.common.base.Objects; |
| import org.apache.jackrabbit.oak.api.PropertyState; |
| import org.apache.jackrabbit.oak.api.Tree; |
| import org.apache.jackrabbit.oak.commons.PathUtils; |
| import org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionPattern; |
| import org.apache.jackrabbit.util.Text; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| /** |
| * {@code GlobPattern} defines a simplistic pattern matching. It consists |
| * of a mandatory (leading) path and an optional "glob" that may contain one or |
| * more wildcard characters ("{@code *}") according to the glob matching |
| * defined by {@link javax.jcr.Node#getNodes(String[])}. In contrast to that |
| * method the {@code GlobPattern} operates on path (not only names). |
| * <p> |
| * |
| * <p> |
| * Please note the following special cases: |
| * <pre> |
| * NodePath | Restriction | Matches |
| * ----------------------------------------------------------------------------- |
| * /foo | null | matches /foo and all children of /foo |
| * /foo | "" | matches /foo only |
| * </pre> |
| * </p> |
| * |
| * <p> |
| * Examples without wildcard char: |
| * <pre> |
| * NodePath = "/foo" |
| * Restriction | Matches |
| * ----------------------------------------------------------------------------- |
| * /cat | the node /foo/cat and all it's children |
| * /cat/ | the descendants of the node /foo/cat |
| * cat | the node /foocat and all it's children |
| * cat/ | all descendants of the node /foocat |
| * </pre> |
| * </p> |
| * |
| * <p> |
| * Examples including wildcard char: |
| * <pre> |
| * NodePath = "/foo" |
| * Restriction | Matches |
| * ----------------------------------------------------------------------------- |
| * * | foo, all siblings of foo and their descendants |
| * /*cat | all children of /foo whose path ends with "cat" |
| * /*/cat | all non-direct descendants of /foo named "cat" |
| * /cat* | all descendant path of /foo that have the direct foo-descendant segment starting with "cat" |
| * *cat | all siblings and descendants of foo that have a name ending with cat |
| * */cat | all descendants of /foo and foo's siblings that have a name segment "cat" |
| * cat/* | all descendants of '/foocat' |
| * /cat/* | all descendants of '/foo/cat' |
| * *cat/* | all siblings and descendants of foo that have an intermediate segment ending with 'cat' |
| * /*cat/* | all descendants of /foo that have an intermediate segment ending with 'cat' |
| * </pre> |
| * </p> |
| */ |
| final class GlobPattern implements RestrictionPattern { |
| |
| private static final char WILDCARD_CHAR = '*'; |
| private static final int MAX_WILDCARD = 20; |
| |
| private final String path; |
| private final String restriction; |
| |
| private final Pattern pattern; |
| |
| private GlobPattern(@NotNull String path, @NotNull String restriction) { |
| this.path = checkNotNull(path); |
| this.restriction = restriction; |
| |
| if (!restriction.isEmpty()) { |
| StringBuilder b = new StringBuilder(path); |
| b.append(restriction); |
| |
| int lastPos = restriction.lastIndexOf(WILDCARD_CHAR); |
| if (lastPos >= 0) { |
| String end; |
| if (lastPos != restriction.length()-1) { |
| end = restriction.substring(lastPos + 1); |
| } else { |
| end = null; |
| } |
| pattern = new WildcardPattern(b.toString(), end); |
| } else { |
| pattern = new PathPattern(b.toString()); |
| } |
| } else { |
| pattern = new PathPattern(restriction); |
| } |
| } |
| |
| static GlobPattern create(@NotNull String nodePath, @NotNull String restrictions) { |
| return new GlobPattern(nodePath, restrictions); |
| } |
| |
| static void validate(@NotNull String restriction) throws AccessControlException { |
| int cnt = 0; |
| for (int i = 0; i < restriction.length(); i++) { |
| if (WILDCARD_CHAR == restriction.charAt(i)) { |
| cnt++; |
| } |
| if (cnt > MAX_WILDCARD) { |
| throw new AccessControlException("Number of wildcards in rep:glob exceeds allowed complexity."); |
| } |
| } |
| } |
| |
| //-------------------------------------------------< RestrictionPattern >--- |
| @Override |
| public boolean matches(@NotNull Tree tree, @Nullable PropertyState property) { |
| String itemPath = (property == null) ? tree.getPath() : PathUtils.concat(tree.getPath(), property.getName()); |
| return matches(itemPath); |
| } |
| |
| @Override |
| public boolean matches(@NotNull String path) { |
| return pattern.matches(path); |
| } |
| |
| @Override |
| public boolean matches() { |
| // repository level permissions never match any glob pattern |
| return false; |
| } |
| |
| //-------------------------------------------------------------< Object >--- |
| /** |
| * @see Object#hashCode() |
| */ |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(path, restriction); |
| } |
| |
| /** |
| * @see Object#toString() |
| */ |
| @Override |
| public String toString() { |
| return path + " : " + restriction; |
| } |
| |
| /** |
| * @see Object#equals(Object) |
| */ |
| @Override |
| public boolean equals(Object obj) { |
| if (obj == this) { |
| return true; |
| } |
| if (obj instanceof GlobPattern) { |
| GlobPattern other = (GlobPattern) obj; |
| return path.equals(other.path) && restriction.equals(other.restriction); |
| } |
| return false; |
| } |
| |
| //------------------------------------------------------< inner classes >--- |
| /** |
| * Base for PathPattern and WildcardPattern |
| */ |
| private abstract class Pattern { |
| abstract boolean matches(@NotNull String toMatch); |
| } |
| |
| /** |
| * Path pattern: The restriction is missing or doesn't contain any wildcard character. |
| */ |
| private final class PathPattern extends Pattern { |
| |
| private final String patternStr; |
| |
| private PathPattern(@NotNull String patternStr) { |
| this.patternStr = patternStr; |
| } |
| |
| @Override |
| boolean matches(@NotNull String toMatch) { |
| if (patternStr.isEmpty()) { |
| return path.equals(toMatch); |
| } else { |
| // no wildcard contained in restriction: use path defined |
| // by path + restriction to calculate the match |
| return Text.isDescendantOrEqual(patternStr, toMatch); |
| } |
| } |
| } |
| |
| /** |
| * Wildcard pattern: The specified restriction contains one or more wildcard character(s). |
| */ |
| private final class WildcardPattern extends Pattern { |
| |
| private final String patternEnd; |
| private final char[] patternChars; |
| |
| private WildcardPattern(@NotNull String patternStr, @Nullable String patternEnd) { |
| patternChars = patternStr.toCharArray(); |
| this.patternEnd = patternEnd; |
| } |
| |
| @Override |
| boolean matches(@NotNull String toMatch) { |
| if (patternEnd != null && !toMatch.endsWith(patternEnd)) { |
| // shortcut: verify if end of pattern matches end of toMatch |
| return false; |
| } |
| char[] tm = (toMatch.endsWith("/")) ? toMatch.substring(0, toMatch.length()-1).toCharArray() : toMatch.toCharArray(); |
| // shortcut didn't reveal mismatch -> need to process the internal match method. |
| return matches(patternChars, 0, tm, 0, MAX_WILDCARD); |
| } |
| |
| /** |
| * |
| * @param pattern The pattern |
| * @param pOff |
| * @param s |
| * @param sOff |
| * @return {@code true} if matches, {@code false} otherwise |
| */ |
| private boolean matches(char[] pattern, int pOff, |
| char[] s, int sOff, int cnt) { |
| |
| if (cnt <= 0) { |
| throw new IllegalArgumentException("Illegal glob pattern " + GlobPattern.this); |
| } |
| |
| int pLength = pattern.length; |
| int sLength = s.length; |
| |
| while (true) { |
| // end of pattern reached: matches only if sOff points at the end |
| // of the string to match. |
| if (pOff >= pLength) { |
| return sOff >= sLength; |
| } |
| |
| // the end of the string to match has been reached but pattern |
| // doesn't have '*' at patternIndex -> no match |
| if (sOff >= sLength && pattern[pOff] != WILDCARD_CHAR) { |
| return false; |
| } |
| |
| // the next character of the pattern is '*' |
| // -> recursively test if the rest of the specified string matches |
| if (pattern[pOff] == WILDCARD_CHAR) { |
| if (++pOff >= pLength) { |
| return true; |
| } |
| |
| cnt--; |
| while (true) { |
| if (matches(pattern, pOff, s, sOff, cnt)) { |
| return true; |
| } |
| if (sOff >= sLength) { |
| return false; |
| } |
| sOff++; |
| } |
| } |
| |
| // not yet reached end of patter nor string and not wildcard character. |
| // the 2 strings don't match in case the characters at the current |
| // position are not the same. |
| if (pOff < pLength && sOff < sLength) { |
| if (pattern[pOff] != s[sOff]) { |
| return false; |
| } |
| } |
| pOff++; |
| sOff++; |
| } |
| } |
| } |
| } |