/**
* Copyright (c) 2009--2014 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package com.redhat.rhn.common.security.acl;
import com.redhat.rhn.common.IllegalRegexException;
import com.redhat.rhn.common.MethodInvocationException;
import com.redhat.rhn.common.localization.LocalizationService;
import org.apache.log4j.Logger;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.MatchResult;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternCompiler;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
/**
* Class for handling ACLs.
* Register {@link AclHandler AclHandlers} with this class with
* {@link #Acl(String[])} and/or {@link #registerHandler(String)}.
* AclHandler implementations must have a no-arg constructor.
* AclHandler methods that begin with the prefix "acl" and have a signature
* like the following are registered as ACL handler methods that can
* be referenced in ACL strings.
* <pre>
* public boolean aclXXXX(Object context, String params[]);
* </pre>
* or
* <pre>
* public static boolean aclXXXX(Object context, String params[]);
* </pre>
* The handlers can then be referred to
* in ACL strings when {@link #evalAcl} is called.
*<p>
* ACL strings take the form:
* <pre>
* ACL := EXPRESSION [; EXPRESSION; ]+
* EXPRESSION := STATEMENT [ OR STATEMENT ]+
* </pre>
* A semicolon separating expressions implies an AND operation.
* <p>
* An expression uses AclHandlers registered through
* {@link #Acl(String[])} and/or {@link #registerHandler(String)}.
* ACL method names are changed to ACL handler names referenceable in
* expression using the following translation algorithm:
* <ul>
* <li>all letters are converted to lower case
* <li>words in the method name defined by mixedCase are separated
* by underscores (Example: aclCheckSomething()
* is referenced as check_something)
* <li>words can have all caps (Example: aclCheckURL() is referenced
* in an ACL expression as check_url)
* </ul>
* More examples:
* <table>
* <tr>
* <td>Method Name</td> <td>ACL Handler Name</td>
* </tr>
* <tr>
* <td>aclFooBar</td> <td>foo_bar</td>
* </tr>
* <tr>
* <td>aclTestSomeValue</td><td>test_some_value</td>
* </tr>
* <tr>
* <td>aclCheckXML</td> <td>check_xml</td>
* </tr>
* <tr>
* <td>aclCheckXMLFile</td><td>check_xml_file</td>
* </tr>
* <tr>
* <td>aclXMLCheck</td><td>xml_check</td>
* </tr>
* </table>
*
* The following demonstrates the use of the Acl class:
* <pre>
* Map context = new HashMap();
* context.put("thingamajig", "foo");
* context.put("doodad", "bar");
* context.put("widget", "baz");
*
* ...
*
* // we can register a default handler with the constructor that takes
* // an array of fully-qualified AclHandler implementations
* Acl acl = new Acl(
* new String[]{"com.redhat.rhn.security.acl.handlers.DefaultHandler"});
*
* // and later register additional handlers
* acl.registerHandler("com.redhat.rhn.security.acl.handlers.MyHandler");
*
* // all will return true
* boolean result = acl.evalAcl(context, "has_thingamajig(foo)");
* result = acl.evalAcl(context, "has_doodad(bar)");
* result = acl.evalAcl(context, "has_widget(baz)");
*
* </pre>
* DefaultHandler:
* <pre>
* package com.redhat.rhn.security.acl.handlers;
*
* import com.rhn.redhat.security.acl.AclHandler;
*
* public class DefaultHandler implements AclHandler {
* // return true if the context has the specified thingamajig
* public boolean aclHasThingmajig(Object context, String[] params) {
* Map map = (Map)context;
* String thingamajig = (String)map.get("thingamajig");
* return thingamajig.equals(params[0]);
* }
* }
* </pre>
* MyHandler:
* <pre>
* package com.redhat.rhn.security.acl.handlers;
*
* import com.rhn.redhat.security.acl.AclHandler;
*
* public class MyHandler implements AclHandler {
* // return true if the context has the specified doodad
* public boolean aclHasDooDad(Object context, String[] params) {
* Map map = (Map)context;
* String doodad = (String)map.get("doodad");
* return doodad.equals(params[0]);
* }
* // return true if the context has the specified widget
* public boolean aclHasWidget(Object context, String[] params) {
* Map map = (Map)context;
* String widget = (String)map.get("widget");
* return widget.equals(params[0]);
* }
* }
* </pre>
* @version $Rev$
*/
public class Acl {
/** RegEx to split ACL into multiple expressions */
private static final String ACL_SPLIT_REGEX = "\\s*;\\s*";
/** RegEx to split expressions into multiple statements */
private static final String EXPR_SPLIT_REGEX = "\\s+or\\s+";
/** RegEx to parse statement to grab negation, function call, params */
private static final String STMT_PARSE_REGEX = "^(not +)?(.*)\\((.*)\\)$";
/** RegEx to split params */
private static final String PARAM_SPLIT_REGEX = "\\s*,\\s*";
/** constant used to identify negation regex group within statement */
private static final int NEGATION_GROUP = 1;
/** constant used to identify handler name regex group within statement */
private static final int HANDLERNAME_GROUP = 2;
/** constant used to identify param regex group within statement */
private static final int PARAM_GROUP = 3;
/** total number of regex groups expected in statement */
private static final int EXPECTED_GROUPS = 4;
/** prefix of acl handler method names */
private static final String ACL_PREFIX = "acl";
/** The log instance for this class */
private static Logger log = Logger.getLogger(Acl.class);
/** Store acl handlers against keys referenced in acl statements */
private Map handlers = new HashMap();
/** store the compiled regex that will be re-used for evalAcl invocations */
private static Pattern parsePattern = null;
// initialize the parse pattern
static {
PatternCompiler compiler = new Perl5Compiler();
try {
parsePattern = compiler.compile(STMT_PARSE_REGEX);
}
catch (MalformedPatternException e) {
// we assume our regex is sane and tested
// and that we don't get here
throw new IllegalRegexException("Invalid when constructing parse " +
"pattern for acls.", e);
}
}
/** Constructor for a new Acl instance without any default ACL handlers. */
public Acl() {
// default constructor with no acl handlers
}
/** Creates a new Acl instance with the specified default ACL handler
* classes.
* @param defaultHandlerClasses an array of handler classes. Each entry
* must be a fully-qualified name of an implementation of
* {@link AclHandler}
* @see #registerHandler(String)
* @see #registerHandler(Class)
* @see #registerHandler(AclHandler)
* */
public Acl(String[] defaultHandlerClasses) {
for (int i = 0; i < defaultHandlerClasses.length; ++i) {
registerHandler(defaultHandlerClasses[i]);
}
}
/** Register an AclHandler class.
* @param aclClassname fully-qualified classname of an {@link AclHandler}
* implementation
* @see #registerHandler(AclHandler)
*/
public void registerHandler(String aclClassname) {
try {
Class clazz = Class.forName(aclClassname);
registerHandler(clazz);
}
catch (ClassNotFoundException e) {
IllegalArgumentException exc =
new IllegalArgumentException("class not found: " + aclClassname);
exc.initCause(e);
throw exc;
}
}
/** Register an AclHandler class.
* @param aclClazz an {@link AclHandler} implementation
* @see #registerHandler(AclHandler)
*/
public void registerHandler(Class aclClazz) {
try {
if (!AclHandler.class.isAssignableFrom(aclClazz)) {
throw new IllegalArgumentException(
LocalizationService.getInstance().getMessage("bad-class",
aclClazz.getName()));
}
AclHandler instance = (AclHandler)aclClazz.newInstance();
registerHandler(instance);
}
catch (InstantiationException e) {
IllegalArgumentException exc = new IllegalArgumentException();
exc.initCause(e);
throw exc;
}
catch (IllegalAccessException e) {
IllegalArgumentException exc = new IllegalArgumentException();
exc.initCause(e);
throw exc;
}
}
/** Register an AclHandler.
* All methods with the valid signature will be registered.
* <pre>
* public boolean aclXXX(Object, String[])
* </pre>
* or
* <pre>
* public static boolean aclXXX(Object, String[])
* </pre>
* Methods without the "acl" prefix are ignored. If a method begins
* with the "acl" prefix but the method signature is invalid, a
* warning is logged and the method is ignored.
*
* @param aclHandler AclHandler
*/
public void registerHandler(AclHandler aclHandler) {
try {
Class clazz = aclHandler.getClass();
// find all the acl* methods. and store them
BeanInfo info = Introspector.getBeanInfo(clazz);
MethodDescriptor[] methodDescriptors = info.getMethodDescriptors();
for (int i = 0; i < methodDescriptors.length; ++i) {
MethodDescriptor methodDescriptor = methodDescriptors[i];
String methodName = methodDescriptor.getName();
// we only care about methods with signatures:
// public boolean aclXXX(Object obj, String[] params);
if (!methodName.startsWith(ACL_PREFIX)) {
continue;
}
Method method = methodDescriptor.getMethod();
Class[] params = method.getParameterTypes();
if (!method.getReturnType().equals(Boolean.TYPE) ||
method.getExceptionTypes().length > 0 ||
params.length != 2 ||
!params[0].equals(Object.class) ||
!params[1].equals(String[].class)) {
log.warn(LocalizationService.getInstance().getMessage(
"bad-signature", method.toString()));
continue;
}
String aclName = methodNameToAclName(methodName);
handlers.put(aclName,
new InstanceMethodPair(aclHandler, method));
}
}
// from reading the javadocs for IntrospectionException,
// dont' really expect to get this one
catch (IntrospectionException e) {
IllegalArgumentException exc = new IllegalArgumentException();
exc.initCause(e);
throw exc;
}
}
/**
* Creates an ACL handler name from an ACL method name.
* See class description for sample conversions.
* @param name The ACL name to convert
* @return The corresponding method name.
*/
private String methodNameToAclName(String name) {
StringBuilder ret = new StringBuilder();
boolean lastWasLower = false;
ret.append(Character.toLowerCase(name.charAt(ACL_PREFIX.length())));
for (int i = ACL_PREFIX.length() + 1; i < name.length(); ++i) {
char ch = name.charAt(i);
boolean nextIsLower = false;
if (i + 1 < name.length()) {
nextIsLower = Character.isLowerCase(name.charAt(i + 1));
}
if (Character.isUpperCase(ch)) {
if (lastWasLower || nextIsLower) {
ret.append('_');
}
ret.append(Character.toLowerCase(ch));
lastWasLower = false;
}
else {
lastWasLower = true;
ret.append(ch);
}
}
return ret.toString();
}
/** Returns the set of registered ACL handler names.
* @return set of handler names usable in an ACL string
* */
public TreeSet getAclHandlerNames() {
return new TreeSet(handlers.keySet());
}
/** Evaluates an ACL string within a given context.
* See class description for sample usage.
* @param context context in which the acl string is evaluated
* @param acl the ACL string.
* @return true if the ACL string and given context allow access,
* false otherwise
* @see AclHandler
*/
public boolean evalAcl(Object context, String acl) {
if (log.isDebugEnabled()) {
log.debug("acl: " + acl);
}
// protect against nulls.
if (acl == null) {
throw new IllegalArgumentException(
LocalizationService.getInstance().getMessage(
"bad-syntax", acl));
}
String[] expressions = acl.split(ACL_SPLIT_REGEX);
int exprLen = expressions.length;
boolean result = false;
PatternMatcher matcher = new Perl5Matcher();
for (int exprIdx = 0; exprIdx < exprLen; ++exprIdx) {
String expression = expressions[exprIdx];
if (log.isDebugEnabled()) {
log.debug("expression[" + exprIdx + "]: " + expression);
}
String[] statements = expression.split(EXPR_SPLIT_REGEX);
int statementLen = statements.length;
for (int stmtIdx = 0; stmtIdx < statementLen; ++stmtIdx) {
String statement = statements[stmtIdx];
if (log.isDebugEnabled()) {
log.debug("statement[" + stmtIdx + "]: " + statement);
}
boolean itMatches = matcher.matches(statement, parsePattern);
MatchResult matchResult = matcher.getMatch();
if (!itMatches || matchResult == null || matchResult.groups() <
EXPECTED_GROUPS) {
throw new IllegalArgumentException(
LocalizationService.getInstance().getMessage(
"bad-syntax", statement));
}
if (log.isDebugEnabled()) {
log.debug("num groups: " + matchResult.groups());
log.debug("not: " + matchResult.group(NEGATION_GROUP));
log.debug("handler: " +
matchResult.group(HANDLERNAME_GROUP));
log.debug("params: " + matchResult.group(PARAM_GROUP));
}
boolean negated = matchResult.group(NEGATION_GROUP) != null;
String func = matchResult.group(HANDLERNAME_GROUP);
String params = matchResult.group(PARAM_GROUP);
InstanceMethodPair pair =
(InstanceMethodPair)handlers.get(func);
if (pair == null) {
Object[] args = new Object[3];
args[0] = func;
args[1] = statement;
args[2] = new TreeSet(handlers.keySet()).toString();
throw new IllegalArgumentException(
LocalizationService.getInstance().getMessage(
"bad-handler", args));
}
Method handler = pair.getMethod();
String[] paramArray = params.split(PARAM_SPLIT_REGEX);
// if no args were givien, make sure we pass a 0-length array
if (paramArray.length == 1 && paramArray[0].trim().equals("")) {
paramArray = new String[0];
}
try {
result = ((Boolean)handler.invoke(pair.getInstance(),
new Object[] {context, paramArray })).booleanValue();
}
// we shouldn't hit any of these exceptions, because the
// handler classes should have been adequately junit-tested
catch (IllegalAccessException iae) {
Object[] args = new Object[3];
args[0] = handler.getName();
args[1] = statement;
args[2] = iae.getMessage();
throw new MethodInvocationException(
LocalizationService.getInstance().getMessage(
"illegal-access", args), iae);
}
catch (InvocationTargetException ite) {
Object[] args = new Object[3];
args[0] = handler.getName();
args[1] = statement;
args[2] = ite.getMessage();
throw new MethodInvocationException(
LocalizationService.getInstance().getMessage(
"invocation-target-exception", args), ite);
}
if (negated) {
result = !result;
}
// break if we hit true, since we're in an or's loop
if (result) {
break;
}
}
// if we got a false, then return that, because we're in an
// and loop
if (!result) {
return result;
}
}
// if we got this far, all acl's passed
if (log.isDebugEnabled()) {
log.debug("acl: " + acl + " returning true");
}
return true;
}
private static class InstanceMethodPair {
private Method method;
private Object instance;
/**
* Create a new InstanceMethodPair
* @param obj The object on which to call the method
* @param meth The method to call
*/
InstanceMethodPair(Object obj, Method meth) {
instance = obj;
method = meth;
}
/**
* Get the object on which to invoke the method
* @return the object on which to invoke the method
*/
public Object getInstance() {
return instance;
}
/**
* Get the method
* @return the Method to invoke
*/
public Method getMethod() {
return method;
}
}
}