/******************************************************************************* * Copyright (c) 2010-2014 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.skalli.services.permit; import java.util.Locale; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.text.StrMatcher; import org.apache.commons.lang.text.StrTokenizer; public class Permit implements Comparable<Permit> { /** * Enumeration representing the default levels of a permit * equivalent to allowing (<code>level = 1</code>) or denying * (<code>level = 0</code>) a certain action. * <p> * Note: Custom levels can be set with the alternative constructors * {@link Permit#Permit(int, String, String)} and * {@link Permit#Permit(int, String, String...)}, respectively. * * @see Permit#Permit(Level, String, String)} */ public static enum Level { FORBID(0), ALLOW(1); private final int level; private Level(int level) { this.level = level; } public int intValue() { return level; } } /** * Wildcard for permit actions. A permit with this action * applies to all actions, i.e. to the standard actions * like <tt>GET</tt>, <tt>PUT</tt>, <tt>POST</tt> and <tt>DELETE</tt>, * but also to all extension actions. */ public static final String ALL_ACTIONS = "**"; //$NON-NLS-1$ /** * Action that implies the retrieval of a resource (in the sense * of an REST <tt>GET</tt> request). */ public static final String ACTION_GET = "GET"; //$NON-NLS-1$ /** * Action that implies the change of a resource (in the sense * of an REST <tt>PUT</tt> request). */ public static final String ACTION_PUT = "PUT"; //$NON-NLS-1$ /** * Action that implies the creation of a member in a collection-like * resource (in the sense of an REST <tt>POST</tt> request). */ public static final String ACTION_POST = "POST"; //$NON-NLS-1$ /** * Action that implies the deletion of a resource (in the sense * of an REST <tt>DELETE</tt> request). */ public static final String ACTION_DELETE = "DELETE"; //$NON-NLS-1$ /** * The root of the permit tree. A permit with this path applies * to all resources of a Skalli instance. */ public static final String ROOT = "/"; //$NON-NLS-1$ /** * Wildcard for permit paths. By replacing a path segment that represents * members of a resource collection, e.g. projects in the collection * of all projects, a permit can be applied to all members of that collection. * <p> * Example: a permit with the path <tt>"/projects/**</tt> would apply to * all known projects. */ public static final String WILDCARD = "**"; //$NON-NLS-1$ /** * Wildcard representing the "current" project the user has requested. * When evaluating permits, this wildcard is replaced with the * {@link org.eclipse.skalli.modelProject#getProjectId() symbolic name} * of a project. * <p> * Example: a permit with the path <tt>"/projects/${project}</tt> would * map to <tt>"/projects/technology.skalli</tt> when a user would look * at the detail page of a project <tt>"technology.skalli"</tt>. */ public static final String PROJECT_WILDCARD = "${project}"; //$NON-NLS-1$ /** * Wildcard representing the currently logged in user. * When evaluating permits, this wildcard is replaced with the identifier * of the current user. * <p> * Example: a permit with the path <tt>"/users/${user}</tt> would * map to <tt>"/projects/hugo</tt> when a user with identifier <tt>"hugo"</tt> * would request that resource. */ public static final String USER_WILDCARD = "${user}"; //$NON-NLS-1$ /** * Permit set that forbids all actions on all resources. */ public static final Permit FORBID_ALL = Permit.valueOf("FORBID ** /"); //$NON-NLS-1$ /** * Permit set that allows all actions on all resources. */ public static final Permit ALLOW_ALL = Permit.valueOf("ALLOW ** /"); //$NON-NLS-1$ /** Shortcut for {@link Level#ALLOW} */ public static final Level ALLOW = Level.ALLOW; /** Shortcut for {@link Level#FORBID} */ public static final Level FORBID = Level.FORBID; private String action = ALL_ACTIONS; private String path = ROOT; private int level = 0; private transient String[] segments; /** * Creates a default permit equivalent to <tt>"FORBID ** /"</tt>. */ public Permit() { } /** * Creates a permit. * * @param level the permission level, i.e. {@link Level#FORBID} or {@link Level#ALLOW}. * @param action the action, e.g. <tt>"GET"</tt> (case is ignored). * @param path the path, e.g. <tt>"/projects/*"</tt> (the leading '/' is optional). */ public Permit(Level level, String action, String path) { setLevel(level.intValue()); setAction(action); setPath(path); } /** * Creates a permit. * * @param level the permission level, i.e. a positive number or 0. Negative * levels are treated as 0. * @param action the action, e.g. <tt>"GET"</tt> (case is ignored). * @param path the path, e.g. <tt>"/projects/*"</tt> (the leading '/' is optional). */ public Permit(int level, String action, String path) { setLevel(level); setAction(action); setPath(path); } /** * Creates a permit. * * @param level the permission level, i.e. a positive number or 0. Negative * levels are treated as 0. * @param action the action, e.g. <tt>"GET"</tt> (case is ignored). * @param segments the segments of the permit's path */ public Permit(int level, String action, String... segments) { setLevel(level); setAction(action); setSegments(segments); } /** * Creates a permit from a given string. * * @param permit a permissions of the form <tt>"ALLOW/FORBID action path"</tt>, * for example <tt>"ALLOW GET /projects/**"</tt>, or <tt>"number action path"</tt>, * for example <tt>"1 GET /projects/**"</tt> or <tt>"+2 GET /projects/**"</tt>. */ public static Permit valueOf(String s) { String[] parts = StringUtils.split(s); if (parts.length != 3) { throw new IllegalArgumentException("Expected format is 'ALLOW/FORBID action path'"); } int level = parseLevel(parts[0]); return new Permit(level, parts[1], parts[2]); } /** * Parses the given string and returns the corresponding level. * * @param s the string to parse, which must be either <code>"ALLOW"</code>, * or <code>"FORBID"</code> (case-insensitive), or an integer number. * If the given string is blank or <code>null</code>, <code>"FORBID"</code> * is assumed. * * @return the level corresponding to the given string. * * @throws NumberFormatException if the given string is neither <code>"ALLOW"</code>, * nor <code>"FORBID"</code>, nor an integer number. */ public static int parseLevel(String s) { int level; if (StringUtils.isBlank(s)) { return Level.FORBID.intValue(); } if (Level.ALLOW.name().equalsIgnoreCase(s)) { level = Level.ALLOW.intValue(); } else if (Level.FORBID.name().equalsIgnoreCase(s)) { level = Level.FORBID.intValue(); } else { if (s.startsWith("+")) { //$NON-NLS-1$ s = s.substring(1); } level = Integer.parseInt(s); } return level; } /** * Returns the permit's action. */ public String getAction() { return action; } /** * Sets the permit's action. * * @param action the action to set, or <code>null</code>, which is treated equivalent to * <code>setAction(ALL_ACTIONS)</code>. */ public void setAction(String action) { this.action = StringUtils.isNotBlank(action) ? action.toUpperCase(Locale.ENGLISH) : ALL_ACTIONS; } /** * Returns the permit's path. */ public String getPath() { return path; } /** * Sets the permit's path. * * @param path the path to set, or <code>null</code>, which is treated equivalent to * <code>setPath(ROOT)</code>. */ public void setPath(String path) { if (StringUtils.isBlank(path)) { setRootPath(); } else { this.segments = split(path); this.path = join(segments); } } /** * Returns the permit's level. */ public int getLevel() { return level; } /** * Sets the permit's level. * * @param level the level to set. Negative arguments are treated equivalent to * <code>setLevel(0)</code>. */ public void setLevel(int level) { this.level = level < 0? 0 : level; } /** * Sets the permit's level. * * @param level the level to set, or <code>null</code>, which is treated equivalent to * <code>setPath(Level.FORBID)</code>. */ public void setLevel(Level level) { setLevel(level != null? level.intValue() : Level.FORBID.intValue()); } /** * Returns the permit's path split up into its segments. */ public String[] getSegments() { if (segments == null) { segments = split(path); path = join(segments); } return segments; } /** * Composes the permit's path from a given list of segments * * @param segments the path segments. Empty lists and <code>null</code> are treated * equivalent to <code>setPath(ROOT)</code>. */ public void setSegments(String...segments) { if (segments == null || segments.length == 0) { setRootPath(); } else { this.segments = new String[segments.length]; for (int i = 0; i< segments.length; ++i) { this.segments[i] = segments[i].trim(); } this.path = join(segments); } } /** * Checks whether any of the given permits matches the requested permit. * * @param permits the permits to search for a match. * @param requestedPermit requested permit. * * @return <code>true</code>, if any of the given permits * matches the requested permit. */ public static boolean match(PermitSet permits, Permit requestedPermit) { return match(permits, requestedPermit.getLevel(), requestedPermit.getAction(), requestedPermit.getSegments()); } /** * Checks whether any of the given permits matches the requested permit. * * @param permits the permits to search for a match. * @param requestedLevel requested permit level. * @param requestedAction requested action. * @param requestedSegments the requested resource path. Either a complete resource * path (with forward slashes (/) as separators), or a list of path * segments (without slashes). If the argument is <code>null</code> or an * empty array, always <code>false</code> is returned. * * @return <code>true</code>, if any of the given permits * matches the requested permit. */ public static boolean match(PermitSet permits, int requestedLevel, String requestedAction, String... requestedSegments) { if (requestedSegments.length == 1) { requestedSegments = split(requestedSegments[0]); } for (Permit permit: permits) { // if permit's action is neither ** nor the same // as the requested action, then the permit is irrelevant if (!matchActions(permit.getAction(), requestedAction)) { continue; } // if requested path is shorter than permit's path, // then the permit applies to an inner resource and // is irrelevant String[] segments = permit.getSegments(); if (segments.length > requestedSegments.length) { continue; } // if permit's path is not a subset of requested path // then the permit is irrelevant if (!matchSegments(segments, requestedSegments)) { continue; } // we have found a matching action and path, // so finally compare the levels: if permit's // level is above or equal requested level, // we have a match! return permit.getLevel() >= requestedLevel; } return false; } private static boolean matchActions(String action, String requestedAction) { return Permit.ALL_ACTIONS.equals(action) || action.equals(requestedAction); } private static boolean matchSegments(String[] segments, String[] requestedSegments) { for (int i = 0; i < segments.length; ++i) { // if permit's segment is ** then it matches any requested segment, // otherwise test corresponding segments for equality if (!Permit.WILDCARD.equals(segments[i]) && !segments[i].equals(requestedSegments[i])) { return false; } } return true; } /** * Compares permits according to the following rules: * The most "concrete" permits should be first in a sorted collection, i.e. * <ul> * <li>paths with more segments are lower than paths with less segments (e.g. /projects/foobar < /projects)</li> * <li>path with less wildcards are lower than paths with more wildcards (e.g. /projects/foobar < /projects/**)</li> * <li>permits with same path should be sorted alphnanumerically according to their actions (GET < PUT)</li> * <li>concrete actions are lower than wildcard actions (GET, PUT... < **) * <li>permits with same paths and actions are equal, regardless of their levels * </ul> */ @Override public int compareTo(Permit o) { int result = compareSegments(getSegments(), o.getSegments()); if (result == 0) { result = compareActions(action, o.action); if (result == 0) { compareWildcardActions(action, o.action); } } return result; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((action == null) ? 0 : action.hashCode()); result = prime * result + ((path == null) ? 0 : path.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } return compareTo((Permit)obj) == 0; } @Override public String toString() { return toString(action, path, level); } public static String toString(String action, String path, Level level) { return toString(action, path, level.intValue()); } public static String toString(String action, String path, int level) { StringBuilder sb = new StringBuilder(); sb.append(action).append(' ').append(path).append(' '); switch (level) { case 0: sb.append(Level.FORBID); break; case 1: sb.append(Level.ALLOW); break; default: sb.append(level); } return sb.toString(); } private static String[] split(String path) { StrTokenizer tokenizer = new StrTokenizer(); tokenizer.setDelimiterChar('/'); tokenizer.setTrimmerMatcher(StrMatcher.trimMatcher()); tokenizer.reset(path); return tokenizer.getTokenArray(); } private static String join(String...segments) { return "/" + StringUtils.join(segments, '/'); //$NON-NLS-1$ } private int compareSegments(String[] left, String[] right) { int result = - Integer.signum(left.length - right.length); if (result == 0) { int i = 0; while (result == 0 && i < left.length && i < right.length) { result = compareSegments(left[i], right[i]); i++; } if (result == 0) { result = i < left.length? -1 : (i < right.length? 1 : 0); } } return result; } private int compareSegments(String s1, String s2) { int result = 0; boolean leftWildcard = WILDCARD.equals(s1); boolean rightWildcard = WILDCARD.equals(s2); if (leftWildcard) { result = rightWildcard ? 0 : 1; } else { result = rightWildcard ? -1 : s1.compareTo(s2); } return result; } private int compareActions(String action1, String action2) { int result = 0; boolean leftWildcard = WILDCARD.equals(action1); boolean rightWildcard = WILDCARD.equals(action2); if (leftWildcard) { result = rightWildcard ? 0 : 1; } else { result = rightWildcard ? -1 : action1.compareTo(action2); } return result; } private int compareWildcardActions(String action1, String action2) { int result = 0; boolean leftWildcard = WILDCARD.equals(action1); boolean rightWildcard = WILDCARD.equals(action2); if (leftWildcard) { result = rightWildcard ? 0 : 1; } else { result = rightWildcard ? -1 : 0; } return result; } private void setRootPath() { this.segments = null; this.path = ROOT; } }