/************************************************************************** * Copyright (c) 2001 by Punch Telematix. All rights reserved. * * * * Redistribution and use in source and binary forms, with or without * * modification, are permitted provided that the following conditions * * are met: * * 1. Redistributions of source code must retain the above copyright * * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * * notice, this list of conditions and the following disclaimer in the * * documentation and/or other materials provided with the distribution. * * 3. Neither the name of Punch Telematix nor the names of * * other contributors may be used to endorse or promote products * * derived from this software without specific prior written permission.* * * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED * * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * * IN NO EVENT SHALL PUNCH TELEMATIX OR OTHER CONTRIBUTORS BE LIABLE * * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN * * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * **************************************************************************/ package wonka.security; import java.io.FilePermission; import java.security.Permission; import java.security.PermissionCollection; import java.util.Enumeration; import java.util.Hashtable; import java.util.HashSet; import java.util.Vector; /** * The class FilePermissionCollection is designed to optimize performance * of the `implies' operation, which must determine whether a candidate * FilePermission is implied by any permission in the collection. * This we do using a `tree' data structure, in which the `leaves' are * Integer's holding a bitmask of permissions: e.g. the four FilePremissions * \texttt{\{"/foo/bar/baz","read,write"\}, * \{"/foo/bar/quux","read,execute"\}, \{"/foo/*","read"\}}, and * \textt{\{"/something/else","read"\}} would result in the following * tree: * * +---> bar = READ|WRITE * | * -+---> foo/bar +---> baz = READ|EXECUTE * | | * | +---> * = READ * | * +---> something +---> else = READ * * Each node in the tree implies a `/' (or rather a SEP_CHAR), * and a leading or trailing `/' is not represented. The leading `/' * is not needed because we assume all paths are absolute (if relative * paths were supported, they would be converted in the constructor of * FilePermission). The trailing `/' is not needed because * `some-directory/' and `some-directory' are synonymous. * * Each level of the tree is a hashtable in which they keys are directory * or file names. The data associated with each key is either a FilePermission * (leaf node) or another hashtable which holds the possible sequels, and * so on recursively. * * Note: this is essentially the same algorithm as that used in * BasicPermissionsCollection, except that we always branch at a '/', * and therefore the '/' can be made implicit. * * TODO: replace the leaf elements by instances of Integer, holding a 4-bit * mask to represent the permissions. This will make implies() faster, but * means that e.g. elements() will need to reconstruct the FilePermissions * on-the-fly. --> elements are stored in a HashSet. this has the advantage * that if a permission is added which is equal to a previously added that we don't * have to add it in the hashTable structure */ public class FilePermissionCollection extends PermissionCollection { private final static String DASH = "-"; private final static String STAR = "*"; private final static int EXECUTE = 1; private final static int WRITE = 2; private final static int READ = 4; private final static int DELETE = 8; private static final int INITIAL_TABSIZE=11; private static final String SEP = System.getProperty("file.separator"); private static final char SEP_CHAR = SEP.charAt(0); private static final String SEP_DASH = SEP + DASH; private static final String SEP_STAR = SEP + STAR; private Hashtable prefixes; private HashSet elements; /** ** this value is used to contain the actions implied by the FilePermission "<<ALL FILES>>" ** this is added so file names like '<<ALL FILES>>/dir' don't cause strange things ... */ private int allMask=0; /** ** Constructor FilePermissionCollection() builds an empty Hashtable and an empty HashSet. */ public FilePermissionCollection() { prefixes = new Hashtable(INITIAL_TABSIZE); elements = new HashSet(); } /** ** Method actions2bitmask converts an ``actions'' string into a bitmask. ** (note: the action string should be one retrieved from getActions(); */ private static int actions2bitmask(String actions) { int result = 0; String remainder = actions; //.toLowerCase();--> not needed (see note) while (remainder != null) { int comma = remainder.indexOf(','); String action; if (comma < 0) { action = remainder; remainder = null; } else { action = remainder.substring(0,comma); remainder = remainder.substring(comma+1); } // we use a string retrieved from getActions --> each actions is only mentioned once, // they are comma seperated, and no whitespace added ... if (action.equals("execute")) { // System.out.println(" Recognised action '"+action+"'"); result |= EXECUTE; } else if (action.equals("write")) { // System.out.println(" Recognised action '"+action+"'"); result |= WRITE; } else if (action.equals("read")) { // System.out.println(" Recognised action '"+action+"'"); result |= READ; } else if (action.equals("delete")) { // System.out.println(" Recognised action '"+action+"'"); result |= DELETE; } else { //this case sould not occur ... // System.out.println(" Ignoring unknown or redundant action '"+action+"'"); } } return result; } /** Method add_action(FilePermission,int) rebuilds the prefixes * to take account of a new FilePermission. The int denotes * the actions, i.e. one of READ, WRITE, EXECUTE, DELETE. * * We try to match the name of the new permission against each key * of the root hashtable ('prefixes') in turn, looking for the longest * prefix common to both the name and the key; the longest such prefix * then determines what should happen next: * - if its length is zero, then in fact we have a new prefix. We add * an entry to the (root) hashtable with the new name as key and * the action as data. * Example in the illustration: something/else * - if the longest prefix is shorter than the key in which it is found, * then we need to split a key. The current key is removed from the * hashtable and replaced by an entry with the shorter prefix as key * and as data a new hashtable containing one entry, namely a mapping * from the remainder of the existing key to the data that was referenced * by the old key. We then resume the algorithm with the new hashtable * and the remainder of the name. * Example: hashtable contains * +---> foo/bar/baz * and we wish to add 'foo/bar/quux'. We replace the existing entry * 'foo.bar.baz' by a link to a new hashtable: * +---> foo/bar ---> baz * and then add 'quux' to the new hashtable. * - else the longest prefix exactly matches some key: we follow the * link from this key to its associated Hashtable and resume the * algorithm with the new hashtable and the remainder of the name. * * note : acunia/dir has no match acuniaCompany/dir */ private void add_action (FilePermission newperm, int action) throws SecurityException { // System.out.println("Prefix table before adding permission '"+newperm+"': "+prefixes); Hashtable table = prefixes; String name = newperm.getName(); String longest_prefix = ""; String target_matched = ""; int longest_prefix_length = 0; int namelen = name.length(); boolean wild = (name.endsWith(SEP_STAR) || name.endsWith(SEP_DASH)); String wildstring=null; if (wild) { namelen -= 2; wildstring = name.substring(namelen+1); name = name.substring(0,namelen); } if (name.charAt(namelen-1) == SEP_CHAR) { namelen -= 1; name = name.substring(0,namelen); // System.out.println("Name ends in '" + SEP + "', use just '"+name+"'."); } while (namelen >= 0) { if (namelen > 0 && name.charAt(0) == SEP_CHAR) { name = name.substring(1); --namelen; } // System.out.println("Looking for prefix '"+name+"' in "+table); Enumeration e = table.keys(); while (e.hasMoreElements()) { String prefix = name; int prefixlen = namelen; String target = (String)e.nextElement(); // System.out.println(" Trying '"+target+"'"); while (prefixlen>longest_prefix_length) { if (target.startsWith(prefix)) { longest_prefix = prefix; target_matched = target; longest_prefix_length = prefixlen; } int i = prefix.lastIndexOf(SEP_CHAR); if (i<0) { prefix = ""; prefixlen = 0; } else { prefix = prefix.substring(0,i); prefixlen = i; } } } if (longest_prefix_length == 0) { if (wild) { // System.out.println(" No match found, adding new entry: '"+name+" --> add wild card"); Hashtable new_table = new Hashtable(INITIAL_TABSIZE); table.put(name,new_table); table = new_table; name = wildstring; namelen = 1; wild = false; } else { Integer new_actions = new Integer(action); // System.out.println(" No match found, adding new entry: '"+name+"'->"+new_actions); table.put(name,new_actions); name = ""; namelen = -1; } } else { Object old_data = table.get(target_matched); if (longest_prefix_length < target_matched.length()) { // System.out.println(" Partial match found, splitting entry for '"+target_matched+"' after '"+longest_prefix+"' ("+longest_prefix_length+" chars)"); Hashtable new_table = new Hashtable(INITIAL_TABSIZE); new_table.put(target_matched.substring(longest_prefix_length+1),old_data); // System.out.println(" New hashtable: "+new_table); table.remove(target_matched); table.put(longest_prefix,new_table); // System.out.println(" Old hashtable: "+table); table = new_table; } else if (old_data instanceof Integer) { // System.out.println(" Total match found, merging with entry for '"+target_matched+"'"); // first case "" (and not wild) if (!wild && namelen == longest_prefix_length) { Integer old_actions = (Integer)old_data; if ((old_actions.intValue() & action) == 0) { table.put(target_matched,new Integer(old_actions.intValue() | action)); } } else { Hashtable new_table = new Hashtable(INITIAL_TABSIZE); table.put(target_matched,new_table); table = new_table; table.put("",old_data); } } else { // System.out.println(" Total match found for '"+longest_prefix+"'"); table = (Hashtable)old_data; // System.out.println(" --> switch to hashtable: "+table); } if (wild && namelen == longest_prefix_length) { name =wildstring; namelen = 1; wild =false; } else { name = name.substring(longest_prefix_length); namelen = name.length(); } longest_prefix = ""; longest_prefix_length = 0; // System.out.println(" Remainder of name is '"+name+"'"); } } // System.out.println("Prefix table after adding permission '"+newperm+"': "+prefixes); } /** * Method add(Permission) rebuilds the Hashtables to take account * of the new Permission (which must be a FilePermission). */ public synchronized void add (Permission permission) throws SecurityException { if (super.isReadOnly()) throw new SecurityException("read-only"); FilePermission newperm; try { newperm = (FilePermission) permission; } catch (ClassCastException e) { throw new IllegalArgumentException("not a FilePermission"); } if(elements.add(newperm)) { if (newperm.getName().equals("<<ALL FILES>>")) { //special case ... allMask |= actions2bitmask(newperm.getActions()); } else { add_action(newperm,actions2bitmask(newperm.getActions())); } } // else {System.out.println("permission not added --> already an equal object in the collection"); } } // private int clearBits(int actions , int mask) { // return (actions & (~mask)); /** * Method implies_action tests whether a given action is implied by any * of the FilePermissions in this collection. * * We first test to see whether the whole pathname of the permission is a * key of the `prefixes' hashtable, with as value a FilePermission * which implies te target action. If so then the algorithm terminates * successfully. It also terminates successfully if the hashtable contains * a key "-", or if the hashtable contains a key "*" and the pathname does * `not contain `/', and the associated value is a FilePermission which * implies the target action. Otherwise, we try to match the pathname * of the new permission against each key in the hashtable: * if any key is a prefix of the pathname then the associated data must be a * Hashtable, and we resume the algorithm with the remainder of the name * in this hashtable. */ public boolean implies_action (String path, int action) { // System.out.println("Testing whether path "+path+" implies action "+action+", allMask="+allMask); if ((allMask & action) == action) { // this test verifies if the special token <<ALL FILES>> return true; // has been set for this action ... } action = (action & (~allMask)); Object data; Hashtable table = prefixes; String name = path; int namelen = name.length(); if (namelen > 0 && name.charAt(namelen-1) == SEP_CHAR) { //names ending with '/' are directories. But they are handled like files (they are stored without the slash) namelen--; name = name.substring(0,namelen); } if (namelen > 0 && name.charAt(0) == SEP_CHAR) { name = name.substring(1); --namelen; } while (namelen >= 0) { // System.out.println("Looking for prefix '"+name+"' in "+table); data = table.get(name); if (data != null) { try { Integer found_actions = (Integer)data; if ((found_actions.intValue() & action) == action) { // System.out.println(" Found an exact match for '"+name+"'->"+data+", success!"); return true; } else { action = (action & (~found_actions.intValue())); } } catch (ClassCastException e) { // Not an exact match, fall through } } data = table.get(DASH); if (data != null && namelen > 0) { try { Integer found_actions = (Integer)data; if ((found_actions.intValue() & action) == action) { // System.out.println(" Found a - ->"+data+", success!"); return true; } action = (action & (~found_actions.intValue())); } catch (ClassCastException e) { // System.err.println("Odd: entry for '-' is not terminal when searching for "+path+" in "+this); } } data = table.get(STAR); if (data != null && namelen > 0) { try { Integer found_actions = (Integer)data; if ((found_actions.intValue() & action) == action && name.indexOf(SEP_CHAR) < 0) { // System.out.println(" Found a * ->"+data+", success!"); return true; } action = (action & (~found_actions.intValue())); } catch (ClassCastException e) { // System.err.println("Odd: entry for '*' is not terminal when searching for "+path+" in "+this); } } Enumeration e = table.keys(); boolean found = false; while (e.hasMoreElements()) { String target = (String)e.nextElement(); // System.out.println(" Trying '"+target+"'"); if (name.startsWith(target)) { data = table.get(target); if (data instanceof Hashtable) { //we should verify if the next char in the name is a SEP_CHAR if (target.length() < namelen && name.charAt(target.length()) != SEP_CHAR) { continue; } table = (Hashtable)data; // System.out.println(" Matched prefix '"+target+"'->hashtable: "+table); name = name.substring((target.length() < namelen ? target.length()+1 : target.length())); namelen = name.length(); // System.out.println(" Remainder of name is '"+name+"'"); found = true; break; } else { // when we reach this point we have a prefix with a mask (this a dir or a file) // if name is target+'/' then we are ok ! if (name.equals(target + SEP)){ int found_actions = ((Integer)data).intValue(); if ((found_actions & action)==action){ return true; } action = (action & (~found_actions)); } // System.err.println(" Matched prefix '"+target+"', but -> "+data+" (?!)"); } } } if (!found) { break; } } // System.out.println(" No match found, failed."); return false; } /** * Method 'implies' tests whether a given permission is implied by any * of the FilePermissions in this collection. The permission must of * course be a FilePermission. A FilePermission is implied by this * collection if none of its actions are not implied by this collection * for the given path. (If you like double negatives, do not fail to * raise your hand.) */ public boolean implies (Permission permission) { FilePermission tryperm; try { tryperm = (FilePermission) permission; } catch (ClassCastException e) { // System.out.println(permission+" is not a FilePermission!"); return false; } return implies_action(tryperm.getName() , actions2bitmask(tryperm.getActions())); /* trypath = tryperm.getName(); return (tryperm.getActions().indexOf("read") < 0 || this.implies_action(trypath,READ)) && (tryperm.getActions().indexOf("write") < 0 || this.implies_action(trypath,WRITE)) && (tryperm.getActions().indexOf("execute") < 0 || this.implies_action(trypath,EXECUTE)) && (tryperm.getActions().indexOf("delete") < 0 || this.implies_action(trypath,DELETE)) ; */ } /** * The elements() method. * a hashSet only provides an iterator which can throw ConcurrentModificationExceptions. * so we make it easy for ourselves and put everything in a vector and call elements() on the vector ... */ public synchronized Enumeration elements() { return new Vector(elements).elements(); } }