One of the least involved approaches towards attaining deny rule support, is to:
- Define a "negative"
Permission
subclass that wraps a regular positive permission and negates it; and
- wrap the default
Policy
such that it (its wrapper) understands such permissions.
The DeniedPermission
class
package com.example.q5003565;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.security.BasicPermission;
import java.security.Permission;
import java.security.UnresolvedPermission;
import java.text.MessageFormat;
/**
* A representation of a "negative" privilege.
* <p>
* A <code>DeniedPermission</code>, when "granted" to some <code>ProtectionDomain</code>, represents
* a privilege which <em>cannot</em> be exercised, regardless of any positive permissions
* (<code>AllPermission</code> included) possessed. In other words, if a set of granted permissions,
* <em>P</em>, contains a permission of this class, <em>D</em>, then the set of effectively granted
* permissions is<br/>
* <br/>
* <em>{ P<sub>implied</sub> - D<sub>implied</sub> }</em>.
* </p>
* <p>
* Each instance of this class encapsulates a <em>target permission</em>, representing the
* "positive" permission being negated.
* </p>
* Denied permissions employ the following naming scheme:<br/>
* <br/>
* <em><target_class_name>:<target_name>(:<target_actions>)</em><br/>
* <br/>
* where:
* <ul>
* <li><em><target_class_name></em> is the fully qualified name of the target permission's
* class,</li>
* <li><em><target_name></em> is the {@linkplain #getName() name} of the target
* permission,</li>
* <li><em>(<target_actions>)</em> is, optionally, the {@linkplain #getActions() actions
* string} of the target permission, and</li>
* <li>the <em>':'</em> character stands for itself.</li>
* </ul>
* A denied permission, having a target permission <em>t</em>, is said to
* {@linkplain #implies(Permission) <em>imply</em>} another permission <em>p</em>, iff:
* <ul>
* <li>p <em>is not</em> itself a denied permission, and <code>(t.implies(p) == true)</code>,
* or</li>
* <li>p <em>is</em> a denied permission, with a target <em>t1</em>, and
* <code>(t.implies(t1) == true)</code>.</li>
* </ul>
* <p>
* It is the responsibility of the policy decision point (e.g., the <code>Policy</code> provider) to
* take denied permission semantics into account when issuing authorization statements.
* </p>
*/
public final class DeniedPermission extends BasicPermission {
private static final String NULL_STR_ARG = "<null>", EMPTY_STR_ARG = "<empty> ";
private static final long serialVersionUID = 2102974454790623344L;
private final Permission target;
/**
* Instantiates a <code>DeniedPermission</code> that encapsulates a target permission of the
* indicated class, specified name and, optionally, actions.
*
* @throws IllegalArgumentException
* if:
* <ul>
* <li><code>targetClassName</code> is <code>null</code>, the empty string, does not
* refer to a concrete <code>Permission</code> descendant, or refers to
* <code>DeniedPermission.class</code> or <code>UnresolvedPermission.class</code>.</li>
* <li><code>targetName</code> is <code>null</code>.</li>
* <li><code>targetClassName</code> cannot be instantiated, and it's the caller's fault;
* e.g., because <code>targetName</code> and/or <code>targetActions</code> do not adhere
* to the naming constraints of the target class; or due to the target class not
* exposing a <code>(String name)</code>, or <code>(String name, String actions)</code>
* constructor, depending on whether <code>targetActions</code> is <code>null</code> or
* not.</li>
* </ul>
* @throws SecurityException
* if a <code>SecurityManager</code>, <code>sm</code>, is installed, and the invocation
* <code>sm.checkPackageAccess(targetClassPackageName)</code> (where
* <code>targetClassPackageName</code> is the package of the class referred to
* by <code>targetClassName</code>) denies access.
*/
public static DeniedPermission newDeniedPermission(String targetClassName, String targetName,
String targetActions) {
if (targetClassName == null || targetClassName.trim().isEmpty() || targetName == null) {
throw new IllegalArgumentException("[targetClassName] and [targetName] must not be null or empty.");
}
StringBuilder sb = new StringBuilder(targetClassName).append(":").append(targetName);
if (targetName != null) {
sb.append(":").append(targetName);
}
return new DeniedPermission(sb.toString());
}
/**
* Instantiates a <code>DeniedPermission</code> that encapsulates the given target permission.
*
* @throws IllegalArgumentException
* if <code>target</code> is <code>null</code>, a <code>DeniedPermission</code>, or an
* <code>UnresolvedPermission</code>.
*/
public static DeniedPermission newDeniedPermission(Permission target) {
if (target == null) {
throw new IllegalArgumentException("[target] must not be null.");
}
if (target instanceof DeniedPermission || target instanceof UnresolvedPermission) {
throw new IllegalArgumentException("[target] must not be a DeniedPermission or an UnresolvedPermission.");
}
StringBuilder sb = new StringBuilder(target.getClass().getName()).append(":").append(target.getName());
String targetActions = target.getActions();
if (targetActions != null) {
sb.append(":").append(targetActions);
}
return new DeniedPermission(sb.toString(), target);
}
private static Permission constructTargetPermission(String targetClassName, String targetName,
String targetActions) {
Class<?> targetClass;
try {
targetClass = Class.forName(targetClassName);
}
catch (ClassNotFoundException cnfe) {
if (targetClassName.trim().isEmpty()) {
targetClassName = EMPTY_STR_ARG;
}
throw new IllegalArgumentException(
MessageFormat.format("Target Permission class [{0}] not found.", targetClassName));
}
if (!Permission.class.isAssignableFrom(targetClass) || Modifier.isAbstract(targetClass.getModifiers())) {
throw new IllegalArgumentException(MessageFormat
.format("Target Permission class [{0}] is not a (concrete) Permission.", targetClassName));
}
if (targetClass == DeniedPermission.class || targetClass == UnresolvedPermission.class) {
throw new IllegalArgumentException(
"Target Permission class must not be a DeniedPermission itself, nor an UnresolvedPermission.");
}
Constructor<?> targetCtor;
try {
if (targetActions == null) {
targetCtor = targetClass.getConstructor(String.class);
}
else {
targetCtor = targetClass.getConstructor(String.class, String.class);
}
}
catch (NoSuchMethodException nsme) {
throw new IllegalArgumentException(MessageFormat.format(
"Target Permission class [{0}] (String name) or (String name, String actions) constructor.",
targetClassName));
}
try {
return (Permission) targetCtor
.newInstance(((targetCtor.getParameterCount() == 1) ? new Object[] { targetName }
: new Object[] { targetName, targetActions }));
}
catch (ReflectiveOperationException roe) {
if (roe instanceof InvocationTargetException) {
if (targetName == null) {
targetName = NULL_STR_ARG;
}
else if (targetName.trim().isEmpty()) {
targetName = EMPTY_STR_ARG;
}
if (targetActions == null) {
targetActions = NULL_STR_ARG;
}
else if (targetActions.trim().isEmpty()) {
targetActions = EMPTY_STR_ARG;
}
throw new IllegalArgumentException(MessageFormat.format(
"Could not instantiate target Permission class [{0}]; provided target name [{1}] and/or target [{2}] actions potentially erroneous.",
targetClassName, targetName, targetActions), roe);
}
throw new RuntimeException(MessageFormat.format(
"Could not instantiate target Permission class [{0}] - an unforeseen error occurred, see attached cause for details.",
targetClassName), roe);
}
}
/**
* Instantiates a <code>DeniedPermission</code> that encapsulates a target permission of the class,
* name and, optionally, actions, collectively provided as the <code>name</code> argument.
*
* @throws IllegalArgumentException
* if:
* <ul>
* <li><code>name</code>'s target permission class name component is empty, does not
* refer to a concrete <code>Permission</code> descendant, or refers to
* <code>DeniedPermission.class</code> or <code>UnresolvedPermission.class</code>.</li>
* <li><code>name</code>'s target name component is <code>empty</code></li>
* <li>the target permission class cannot be instantiated, and it's the caller's fault;
* e.g., because <code>name</code>'s target name and/or target actions component(s) do
* not adhere to the naming constraints of the target class; or due to the target class
* not exposing a <code>(String name)</code>, or
* <code>(String name, String actions)</code> constructor, depending on whether the
* target actions component is empty or not.</li>
* </ul>
* @throws SecurityException
* if a <code>SecurityManager</code>, <code>sm</code>, is installed, and the invocation
* <code>sm.checkPackageAccess(targetClassPackageName)</code>
* (where <code>targetClassPackageName</code> is the package of the class referred to
* by <code>name</code>'s target name component) denies access.
*/
public DeniedPermission(String name) {
super(name);
String[] comps = name.split(":");
if (comps.length < 2) {
throw new IllegalArgumentException(MessageFormat.format("Malformed [name] argument: {0}", name));
}
this.target = constructTargetPermission(comps[0], comps[1], ((comps.length < 3) ? null : comps[2]));
}
private DeniedPermission(String name, Permission target) {
super(name);
this.target = target;
}
/**
* Checks whether the given permission is implied by this one, as per the
* {@linkplain DeniedPermission overview}.
*/
@Override
public boolean implies(Permission p) {
if (p instanceof DeniedPermission) {
return target.implies(((DeniedPermission) p).target);
}
return target.implies(p);
}
/**
* Returns this denied permission's target permission.
*/
public Permission getTargetPermission() {
return target;
}
}
The DenyingPolicy
class
package com.example.q5003565;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Policy;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.UnresolvedPermission;
import java.util.Enumeration;
/**
* Wrapper that adds rudimentary {@link DeniedPermission} processing capabilities to the standard
* file-backed <code>Policy</code>.
*/
public final class DenyingPolicy extends Policy {
/*
* doPrivileged needed just in case there's already a SecurityManager installed at class loading
* time.
*/
private static final ProtectionDomain OWN_PD = AccessController
.doPrivileged((PrivilegedAction<ProtectionDomain>) DenyingPolicy.class::getProtectionDomain);
private final Policy defaultPolicy;
{
try {
// will fail unless the calling acc has SecurityPermission "createPolicy.javaPolicy"
defaultPolicy = Policy.getInstance("javaPolicy", null, "SUN");
}
catch (NoSuchProviderException | NoSuchAlgorithmException e) {
throw new RuntimeException("Could not acquire default Policy.", e);
}
}
@Override
public PermissionCollection getPermissions(CodeSource codesource) {
return defaultPolicy.getPermissions(codesource);
}
@Override
public PermissionCollection getPermissions(ProtectionDomain domain) {
return defaultPolicy.getPermissions(domain);
}
/**
* @return <code>true</code> iff:
* <ul>
* <li><code>permission</code> <em>is not</em> an instance of
* <code>DeniedPermission</code>,</li>
* <li>an <code>implies(domain, permission)</code> invocation on the system-default
* <code>Policy</code> yields <code>true</code>, and</li>
* <li><code>permission</code> <em>is not</em> implied by any <code>DeniedPermission</code>s
* having potentially been assigned to <code>domain</code>.</li>
* </ul>
*/
@Override
public boolean implies(ProtectionDomain domain, Permission permission) {
if (OWN_PD.equals(domain)) {
/*
* Recursive invocation due to a privilege-requiring method we invoked. If you're uncomfortable with
* this, get rid of it and grant (via .policy) a RuntimePermission "accessClassInPackage.*" to
* OWN_PD.
*/
return true;
}
if (permission instanceof DeniedPermission) {
/*
* At the policy decision level, DeniedPermissions can only themselves imply, not be implied (as
* they take away, rather than grant, privileges). Returning true for a deny rule would be
* more confusing than convenient.
*/
return false;
}
if (!defaultPolicy.implies(domain, permission)) {
// permission not granted--no need to check whether denied
return false;
}
/*
* Permission granted--now check whether there's an overriding DeniedPermission. The following
* assumes that defaultPolicy (its wrapped PolicySpi) is a sun.security.provider.PolicySpiFile
* (other implementations might not support #getPermissions(ProtectionDomain)
* and/or handle resolution of UnresolvedPermissions differently).
*/
Enumeration<Permission> perms = defaultPolicy.getPermissions(domain).elements();
while (perms.hasMoreElements()) {
Permission p = perms.nextElement();
/*
* DeniedPermissions will generally remain unresolved, as no code is expected to check whether other
* code has been "granted" such a permission.
*/
if (p instanceof UnresolvedPermission) {
UnresolvedPermission up = (UnresolvedPermission) p;
if (up.getUnresolvedType().equals(DeniedPermission.class.getName())) {
// force resolution
defaultPolicy.implies(domain, up);
// evaluate right away, to avoid reiterating over the collection
p = AccessController.doPrivileged(
(PrivilegedAction<Permission>) () -> new DeniedPermission(up.getUnresolvedName()));
}
}
if (p instanceof DeniedPermission && p.implies(permission)) {
// permission denied
return false;
}
}
// permission granted
return true;
}
@Override
public void refresh() {
defaultPolicy.refresh();
}
}
Usage
Just embed DeniedPermission
s within plain old grant rules; for instance, the following rule will grant everything but the ability to read System Properties with a name starting with "user.", to some.jar's classes.
grant codeBase "file:/home/your_user/classpath/some.jar" {
permission java.security.AllPermission;
permission com.example.q5003565.DeniedPermission "java.util.PropertyPermission:user.*:read";
};
Then install a DenyingPolicy
via Policy.setPolicy(new DenyingPolicy());
.
Caveat: While semantically correct, as was mentioned in a previous answer's comment, the above example is ineffective, as it still grants dangerous permissions, such as SecurityPermission "setPolicy"
, which implicitly allow sandboxed code to do whatever it pleases, including the action prohibited by the DeniedPermission
. To prevent this from occurring, rather than subtracting permissions from AllPermission
, consider subtracting from an AllSafePermission
instead, where AllSafePermission
is defined such that it implies
everything except known sandbox-defeating permissions1.
Notes
- Any permission can be wrapped by a denied permission, as long as it follows the standard target name-action(s) convention, exposes a
(String)
and/or (String, String)
constructor, and appropriately overrides implies(Permission)
.
- To deny multiple permissions at once:
- Create an ordinary permission subclass that
implies
the to-be-denied permissions.
- "Grant" a denied permission, in turn referencing an instance of your implementation, from the policy configuration.
- The
DenyingPolicy
does not prevent permissions statically assigned to a protection domain (such as RuntimePermission "exitVM.*"
granted by default to code originating from the classpath) from being granted, as, generally, evaluation of such permissions occurs prior to evaluation of permissions maintained by the policy. In order to deny any of these permissions as well, you will have to replace the ClassLoader
with one that:
- does either not grant the permissions in the first place, or
- maps classes it loads to instances of a
ProtectionDomain
subclass, that overrides implies(Permission)
such that:
- it always delegates to policy, or
- processes
DeniedPermission
s in a manner similar to DenyingPolicy
.
1: For a listing of such permissions see e.g. Maass, M. (2016). A Theory and Tools for Applying Sandboxes Effectively., table 3.1 (page 47).