org.osgi.service.dmt.Acl 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;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;
/**
* {@code Acl} is an immutable class representing structured access to DMT ACLs.
* Under OMA DM the ACLs are defined as strings with an internal syntax.
*
* The methods of this class taking a principal as parameter accept remote
* server IDs (as passed to {@link DmtAdmin#getSession(String, String, int)
* DmtAdmin.getSession}), as well as " {@code *} " indicating any
* principal.
*
* The syntax for valid remote server IDs:
*
* <server-identifier> ::= All printable characters except
* {@code '='}, {@code '&'}, {@code '*'}, {@code '+'} or white-space characters.
*
* @author $Id: a8eef2d9a9105197eb1e5e6f94013d7976a4f929 $
*/
public final class Acl {
// ----- Public constants -----//
/**
* Principals holding this permission can issue GET command on the node
* having this ACL.
*/
public static final int GET = 1;
/**
* Principals holding this permission can issue ADD commands on the node
* having this ACL.
*/
public static final int ADD = 2;
/**
* Principals holding this permission can issue REPLACE commands on the node
* having this ACL.
*/
public static final int REPLACE = 4;
/**
* Principals holding this permission can issue DELETE commands on the node
* having this ACL.
*/
public static final int DELETE = 8;
/**
* Principals holding this permission can issue EXEC commands on the node
* having this ACL.
*/
public static final int EXEC = 16;
/**
* Principals holding this permission can issue any command on the node
* having this ACL. This permission is the logical OR of {@link #ADD},
* {@link #DELETE}, {@link #EXEC}, {@link #GET} and {@link #REPLACE}
* permissions.
*/
public static final int ALL_PERMISSION = ADD | DELETE | EXEC | GET | REPLACE;
// ----- Private constants -----//
private static final int[] PERMISSION_CODES = new int[] {ADD, DELETE, EXEC, GET, REPLACE};
private static final String[] PERMISSION_NAMES = new String[] {"Add", "Delete", "Exec", "Get", "Replace"};
private static final String ALL_PRINCIPALS = "*";
// ----- Private fields -----//
// the implementation takes advantage of this being a sorted map
private final TreeMap principalPermissions;
private final int globalPermissions;
// ----- Public constructors -----//
/**
* Create an instance of the ACL from its canonical string representation.
*
* @param acl The string representation of the ACL as defined in OMA DM. If
* {@code null} or empty then it represents an empty list of
* principals with no permissions.
* @throws IllegalArgumentException if acl is not a valid OMA DM ACL string
*/
public Acl(String acl) {
if (acl == null || acl.equals("")) { // empty permission set
principalPermissions = new TreeMap();
globalPermissions = 0;
return;
}
TreeMap tempPrincipalPermissions = new TreeMap();
int tempGlobalPermissions = 0;
String[] aclEntries = split(acl, '&', -1);
for (int i = 0; i < aclEntries.length; i++) {
if (aclEntries[i].length() == 0)
throw new IllegalArgumentException("Invalid ACL string: empty ACL entry.");
String[] entryParts = split(aclEntries[i], '=', 2);
if (entryParts.length == 1)
throw new IllegalArgumentException("Invalid ACL string: no '=' in ACL entry.");
if (entryParts[1].length() == 0)
throw new IllegalArgumentException("Invalid ACL string: no server identifiers in ACL entry.");
int command = parseCommand(entryParts[0]);
String[] serverIds = split(entryParts[1], '+', -1);
for (int j = 0; j < serverIds.length; j++) {
if (serverIds[j].length() == 0)
throw new IllegalArgumentException("Invalid ACL string: empty server identifier.");
if (serverIds[j].equals(ALL_PRINCIPALS))
tempGlobalPermissions |= command;
else {
checkServerId(serverIds[j], "Invalid ACL string: " + "server ID contains illegal character");
Integer n = (Integer) tempPrincipalPermissions.get(serverIds[j]);
int oldPermission = (n != null) ? n.intValue() : 0;
tempPrincipalPermissions.put(serverIds[j], new Integer(oldPermission | command));
}
}
}
principalPermissions = tempPrincipalPermissions;
globalPermissions = tempGlobalPermissions;
}
/**
* Creates an instance with a specified list of principals and the
* permissions they hold. The two arrays run in parallel, that is
* {@code principals[i]} will hold {@code permissions[i]} in the ACL.
*
* A principal name may not appear multiple times in the 'principals'
* argument. If the "*" principal appears in the array, the
* corresponding permissions will be granted to all principals (regardless
* of whether they appear in the array or not).
*
* @param principals The array of principals
* @param permissions The array of permissions
* @throws IllegalArgumentException if the length of the two arrays are not
* the same, if any array element is invalid, or if a principal
* appears multiple times in the {@code principals} array
*/
public Acl(String[] principals, int[] permissions) {
if (principals.length != permissions.length)
throw new IllegalArgumentException("The lengths of the principal and permission arrays are not the same.");
TreeMap tempPrincipalPermissions = new TreeMap();
int tempGlobalPermissions = 0;
for (int i = 0; i < principals.length; i++) {
// allow one * in 'principals' array, remove after loop
if (!ALL_PRINCIPALS.equals(principals[i]))
checkPrincipal(principals[i]);
checkPermissions(permissions[i]);
Integer permInt = new Integer(permissions[i]);
Object old = tempPrincipalPermissions.put(principals[i], permInt);
if (old != null)
throw new IllegalArgumentException("Principal '" + principals[i] + "' appears multiple times in the principal array.");
}
// set the global permissions if there was a * in the array
Object globalPermObj = tempPrincipalPermissions.remove(ALL_PRINCIPALS);
if (globalPermObj != null)
tempGlobalPermissions = ((Integer) globalPermObj).intValue();
principalPermissions = tempPrincipalPermissions;
globalPermissions = tempGlobalPermissions;
}
// ----- Private constructors -----//
/**
* Creates an instance identical to the {@code base} ACL except for the
* permissions of the given {@code principal}, which are overwritten with
* the given {@code permissions}.
*
* Assumes that the permissions parameter has been checked. All
* modifications of an {@code Acl} (add, delete, set) are done through this
* method.
*
* @param base The ACL that provides all permissions except for permissions
* of the given principal.
* @param principal The entity to which permission should be granted.
* @param permissions The set of permissions to be given. The parameter can
* be a logical {@code or} of the permission constants defined in
* this class.
*/
private Acl(Acl base, String principal, int permissions) {
// make a shallow copy of the permission table, the keys (String) and
// values (Integer) are immutable anyway
TreeMap tempPrincipalPermissions = (TreeMap) base.principalPermissions.clone();
int tempGlobalPermissions = base.globalPermissions;
int deletedGlobalPerm = tempGlobalPermissions & ~permissions;
if (ALL_PRINCIPALS.equals(principal)) {
deleteFromAll(tempPrincipalPermissions, deletedGlobalPerm);
tempGlobalPermissions = permissions;
} else {
checkPrincipal(principal);
if (deletedGlobalPerm != 0)
throw new IllegalArgumentException("Cannot revoke globally set permissions (" + writeCommands(deletedGlobalPerm) + ") from a specific principal (" + principal + ").");
setPrincipalPermission(tempPrincipalPermissions, principal, permissions);
}
principalPermissions = tempPrincipalPermissions;
globalPermissions = tempGlobalPermissions;
}
// ----- Public methods -----//
/**
* Checks whether the given object is equal to this {@code Acl} instance.
* Two {@code Acl} instances are equal if they allow the same set of
* permissions for the same set of principals.
*
* @param obj the object to compare with this {@code Acl} instance
* @return {@code true} if the parameter represents the same ACL as this
* instance
*/
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof Acl))
return false;
Acl other = (Acl) obj;
if (globalPermissions != other.globalPermissions || principalPermissions.size() != other.principalPermissions.size())
return false;
// principalPermissions sets cannot be easily compared, because they are
// not canonical: the global permissions may or may not be present for
// each principal, without changing the meaning of the Acl object.
// Compare canonical string representations, inefficient but simple.
return toString().equals(other.toString());
}
/**
* Returns the hash code for this ACL instance. If two {@code Acl} instances
* are equal according to the {@link #equals(Object)} method, then calling
* this method on each of them must produce the same integer result.
*
* @return hash code for this ACL
*/
public int hashCode() {
// Using the hash code of the canonical string representation, because
// the principalPermissions set is not canonical (see above).
return toString().hashCode();
}
/**
* Create a new {@code Acl} instance from this {@code Acl} with the given
* permission added for the given principal. The already existing
* permissions of the principal are not affected.
*
* @param principal The entity to which permissions should be granted, or
* "*" to grant permissions to all principals.
* @param permissions The permissions to be given. The parameter can be a
* logical {@code or} of more permission constants defined in this
* class.
* @return a new {@code Acl} instance
* @throws IllegalArgumentException if {@code principal} is not a valid
* principal name or if {@code permissions} is not a valid
* combination of the permission constants defined in this class
*/
public synchronized Acl addPermission(String principal, int permissions) {
checkPermissions(permissions);
int oldPermissions = getPermissions(principal);
return setPermission(principal, oldPermissions | permissions);
}
/**
* Create a new {@code Acl} instance from this {@code Acl} with the given
* permission revoked from the given principal. Other permissions of the
* principal are not affected.
*
* Note, that it is not valid to revoke a permission from a specific
* principal if that permission is granted globally to all principals.
*
* @param principal The entity from which permissions should be revoked, or
* "*" to revoke permissions from all principals.
* @param permissions The permissions to be revoked. The parameter can be a
* logical {@code or} of more permission constants defined in this
* class.
* @return a new {@code Acl} instance
* @throws IllegalArgumentException if {@code principal} is not a valid
* principal name, if {@code permissions} is not a valid combination
* of the permission constants defined in this class, or if a
* globally granted permission would have been revoked from a
* specific principal
*/
public synchronized Acl deletePermission(String principal, int permissions) {
checkPermissions(permissions);
int oldPermissions = getPermissions(principal);
return setPermission(principal, oldPermissions & ~permissions);
}
/**
* Get the permissions associated to a given principal.
*
* @param principal The entity whose permissions to query, or "*"
* to query the permissions that are granted globally, to all
* principals
* @return The permissions of the given principal. The returned {@code int}
* is a bitmask of the permission constants defined in this class
* @throws IllegalArgumentException if {@code principal} is not a valid
* principal name
*/
public synchronized int getPermissions(String principal) {
int permissions = 0;
if (!(ALL_PRINCIPALS.equals(principal))) {
checkPrincipal(principal);
Object po = principalPermissions.get(principal);
if (po != null)
permissions = ((Integer) po).intValue();
}
return permissions | globalPermissions;
}
/**
* Check whether the given permissions are granted to a certain principal.
* The requested permissions are specified as a bitfield, for example
* {@code (Acl.ADD | Acl.DELETE | Acl.GET)}.
*
* @param principal The entity to check, or "*" to check whether
* the given permissions are granted to all principals globally
* @param permissions The permissions to check
* @return {@code true} if the principal holds all the given permissions
* @throws IllegalArgumentException if {@code principal} is not a valid
* principal name or if {@code permissions} is not a valid
* combination of the permission constants defined in this class
*/
public synchronized boolean isPermitted(String principal, int permissions) {
checkPermissions(permissions);
int hasPermissions = getPermissions(principal);
return (permissions & hasPermissions) == permissions;
}
/**
* Create a new {@code Acl} instance from this {@code Acl} where all
* permissions for the given principal are overwritten with the given
* permissions.
*
* Note, that when changing the permissions of a specific principal, it is
* not allowed to specify a set of permissions stricter than the global set
* of permissions (that apply to all principals).
*
* @param principal The entity to which permissions should be granted, or
* "*" to globally grant permissions to all principals.
* @param permissions The set of permissions to be given. The parameter is a
* bitmask of the permission constants defined in this class.
* @return a new {@code Acl} instance
* @throws IllegalArgumentException if {@code principal} is not a valid
* principal name, if {@code permissions} is not a valid combination
* of the permission constants defined in this class, or if a
* globally granted permission would have been revoked from a
* specific principal
*/
public synchronized Acl setPermission(String principal, int permissions) {
checkPermissions(permissions);
Acl newPermission = new Acl(this, principal, permissions);
return newPermission;
}
/**
* Get the list of principals who have any kind of permissions on this node.
* The list only includes those principals that have been explicitly
* assigned permissions (so "*" is never returned), globally set
* permissions naturally apply to all other principals as well.
*
* @return The array of principals having permissions on this node.
*/
public String[] getPrincipals() {
return (String[]) (principalPermissions.keySet().toArray(new String[0]));
}
/**
* Give the canonical string representation of this ACL. The operations are
* in the following order: {Add, Delete, Exec, Get, Replace}, principal
* names are sorted alphabetically.
*
* @return The string representation as defined in OMA DM.
*/
public synchronized String toString() {
String acl = null;
for (int i = 0; i < PERMISSION_CODES.length; i++)
acl = writeEntry(PERMISSION_CODES[i], acl);
return (acl != null) ? acl : "";
}
// ----- Private utility methods -----//
private String writeEntry(int command, String acl) {
String aclEntry = null;
if ((command & globalPermissions) > 0)
aclEntry = ALL_PRINCIPALS;
else {
// TreeMap guarantees alphabetical ordering of keys during traversal
Iterator i = principalPermissions.entrySet().iterator();
while (i.hasNext()) {
Map.Entry entry = (Map.Entry) i.next();
if ((command & ((Integer) entry.getValue()).intValue()) > 0)
aclEntry = appendEntry(aclEntry, '+', (String) entry.getKey());
}
}
if (aclEntry == null)
return acl;
return appendEntry(acl, '&', writeCommands(command) + '=' + aclEntry);
}
private static void deleteFromAll(TreeMap principalPermissions, int perm) {
Iterator i = principalPermissions.entrySet().iterator();
while (i.hasNext()) {
Map.Entry entry = (Map.Entry) i.next();
setPrincipalPermission(principalPermissions, (String) entry.getKey(), ((Integer) entry.getValue()).intValue() & ~perm);
}
}
private static void setPrincipalPermission(TreeMap principalPermissions, String principal, int perm) {
if (perm == 0)
principalPermissions.remove(principal);
else
principalPermissions.put(principal, new Integer(perm));
}
private static String writeCommands(int command) {
String commandStr = null;
for (int i = 0; i < PERMISSION_CODES.length; i++)
if ((command & PERMISSION_CODES[i]) != 0)
commandStr = appendEntry(commandStr, ',', PERMISSION_NAMES[i]);
return (commandStr != null) ? commandStr : "";
}
private static String appendEntry(String base, char separator, String entry) {
return (base != null) ? base + separator + entry : entry;
}
private static int parseCommand(String command) {
int i = Arrays.asList(PERMISSION_NAMES).indexOf(command);
if (i == -1)
throw new IllegalArgumentException("Invalid ACL string: unknown command '" + command + "'.");
return PERMISSION_CODES[i];
}
private static void checkPermissions(int perm) {
if ((perm & ~ALL_PERMISSION) != 0)
throw new IllegalArgumentException("Invalid ACL permission value: " + perm);
}
private static void checkPrincipal(String principal) {
if (principal == null || principal.length() == 0)
throw new IllegalArgumentException("Principal is null or empty.");
checkServerId(principal, "Principal name contains illegal character");
}
private static void checkServerId(String serverId, String errorText) {
char[] chars = serverId.toCharArray();
for (int i = 0; i < chars.length; i++)
if ("*=+&".indexOf(chars[i]) != -1 || Character.isWhitespace(chars[i]))
throw new IllegalArgumentException(errorText + " '" + chars[i] + "'.");
}
private static String[] split(String input, char sep, int limit) {
Vector v = new Vector();
boolean limited = (limit > 0);
int applied = 0;
int index = 0;
StringBuffer part = new StringBuffer();
while (index < input.length()) {
char ch = input.charAt(index);
if (ch != sep)
part.append(ch);
else {
++applied;
v.add(part.toString());
part = new StringBuffer();
}
++index;
if (limited && applied == limit - 1)
break;
}
while (index < input.length()) {
char ch = input.charAt(index);
part.append(ch);
++index;
}
v.add(part.toString());
int last = v.size();
if (0 == limit) {
for (int j = v.size() - 1; j >= 0; --j) {
String s = (String) v.elementAt(j);
if ("".equals(s))
--last;
else
break;
}
}
String[] ret = new String[last];
for (int i = 0; i < last; ++i)
ret[i] = (String) v.elementAt(i);
return ret;
}
}