org.osgi.service.dmt.security.DmtPermission Maven / Gradle / Ivy
/*
* Copyright (c) OSGi Alliance (2004, 2013). All Rights Reserved.
*
* Licensed 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.osgi.service.dmt.security;
import java.security.Permission;
import java.security.PermissionCollection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.StringTokenizer;
import org.osgi.service.dmt.Acl;
import org.osgi.service.dmt.Uri;
/**
* Controls access to management objects in the Device Management Tree (DMT). It
* is intended to control local access to the DMT. DmtPermission target string
* identifies the management object URI and the action field lists the OMA DM
* commands that are permitted on the management object. Example:
*
*
* DmtPermission("./OSGi/bundles", "Add,Replace,Get");
*
*
* This means that owner of this permission can execute Add, Replace and Get
* commands on the ./OSGi/bundles management object. It is possible to use
* wildcards in both the target and the actions field. Wildcard in the target
* field means that the owner of the permission can access children nodes of the
* target node. Example:
*
*
* DmtPermission("./OSGi/bundles/*", "Get");
*
*
* This means that owner of this permission has Get access on every child node
* of ./OSGi/bundles. The asterix does not necessarily have to follow a '/'
* character. For example the {@code "./OSGi/a*"} target matches the
* {@code ./OSGi/applications} subtree.
*
* If wildcard is present in the actions field, all legal OMA DM commands are
* allowed on the designated nodes(s) by the owner of the permission. Action
* names are interpreted case-insensitively, but the canonical action string
* returned by {@link #getActions()} uses the forms defined by the action
* constants.
*
* @author $Id: 5fc6f3d37c4fac64313f532ad9bd9f07bc48dc39 $
*/
public class DmtPermission extends Permission {
private static final long serialVersionUID = -1910969921419407809L;
/**
* Holders of DmtPermission with the Add action present can create new nodes
* in the DMT, that is they are authorized to execute the
* createInteriorNode() and createLeafNode() methods of the DmtSession. This
* action is also required for the copy() command, which needs to perform
* node creation operations (among others).
*/
public static final String ADD = "Add";
/**
* Holders of DmtPermission with the Delete action present can delete nodes
* from the DMT, that is they are authorized to execute the deleteNode()
* method of the DmtSession.
*/
public static final String DELETE = "Delete";
/**
* Holders of DmtPermission with the Exec action present can execute nodes
* in the DMT, that is they are authorized to call the execute() method of
* the DmtSession.
*/
public static final String EXEC = "Exec";
/**
* Holders of DmtPermission with the Get action present can query DMT node
* value or properties, that is they are authorized to execute the
* isLeafNode(), getNodeAcl(), getEffectiveNodeAcl(), getMetaNode(),
* getNodeValue(), getChildNodeNames(), getNodeTitle(), getNodeVersion(),
* getNodeTimeStamp(), getNodeSize() and getNodeType() methods of the
* DmtSession. This action is also required for the copy() command, which
* needs to perform node query operations (among others).
*/
public static final String GET = "Get";
/**
* Holders of DmtPermission with the Replace action present can update DMT
* node value or properties, that is they are authorized to execute the
* setNodeAcl(), setNodeTitle(), setNodeValue(), setNodeType() and
* renameNode() methods of the DmtSession. This action is also be required
* for the copy() command if the original node had a title property (which
* must be set in the new node).
*/
public static final String REPLACE = "Replace";
// does this permission have a wildcard at the end?
private final boolean prefixPath;
// the name without the wildcard on the end
private final String path;
// the actions mask
private final int mask;
// the canonical action string (redundant)
private final String actions;
/**
* Creates a new DmtPermission object for the specified DMT URI with the
* specified actions. The given URI can be:
*
* - {@code "*"}, which matches all valid (see
* {@link Uri#isValidUri(String)}) absolute URIs;
* - the prefix of an absolute URI followed by the {@code *} character
* (for example {@code "./OSGi/L*"}), which matches all valid absolute URIs
* beginning with the given prefix;
* - a valid absolute URI, which matches itself.
*
*
* Since the {@code *} character is itself a valid URI character, it can
* appear as the last character of a valid absolute URI. To distinguish this
* case from using {@code *} as a wildcard, the {@code *} character at the
* end of the URI must be escaped with the {@code \} charater. For example
* the URI {@code "./a*"} matches {@code "./a"}, {@code "./aa"},
* {@code "./a/b"} etc. while {@code "./a\*"} matches {@code "./a*"} only.
*
* The actions string must either be "*" to allow all actions, or it must
* contain a non-empty subset of the valid actions, defined as constants in
* this class.
*
* @param dmtUri URI of the management object (or subtree)
* @param actions OMA DM actions allowed
* @throws NullPointerException if any of the parameters are {@code null}
* @throws IllegalArgumentException if any of the parameters are invalid
*/
public DmtPermission(String dmtUri, String actions) {
super(dmtUri);
mask = getMask(actions);
this.actions = canonicalActions(mask);
if (dmtUri == null)
throw new NullPointerException("'dmtUri' parameter must not be " + "null.");
prefixPath = dmtUri.endsWith("*") && !dmtUri.endsWith("\\*");
if (prefixPath) {
dmtUri = dmtUri.substring(0, dmtUri.length() - 1);
// the single "*" as dmtUri is the only valid non-absolute URI param
if (dmtUri.length() == 0) {
path = "";
return;
}
}
// if URI ends with "/*", remove it before the validity check
if (prefixPath && dmtUri.endsWith("/") && !dmtUri.endsWith("\\/"))
checkUri(dmtUri.substring(0, dmtUri.length() - 1));
else
checkUri(dmtUri);
// canonicalize URI: remove escapes from non-special characters
StringBuffer sb = new StringBuffer(dmtUri);
int i = 0;
while (i < sb.length()) { // length can decrease during the loop!
if (sb.charAt(i) == '\\') {
// there must be a next character after a '\' in a valid URI
char nextCh = sb.charAt(i + 1);
if (nextCh != '/' && nextCh != '\\')
sb.deleteCharAt(i); // remove the extra '\'
else
i++;
}
i++;
}
path = sb.toString();
}
private void checkUri(String dmtUri) throws IllegalArgumentException {
if (!Uri.isValidUri(dmtUri))
throw new IllegalArgumentException("'dmtUri' parameter does not " + "contain a valid URI.");
if (!Uri.isAbsoluteUri(dmtUri))
throw new IllegalArgumentException("'dmtUri' parameter does not " + "contain an absolute URI.");
}
/**
* Checks whether the given object is equal to this DmtPermission instance.
* Two DmtPermission instances are equal if they have the same target string
* and the same action mask. The "*" action mask is considered equal to a
* mask containing all actions.
*
* @param obj the object to compare to this DmtPermission instance
* @return {@code true} if the parameter represents the same permissions as
* this instance
*/
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof DmtPermission))
return false;
DmtPermission other = (DmtPermission) obj;
return mask == other.mask && prefixPath == other.prefixPath && path.equals(other.path);
}
/**
* Returns the String representation of the action list. The allowed actions
* are listed in the following order: Add, Delete, Exec, Get, Replace. The
* wildcard character is not used in the returned string, even if the class
* was created using the "*" wildcard.
*
* @return canonical action list for this permission object
*/
public String getActions() {
return actions;
}
/**
* Returns the hash code for this permission object. If two DmtPermission
* objects are equal according to the {@link #equals(Object)} method, then
* calling this method on each of the two DmtPermission objects must produce
* the same integer result.
*
* @return hash code for this permission object
*/
public int hashCode() {
return new Integer(mask).hashCode() ^ new Boolean(prefixPath).hashCode() ^ path.hashCode();
}
/**
* Checks if this DmtPermission object "implies" the specified
* permission. This method returns {@code false} if and only if at least one
* of the following conditions are fulfilled for the specified permission:
*
* - it is not a DmtPermission
* - its set of actions contains an action not allowed by this permission
* - the set of nodes defined by its path contains a node not defined by
* the path of this permission
*
*
* @param p the permission to check for implication
* @return true if this DmtPermission instance implies the specified
* permission
*/
public boolean implies(Permission p) {
if (!(p instanceof DmtPermission))
return false;
DmtPermission other = (DmtPermission) p;
if ((mask & other.mask) != other.mask)
return false;
return impliesPath(other);
}
/**
* Returns a new PermissionCollection object for storing DmtPermission
* objects.
*
* @return the new PermissionCollection
*/
public PermissionCollection newPermissionCollection() {
return new DmtPermissionCollection();
}
// parses the given action string, and returns the corresponding action mask
private static int getMask(String actions) {
int mask = 0;
if (actions == null)
throw new NullPointerException("'actions' parameter cannot be null.");
if (actions.equals("*"))
return Acl.ALL_PERMISSION;
// empty tokens (swallowed by StringTokenizer) are not considered errors
StringTokenizer st = new StringTokenizer(actions, ",");
while (st.hasMoreTokens()) {
String action = st.nextToken().trim();
if (action.equalsIgnoreCase(GET)) {
mask |= Acl.GET;
} else
if (action.equalsIgnoreCase(ADD)) {
mask |= Acl.ADD;
} else
if (action.equalsIgnoreCase(REPLACE)) {
mask |= Acl.REPLACE;
} else
if (action.equalsIgnoreCase(DELETE)) {
mask |= Acl.DELETE;
} else
if (action.equalsIgnoreCase(EXEC)) {
mask |= Acl.EXEC;
} else
throw new IllegalArgumentException("Invalid action '" + action + "'");
}
if (mask == 0)
throw new IllegalArgumentException("Action mask cannot be empty.");
return mask;
}
// generates the canonical string representation of the action list
private static String canonicalActions(int mask) {
StringBuffer sb = new StringBuffer();
addAction(sb, mask, Acl.ADD, ADD);
addAction(sb, mask, Acl.DELETE, DELETE);
addAction(sb, mask, Acl.EXEC, EXEC);
addAction(sb, mask, Acl.GET, GET);
addAction(sb, mask, Acl.REPLACE, REPLACE);
return sb.toString();
}
// if 'flag' appears in 'mask', appends the 'action' string to the contents
// of 'sb', separated by a comma if needed
private static void addAction(StringBuffer sb, int mask, int flag, String action) {
if ((mask & flag) != 0) {
if (sb.length() > 0)
sb.append(',');
sb.append(action);
}
}
// used by DmtPermissionCollection to retrieve the action mask
int getMask() {
return mask;
}
// returns true if the path parameter of the given DmtPermission is
// implied by the path of this permission, i.e. this path is a prefix of the
// other path, but ends with a *, or the two path strings are equal
boolean impliesPath(DmtPermission p) {
return prefixPath ? p.path.startsWith(path) : !p.prefixPath && p.path.equals(path);
}
}
/**
* Represents a homogeneous collection of DmtPermission objects.
*/
final class DmtPermissionCollection extends PermissionCollection {
private static final long serialVersionUID = -4172481774562012941L;
// OPTIMIZE keep a special flag for permissions of "*" path
private ArrayList perms;
/**
* Create an empty DmtPermissionCollection object.
*/
public DmtPermissionCollection() {
perms = new ArrayList();
}
/**
* Adds a permission to the DmtPermissionCollection.
*
* @param permission the Permission object to add
* @exception IllegalArgumentException if the permission is not a
* DmtPermission
* @exception SecurityException if this DmtPermissionCollection object has
* been marked readonly
*/
public void add(Permission permission) {
if (!(permission instanceof DmtPermission))
throw new IllegalArgumentException("Cannot add permission, invalid permission type: " + permission);
if (isReadOnly())
throw new SecurityException("Cannot add permission, collection is marked read-only.");
// No need to synchronize because all adds are done sequentially
// before any implies() calls
perms.add(permission);
}
/**
* Check whether this set of permissions implies the permission specified in
* the parameter.
*
* @param permission the Permission object to compare
* @return true if the parameter permission is a proper subset of the
* permissions in the collection, false otherwise
*/
public boolean implies(Permission permission) {
if (!(permission instanceof DmtPermission))
return false;
DmtPermission other = (DmtPermission) permission;
int required = other.getMask();
int available = 0;
int needed = required;
Iterator i = perms.iterator();
while (i.hasNext()) {
DmtPermission p = (DmtPermission) i.next();
if (((needed & p.getMask()) != 0) && p.impliesPath(other)) {
available |= p.getMask();
if ((available & required) == required)
return true;
needed = (required ^ available);
}
}
return false;
}
/**
* Returns an enumeration of all the DmtPermission objects in the container.
* The returned value cannot be {@code null}.
*
* @return an enumeration of all the DmtPermission objects
*/
public Enumeration elements() {
// Convert Iterator into Enumeration
return Collections.enumeration(perms);
}
}