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
/******************************************************************************* * 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, StringBuilder 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) { StringBuilder output = new StringBuilder(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(); } @Override public String getName() { return name; } @Override public ConditionInfo[] getConditionInfos() { // must make a copy for the public API method to prevent modification return (ConditionInfo[]) cloneArray(conditionInfos); } ConditionInfo[] internalGetConditionInfos() { return conditionInfos; } @Override public String getAccessDecision() { return deny ? ConditionalPermissionInfo.DENY : ConditionalPermissionInfo.ALLOW; } @Override 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:
/** * @deprecated */
@Override 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; } @Override public String toString() { return getEncoded(); } @Override public String getEncoded() { return getEncoded(name, conditionInfos, internalGetPermissionInfos(), deny); } @Override 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()); } @Override 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 (ConditionInfo cond : conds) { h = 31 * h + cond.hashCode(); } for (PermissionInfo perm : perms) { h = 31 * h + perm.hashCode(); } if (name != null) h = 31 * h + name.hashCode(); return h; } static String getEncoded(String name, ConditionInfo[] conditionInfos, PermissionInfo[] permissionInfos, boolean deny) { StringBuilder result = new StringBuilder(); if (deny) result.append(ConditionalPermissionInfo.DENY); else result.append(ConditionalPermissionInfo.ALLOW); result.append(" { "); //$NON-NLS-1$ if (conditionInfos != null) for (ConditionInfo conditionInfo : conditionInfos) { result.append(conditionInfo.getEncoded()).append(' '); } if (permissionInfos != null) for (PermissionInfo permissionInfo : permissionInfos) { result.append(permissionInfo.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); } } } } }