blob: 33d4003b81b0f61d9a5f983d198ee733c170a6c1 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2016 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Connexta, LLC - performance improvements
*******************************************************************************/
package org.eclipse.osgi.internal.permadmin;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.Permission;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.osgi.framework.Bundle;
import org.osgi.service.condpermadmin.Condition;
import org.osgi.service.condpermadmin.ConditionInfo;
import org.osgi.service.condpermadmin.ConditionalPermissionInfo;
import org.osgi.service.permissionadmin.PermissionInfo;
public final class SecurityRow implements ConditionalPermissionInfo {
/* Used to find condition constructors getConditions */
static final Class<?>[] conditionMethodArgs = new Class[] {Bundle.class, ConditionInfo.class};
static Condition[] ABSTAIN_LIST = new Condition[0];
static Condition[] SATISFIED_LIST = new Condition[0];
static final Decision DECISION_ABSTAIN = new Decision(SecurityTable.ABSTAIN, null, null, null);
static final Decision DECISION_GRANTED = new Decision(SecurityTable.GRANTED, null, null, null);
static final Decision DECISION_DENIED = new Decision(SecurityTable.DENIED, null, null, null);
private final SecurityAdmin securityAdmin;
private final String name;
private final ConditionInfo[] conditionInfos;
private final PermissionInfoCollection permissionInfoCollection;
private final boolean deny;
/* GuardedBy(bundleConditions) */
final Map<BundlePermissions, Condition[]> bundleConditions;
final Object bundleConditionsLock = new Object();
public SecurityRow(SecurityAdmin securityAdmin, String name, ConditionInfo[] conditionInfos, PermissionInfo[] permissionInfos, String decision) {
if (permissionInfos == null || permissionInfos.length == 0)
throw new IllegalArgumentException("It is invalid to have empty permissionInfos"); //$NON-NLS-1$
this.securityAdmin = securityAdmin;
this.conditionInfos = conditionInfos == null ? new ConditionInfo[0] : conditionInfos;
decision = decision.toLowerCase();
boolean d = ConditionalPermissionInfo.DENY.equals(decision);
boolean a = ConditionalPermissionInfo.ALLOW.equals(decision);
if (!(d | a))
throw new IllegalArgumentException("Invalid decision: " + decision); //$NON-NLS-1$
this.deny = d;
this.name = name;
this.permissionInfoCollection = new PermissionInfoCollection(permissionInfos);
if (conditionInfos == null || conditionInfos.length == 0)
bundleConditions = null;
else
bundleConditions = new HashMap<>();
}
static SecurityRowSnapShot createSecurityRowSnapShot(String encoded) {
return (SecurityRowSnapShot) createConditionalPermissionInfo(null, encoded);
}
static SecurityRow createSecurityRow(SecurityAdmin securityAdmin, String encoded) {
return (SecurityRow) createConditionalPermissionInfo(securityAdmin, encoded);
}
private static ConditionalPermissionInfo createConditionalPermissionInfo(SecurityAdmin securityAdmin, String encoded) {
encoded = encoded.trim();
if (encoded.length() == 0)
throw new IllegalArgumentException("Empty encoded string is invalid"); //$NON-NLS-1$
char[] chars = encoded.toCharArray();
int end = encoded.length() - 1;
char lastChar = chars[end];
if (lastChar != '}' && lastChar != '"')
throw new IllegalArgumentException(encoded);
String encodedName = null;
if (lastChar == '"') {
// we have a name: an empty name must have at least 2 chars for the quotes
if (chars.length < 2)
throw new IllegalArgumentException(encoded);
int endName = encoded.length() - 1;
int startName = endName - 1;
while (startName > 0) {
if (chars[startName] == '"') {
startName--;
if (startName > 0 && chars[startName] == '\\')
startName--;
else {
startName++;
break;
}
}
startName--;
}
if (chars[startName] != '"')
throw new IllegalArgumentException(encoded);
encodedName = unescapeString(encoded.substring(startName + 1, endName));
end = encoded.lastIndexOf('}', startName);
}
int start = encoded.indexOf('{');
if (start < 0 || end < start)
throw new IllegalArgumentException(encoded);
String decision = encoded.substring(0, start);
decision = decision.trim();
if (decision.length() == 0 || (!ConditionalPermissionInfo.DENY.equalsIgnoreCase(decision) && !ConditionalPermissionInfo.ALLOW.equalsIgnoreCase(decision)))
throw new IllegalArgumentException(encoded);
List<ConditionInfo> condList = new ArrayList<>();
List<PermissionInfo> permList = new ArrayList<>();
int pos = start + 1;
while (pos < end) {
while (pos < end && chars[pos] != '[' && chars[pos] != '(')
pos++;
if (pos == end)
break; // no perms or conds left
int startPos = pos;
char endChar = chars[startPos] == '[' ? ']' : ')';
while (pos < end && chars[pos] != endChar) {
if (chars[pos] == '"') {
pos++;
while (chars[pos] != '"') {
if (chars[pos] == '\\')
pos++;
pos++;
}
}
pos++;
}
int endPos = pos;
String token = new String(chars, startPos, endPos - startPos + 1);
if (endChar == ']')
condList.add(new ConditionInfo(token));
else
permList.add(new PermissionInfo(token));
pos++;
}
if (permList.size() == 0)
throw new IllegalArgumentException("No Permission infos: " + encoded); //$NON-NLS-1$
ConditionInfo[] conds = condList.toArray(new ConditionInfo[condList.size()]);
PermissionInfo[] perms = permList.toArray(new PermissionInfo[permList.size()]);
if (securityAdmin == null)
return new SecurityRowSnapShot(encodedName, conds, perms, decision);
return new SecurityRow(securityAdmin, encodedName, conds, perms, decision);
}
static Object cloneArray(Object[] array) {
if (array == null)
return null;
Object result = Array.newInstance(array.getClass().getComponentType(), array.length);
System.arraycopy(array, 0, result, 0, array.length);
return result;
}
private static void escapeString(String str, StringBuffer output) {
int len = str.length();
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
switch (c) {
case '"' :
case '\\' :
output.append('\\');
output.append(c);
break;
case '\r' :
output.append("\\r"); //$NON-NLS-1$
break;
case '\n' :
output.append("\\n"); //$NON-NLS-1$
break;
default :
output.append(c);
break;
}
}
}
private static String unescapeString(String str) {
StringBuffer output = new StringBuffer(str.length());
int end = str.length();
for (int i = 0; i < end; i++) {
char c = str.charAt(i);
if (c == '\\') {
i++;
if (i < end) {
c = str.charAt(i);
switch (c) {
case '"' :
case '\\' :
break;
case 'r' :
c = '\r';
break;
case 'n' :
c = '\n';
break;
default :
c = '\\';
i--;
break;
}
}
}
output.append(c);
}
return output.toString();
}
public String getName() {
return name;
}
public ConditionInfo[] getConditionInfos() {
// must make a copy for the public API method to prevent modification
return (ConditionInfo[]) cloneArray(conditionInfos);
}
ConditionInfo[] internalGetConditionInfos() {
return conditionInfos;
}
public String getAccessDecision() {
return deny ? ConditionalPermissionInfo.DENY : ConditionalPermissionInfo.ALLOW;
}
public PermissionInfo[] getPermissionInfos() {
// must make a copy for the public API method to prevent modification
return (PermissionInfo[]) cloneArray(permissionInfoCollection.getPermissionInfos());
}
PermissionInfo[] internalGetPermissionInfos() {
return permissionInfoCollection.getPermissionInfos();
}
/**
* @deprecated
*/
public void delete() {
securityAdmin.delete(this, true);
}
Condition[] getConditions(BundlePermissions bundlePermissions) {
synchronized (bundleConditionsLock) {
Condition[] conditions = null;
if (bundleConditions != null) {
conditions = bundleConditions.get(bundlePermissions);
}
if (conditions == null) {
conditions = new Condition[conditionInfos.length];
for (int i = 0; i < conditionInfos.length; i++) {
/*
* TODO: Can we pre-get the Constructors in our own constructor
*/
Class<?> clazz;
try {
clazz = Class.forName(conditionInfos[i].getType());
} catch (ClassNotFoundException e) {
/* If the class isn't there, we fail */
return null;
}
Constructor<?> constructor = null;
Method method = getConditionMethod(clazz);
if (method == null) {
constructor = getConditionConstructor(clazz);
if (constructor == null) {
// TODO should post a FrameworkEvent of type error here
conditions[i] = Condition.FALSE;
continue;
}
}
Object[] args = {bundlePermissions.getBundle(), conditionInfos[i]};
try {
if (method != null)
conditions[i] = (Condition) method.invoke(null, args);
else
conditions[i] = (Condition) constructor.newInstance(args);
} catch (Exception e) {
// TODO should post a FrameworkEvent of type error here
conditions[i] = Condition.FALSE;
}
}
if (bundleConditions != null) {
bundleConditions.put(bundlePermissions, conditions);
}
}
return conditions;
}
}
private Method getConditionMethod(Class<?> clazz) {
for (Method checkMethod : clazz.getMethods()) {
if (checkMethod.getName().equals("getCondition") //$NON-NLS-1$
&& (checkMethod.getModifiers() & Modifier.STATIC) == Modifier.STATIC //
&& checkParameterTypes(checkMethod.getParameterTypes())) {
return checkMethod;
}
}
return null;
}
private Constructor<?> getConditionConstructor(Class<?> clazz) {
for (Constructor<?> checkConstructor : clazz.getConstructors()) {
if (checkParameterTypes(checkConstructor.getParameterTypes())) {
return checkConstructor;
}
}
return null;
}
private boolean checkParameterTypes(Class<?>[] foundTypes) {
if (foundTypes.length != conditionMethodArgs.length) {
return false;
}
for (int i = 0; i < foundTypes.length; i++) {
if (!foundTypes[i].isAssignableFrom(conditionMethodArgs[i])) {
return false;
}
}
return true;
}
Decision evaluate(BundlePermissions bundlePermissions, Permission permission) {
if (bundleConditions == null || bundlePermissions == null)
return evaluatePermission(permission);
Condition[] conditions = getConditions(bundlePermissions);
if (conditions == ABSTAIN_LIST)
return DECISION_ABSTAIN;
if (conditions == SATISFIED_LIST)
return evaluatePermission(permission);
boolean empty = true;
List<Condition> postponedConditions = null;
Decision postponedPermCheck = null;
for (int i = 0; i < conditions.length; i++) {
Condition condition = conditions[i];
if (condition == null)
continue; // this condition must have been satisfied && !mutable in a previous check
if (!isPostponed(condition)) {
// must call isMutable before calling isSatisfied according to the specification.
boolean mutable = condition.isMutable();
if (condition.isSatisfied()) {
if (!mutable)
conditions[i] = null; // ignore this condition for future checks
} else {
if (!mutable)
// this will cause the row to always abstain; mark this to be ignored in future checks
synchronized (bundleConditionsLock) {
bundleConditions.put(bundlePermissions, ABSTAIN_LIST);
}
return DECISION_ABSTAIN;
}
} else { // postponed case
if (postponedPermCheck == null)
// perform a permission check now
postponedPermCheck = evaluatePermission(permission);
if (postponedPermCheck == DECISION_ABSTAIN)
return postponedPermCheck; // no need to postpone the condition if the row abstains
// this row will deny or allow the permission; must queue the postponed condition
if (postponedConditions == null)
postponedConditions = new ArrayList<>(1);
postponedConditions.add(condition);
}
empty &= conditions[i] == null;
}
if (empty) {
synchronized (bundleConditionsLock) {
bundleConditions.put(bundlePermissions, SATISFIED_LIST);
}
}
if (postponedPermCheck != null)
return new Decision(postponedPermCheck.decision | SecurityTable.POSTPONED, postponedConditions.toArray(new Condition[postponedConditions.size()]), this, bundlePermissions);
return evaluatePermission(permission);
}
private boolean isPostponed(Condition condition) {
// postponed checks can only happen if we are using a supported security manager
return condition.isPostponed() && securityAdmin.getSupportedSecurityManager() != null;
}
private Decision evaluatePermission(Permission permission) {
return permissionInfoCollection.implies(permission) ? (deny ? DECISION_DENIED : DECISION_GRANTED) : DECISION_ABSTAIN;
}
public String toString() {
return getEncoded();
}
public String getEncoded() {
return getEncoded(name, conditionInfos, internalGetPermissionInfos(), deny);
}
public boolean equals(Object obj) {
// doing the simple (slow) thing for now
if (obj == this)
return true;
if (!(obj instanceof ConditionalPermissionInfo))
return false;
// we assume the encoded string provides a canonical (comparable) form
return getEncoded().equals(((ConditionalPermissionInfo) obj).getEncoded());
}
public int hashCode() {
return getHashCode(name, internalGetConditionInfos(), internalGetPermissionInfos(), getAccessDecision());
}
static int getHashCode(String name, ConditionInfo[] conds, PermissionInfo[] perms, String decision) {
int h = 31 * 17 + decision.hashCode();
for (int i = 0; i < conds.length; i++)
h = 31 * h + conds[i].hashCode();
for (int i = 0; i < perms.length; i++)
h = 31 * h + perms[i].hashCode();
if (name != null)
h = 31 * h + name.hashCode();
return h;
}
static String getEncoded(String name, ConditionInfo[] conditionInfos, PermissionInfo[] permissionInfos, boolean deny) {
StringBuffer result = new StringBuffer();
if (deny)
result.append(ConditionalPermissionInfo.DENY);
else
result.append(ConditionalPermissionInfo.ALLOW);
result.append(" { "); //$NON-NLS-1$
if (conditionInfos != null)
for (int i = 0; i < conditionInfos.length; i++)
result.append(conditionInfos[i].getEncoded()).append(' ');
if (permissionInfos != null)
for (int i = 0; i < permissionInfos.length; i++)
result.append(permissionInfos[i].getEncoded()).append(' ');
result.append('}');
if (name != null) {
result.append(" \""); //$NON-NLS-1$
escapeString(name, result);
result.append('"');
}
return result.toString();
}
PermissionInfoCollection getPermissionInfoCollection() {
return permissionInfoCollection;
}
void clearCaches() {
permissionInfoCollection.clearPermissionCache();
if (bundleConditions != null)
synchronized (bundleConditionsLock) {
bundleConditions.clear();
}
}
static class Decision {
final int decision;
final Condition[] postponed;
private final SecurityRow row;
private final BundlePermissions bundlePermissions;
Decision(int decision, Condition[] postponed, SecurityRow row, BundlePermissions bundlePermissions) {
this.decision = decision;
this.postponed = postponed;
this.row = row;
this.bundlePermissions = bundlePermissions;
}
void handleImmutable(Condition condition, boolean isSatisfied, boolean mutable) {
if (mutable || !condition.isPostponed())
return; // do nothing
if (isSatisfied) {
synchronized (row.bundleConditionsLock) {
Condition[] rowConditions = row.bundleConditions.get(bundlePermissions);
boolean isEmpty = true;
for (int i = 0; i < rowConditions.length; i++) {
if (rowConditions[i] == condition)
if (isSatisfied)
rowConditions[i] = null;
isEmpty &= rowConditions[i] == null;
}
if (isEmpty)
row.bundleConditions.put(bundlePermissions, SATISFIED_LIST);
}
} else {
synchronized (row.bundleConditionsLock) {
row.bundleConditions.put(bundlePermissions, ABSTAIN_LIST);
}
}
}
}
}