/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.karaf.service.guard.tools;
import java.util.*;
import org.apache.karaf.service.guard.impl.GuardProxyCatalog;
public class ACLConfigurationParser {
// note that the order of the enums is important. Needs to be from most specific to least specific.
public enum Specificity {
ARGUMENT_MATCH,
SIGNATURE_MATCH,
NAME_MATCH,
WILDCARD_MATCH,
NO_MATCH
};
static String compulsoryRoles;
static {
compulsoryRoles = System.getProperty(GuardProxyCatalog.KARAF_SECURED_COMMAND_COMPULSORY_ROLES_PROPERTY);
}
/**
* <p>Returns the roles that can invoke the given operation. This is determined by matching the
* operation details against configuration provided.</p>
*
* <p>The following configuration is supported. Keys are used to match an invocation against. The value can contain
* a comma-separated list of roles. Spaces are ignored for the role values. Note that comments are allowed in the
* value field after the hash {@code #} character:</p>
*
* <pre>
* {@code
* myMethod = role1, role2
* methodName(int)[/17/] = role1 # regex match, assume it's surrounded by ^ and $
* methodName(int)[/[01]8/] = role2
* methodName(int)["19"] = role3 # exact value match
* methodName(int) = role4 # signature match
* methodName(java.lang.String, int) = role5 # signature match
* methodName = # no roles can invoke this command
* method* = role6 # name prefix/wildcard match. The asterisk must appear at the end.
* }
* </pre>
*
* <p>The following algorithm is used to find matching roles:</p>
* <ol>
* <li>Find all regex and exact value matches. For all parameters these matches are found by calling {@code toString()}
* on the parameters passed in. If there are multiple matches in this category all the matching roles are collected.
* If any is found return these roles.
* </li>
* <li>Find a signature match. If found return the associated roles.</li>
* <li>Find a method name match. If found return the associated roles.</li>
* <li>Find a method name prefix/wildcard match. If more than one prefix match, the roles associated with the longest
* prefix is used. So for example, if there are rules for {@code get*} and {@code *} only the roles associated with
* {@code get*} are returned.
* </li>
* <li>If none of the above criteria match, this method returns {@code null}.</li>
* </ol>
*
* @param methodName the method name to be invoked.
* @param params the parameters provided for the invocation. May be {@code null} for cases there the parameters are not yet
* known. In this case the roles that can <em>potentially</em> invoke the method are returned, although based on
* parameter values the actual invocation may still be denied.
* @param signature the signature of the method specified as an array of class name. For simple types, the simple type name
* is used (e.g. "int").
* @param config the configuration to check against.
* @param addToRoles the list of roles (which may be empty) if a matching configuration iteam has been found.
* @return the specificity
*/
public static Specificity getRolesForInvocation(String methodName, Object[] params, String[] signature,
Dictionary<String, Object> config, List<String> addToRoles) {
Dictionary<String, Object> properties = trimKeys(config);
Specificity s = getRolesBasedOnSignature(methodName, params, signature, properties, addToRoles);
if (s != Specificity.NO_MATCH) {
return s;
}
s = getRolesBasedOnSignature(methodName, params, null, properties, addToRoles);
if (s != Specificity.NO_MATCH) {
return s;
}
List<String> roles = getMethodNameWildcardRoles(properties, methodName);
if (roles != null) {
addToRoles.addAll(roles);
return Specificity.WILDCARD_MATCH;
} else if (compulsoryRoles != null){
addToRoles.addAll(ACLConfigurationParser.parseRoles(compulsoryRoles));
return Specificity.NAME_MATCH;
} else {
return Specificity.NO_MATCH;
}
}
private static Specificity getRolesBasedOnSignature(String methodName, Object[] params, String[] signature,
Dictionary<String, Object> properties, List<String> roles) {
if (params != null) {
boolean foundExactOrRegex = false;
Object exactArgMatchRoles = properties.get(getExactArgSignature(methodName, signature, params));
if (exactArgMatchRoles instanceof String) {
roles.addAll(parseRoles((String) exactArgMatchRoles));
foundExactOrRegex = true;
}
List<String> r = getRegexRoles(properties, methodName, signature, params);
if (r != null) {
foundExactOrRegex = true;
roles.addAll(r);
}
if (foundExactOrRegex) {
// since we have the actual parameters we can match them and if they do we won't look for any
// more generic rules...
return Specificity.ARGUMENT_MATCH;
}
} else {
// this is used in the case where parameters aren't known yet and the system wants to find out
// what roles in principle can invoke this method
List<String> r = getExactArgOrRegexRoles(properties, methodName, signature);
if (r != null) {
roles.addAll(r);
}
}
Object signatureRoles = properties.get(getSignature(methodName, signature));
if (signatureRoles instanceof String) {
roles.addAll(parseRoles((String) signatureRoles));
return signature == null ? Specificity.NAME_MATCH : Specificity.SIGNATURE_MATCH;
}
return Specificity.NO_MATCH;
}
private static Dictionary<String, Object> trimKeys(Dictionary<String, Object> properties) {
Dictionary<String, Object> d = new Hashtable<String, Object>();
for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
String key = e.nextElement();
Object value = properties.get(key);
d.put(removeSpaces(key), value);
}
return d;
}
private static String removeSpaces(String key) {
StringBuilder sb = new StringBuilder();
char quoteChar = 0;
for (int i = 0; i < key.length(); i++) {
char c = key.charAt(i);
if (quoteChar == 0 && c == ' ')
continue;
if (quoteChar == 0 && (c == '\"' || c == '/') && sb.length() > 0 &&
(sb.charAt(sb.length() - 1) == '[' || sb.charAt(sb.length() - 1) == ',')) {
// we're in a quoted string
quoteChar = c;
} else if (quoteChar != 0 && c == quoteChar) {
// look ahead to see if the next non-space is the closing bracket or a comma, which ends the quoted string
for (int j = i + 1; j < key.length(); j++) {
if (key.charAt(j) == ' ')
continue;
if (key.charAt(j) == ']' || key.charAt(j) == ',')
quoteChar = 0;
break;
}
}
sb.append(c);
}
return sb.toString();
}
public static List<String> parseRoles(String roleStr) {
int hashIdx = roleStr.indexOf('#');
if (hashIdx >= 0) {
// you can put a comment at the end
roleStr = roleStr.substring(0, hashIdx);
}
List<String> roles = new ArrayList<String>();
for (String role : roleStr.split("[,]")) {
String trimmed = role.trim();
if (trimmed.length() > 0) {
roles.add(trimmed);
}
}
return roles;
}
private static Object getExactArgSignature(String methodName, String[] signature, Object[] params) {
StringBuilder sb = new StringBuilder(getSignature(methodName, signature));
sb.append('[');
boolean first = true;
for (Object param : params) {
if (first)
first = false;
else
sb.append(',');
sb.append('"');
if (param != null)
sb.append(param.toString().trim());
sb.append('"');
}
sb.append(']');
return sb.toString();
}
private static String getSignature(String methodName, String[] signature) {
StringBuilder sb = new StringBuilder(methodName);
if (signature == null)
return sb.toString();
sb.append('(');
boolean first = true;
for (String s : signature) {
if (first)
first = false;
else
sb.append(',');
sb.append(s);
}
sb.append(')');
return sb.toString();
}
private static List<String> getRegexRoles(Dictionary<String, Object> properties, String methodName, String[] signature, Object[] params) {
List<String> roles = new ArrayList<String>();
boolean matchFound = false;
String methodSig = getSignature(methodName, signature);
String prefix = methodSig + "[/";
for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
String key = e.nextElement().trim();
if (key.startsWith(prefix) && key.endsWith("/]")) {
List<String> regexArgs = getRegexDecl(key.substring(methodSig.length()));
if (allParamsMatch(regexArgs, params)) {
matchFound = true;
Object roleStr = properties.get(key);
if (roleStr instanceof String) {
roles.addAll(parseRoles((String) roleStr));
}
}
}
}
return matchFound ? roles : null;
}
private static List<String> getExactArgOrRegexRoles(Dictionary<String, Object> properties, String methodName, String[] signature) {
List<String> roles = new ArrayList<String>();
boolean matchFound = false;
String methodSig = getSignature(methodName, signature);
String prefix = methodSig + "[";
for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
String key = e.nextElement().trim();
if (key.startsWith(prefix) && key.endsWith("]")) {
matchFound = true;
Object roleStr = properties.get(key);
if (roleStr instanceof String) {
roles.addAll(parseRoles((String) roleStr));
}
}
}
return matchFound ? roles : null;
}
private static List<String> getMethodNameWildcardRoles(Dictionary<String, Object> properties, String methodName) {
SortedMap<String, String> wildcardRules = new TreeMap<String, String>(new Comparator<String>() {
public int compare(String s1, String s2) {
// returns longer entries before shorter ones...
return s2.length() - s1.length();
}
});
for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
String key = e.nextElement();
if (key.endsWith("*")) {
String prefix = key.substring(0, key.length() - 1);
if (methodName.startsWith(prefix)) {
wildcardRules.put(prefix, properties.get(key).toString());
}
}
if (key.startsWith("*")) {
String suffix = key.substring(1);
if (methodName.endsWith(suffix)) {
wildcardRules.put(suffix, properties.get(key).toString());
}
}
if (key.startsWith("*") && key.endsWith("*") && key.length() > 1) {
String middle = key.substring(1, key.length() - 1);
if (methodName.contains(middle)) {
wildcardRules.put(middle, properties.get(key).toString());
}
}
}
if (wildcardRules.size() != 0) {
return parseRoles(wildcardRules.values().iterator().next());
} else {
return null;
}
}
private static boolean allParamsMatch(List<String> regexArgs, Object[] params) {
if (regexArgs.size() != params.length)
return false;
for (int i = 0; i < regexArgs.size(); i++) {
if (params[i] == null)
return false;
if (!params[i].toString().trim().matches(regexArgs.get(i))) {
return false;
}
}
return true;
}
private static List<String> getRegexDecl(String key) {
List<String> l = new ArrayList<String>();
boolean inRegex = false;
StringBuilder curRegex = new StringBuilder();
for (int i = 0; i < key.length(); i++) {
if (!inRegex) {
if (key.length() > i + 1) {
String s = key.substring(i, i + 2);
if ("[/".equals(s) || ",/".equals(s)) {
inRegex = true;
i++;
continue;
}
}
} else {
String s = key.substring(i, i + 2);
if ("/]".equals(s) || "/,".equals(s)) {
l.add(curRegex.toString());
curRegex = new StringBuilder();
inRegex = false;
continue;
}
curRegex.append(key.charAt(i));
}
}
return l;
}
}