diff options
author | Robert Gemmell <robbie@apache.org> | 2010-05-31 16:04:45 +0000 |
---|---|---|
committer | Robert Gemmell <robbie@apache.org> | 2010-05-31 16:04:45 +0000 |
commit | 5b9f0c5168d17c2d61a114d32749f11a833eacd9 (patch) | |
tree | 746111a25934e4ffe37c26e192769702842b6eeb /java/broker-plugins/access-control/src/main | |
parent | 48e49bef0775e91625ba7b5c03823dbaca943bf7 (diff) | |
download | qpid-python-5b9f0c5168d17c2d61a114d32749f11a833eacd9.tar.gz |
QPID-2542: Implement ACL checking as OSGi plugin
Applied patch from Andrew Kennedy <andrew.international@gmail.com>
git-svn-id: https://svn.apache.org/repos/asf/qpid/trunk/qpid@949783 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'java/broker-plugins/access-control/src/main')
11 files changed, 1455 insertions, 0 deletions
diff --git a/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AbstractConfiguration.java b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AbstractConfiguration.java new file mode 100644 index 0000000000..f6728bd16e --- /dev/null +++ b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AbstractConfiguration.java @@ -0,0 +1,57 @@ +package org.apache.qpid.server.security.access.config; + +import java.io.File; + +import org.apache.commons.configuration.ConfigurationException; +import org.apache.log4j.Logger; + +public abstract class AbstractConfiguration implements ConfigurationFile +{ + protected static final Logger _logger = Logger.getLogger(ConfigurationFile.class); + + protected File _file; + protected RuleSet _config; + + public AbstractConfiguration(File file) + { + _file = file; + } + + public File getFile() + { + return _file; + } + + public RuleSet load() throws ConfigurationException + { + _config = new RuleSet(); + return _config; + } + + public RuleSet getConfiguration() + { + return _config; + } + + public boolean save(RuleSet configuration) + { + return true; + } + + public RuleSet reload() + { + RuleSet oldRules = _config; + + try + { + RuleSet newRules = load(); + _config = newRules; + } + catch (Exception e) + { + _config = oldRules; + } + + return _config; + } +} diff --git a/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/Action.java b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/Action.java new file mode 100644 index 0000000000..fdbd96e63e --- /dev/null +++ b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/Action.java @@ -0,0 +1,192 @@ +/* + * 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.qpid.server.security.access.config; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; +import org.apache.qpid.server.security.access.ObjectProperties; +import org.apache.qpid.server.security.access.ObjectType; +import org.apache.qpid.server.security.access.Operation; + +/** + * An access control v2 rule action. + * + * An action consists of an {@link Operation} on an {@link ObjectType} with certain properties, stored in a {@link Map}. + * The operation and object should be an allowable combination, based on the {@link ObjectType#isAllowed(Operation)} + * method of the object, which is exposed as the {@link #isAllowed()} method here. The internal {@link #propertiesMatch(Map)} + * and {@link #valueMatches(String, String)} methods are used to determine wildcarded matching of properties, with + * the empty string or "*" matching all values, and "*" at the end of a rule value indicating prefix matching. + * <p> + * The {@link #matches(Action)} method is intended to be used when determining precedence of rules, and + * {@link #equals(Object)} and {@link #hashCode()} are intended for use in maps. This is due to the wildcard matching + * described above. + */ +public class Action +{ + private Operation _operation; + private ObjectType _object; + private ObjectProperties _properties; + + public Action(Operation operation) + { + this(operation, ObjectType.ALL); + } + + public Action(Operation operation, ObjectType object, String name) + { + this(operation, object, new ObjectProperties(name)); + } + + public Action(Operation operation, ObjectType object) + { + this(operation, object, ObjectProperties.EMPTY); + } + + public Action(Operation operation, ObjectType object, ObjectProperties properties) + { + setOperation(operation); + setObjectType(object); + setProperties(properties); + } + + public Operation getOperation() + { + return _operation; + } + + public void setOperation(Operation operation) + { + _operation = operation; + } + + public ObjectType getObjectType() + { + return _object; + } + + public void setObjectType(ObjectType object) + { + _object = object; + } + + public ObjectProperties getProperties() + { + return _properties; + } + + public void setProperties(ObjectProperties properties) + { + _properties = properties; + } + + public boolean isAllowed() + { + return _object.isAllowed(_operation); + } + + /** @see Comparable#compareTo(Object) */ + public boolean matches(Action a) + { + return (Operation.ALL == a.getOperation() + || (getOperation() == a.getOperation() + && getObjectType() == a.getObjectType() + && _properties.matches(a.getProperties()))); + } + + /** + * An ordering based on specificity + * + * @see Comparator#compare(Object, Object) + */ + public class Specificity implements Comparator<Action> + { + public int compare(Action a, Action b) + { + if (a.getOperation() == Operation.ALL && b.getOperation() != Operation.ALL) + { + return 1; // B is more specific + } + else if (b.getOperation() == Operation.ALL && a.getOperation() != Operation.ALL) + { + return 1; // A is more specific + } + else if (a.getOperation() == b.getOperation()) + { + // Same operator, compare rest of action + +// || (getOperation() == a.getOperation() +// && getObjectType() == a.getObjectType() +// && _properties.matches(a.getProperties()))); + + return 1; // b is more specific + } + else // Different operations + { + return a.getOperation().compareTo(b.getOperation()); // Arbitrary + } + } + } + + /** @see Object#equals(Object) */ + @Override + public boolean equals(Object o) + { + if (!(o instanceof Action)) + { + return false; + } + Action a = (Action) o; + + return new EqualsBuilder() + .append(_operation, a.getOperation()) + .append(_object, a.getObjectType()) + .appendSuper(_properties.equals(a.getProperties())) + .isEquals(); + } + + /** @see Object#hashCode() */ + @Override + public int hashCode() + { + return new HashCodeBuilder() + .append(_operation) + .append(_operation) + .append(_properties) + .toHashCode(); + } + + /** @see Object#toString() */ + @Override + public String toString() + { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("operation", _operation) + .append("objectType", _object) + .append("properties", _properties) + .toString(); + } +} diff --git a/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/ConfigurationFile.java b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/ConfigurationFile.java new file mode 100644 index 0000000000..ad329a4f7c --- /dev/null +++ b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/ConfigurationFile.java @@ -0,0 +1,36 @@ +package org.apache.qpid.server.security.access.config; + +import java.io.File; + +import org.apache.commons.configuration.ConfigurationException; + +public interface ConfigurationFile +{ + /** + * Return the actual {@link File} object containing the configuration. + */ + File getFile(); + + /** + * Load this configuration file's contents into a {@link RuleSet}. + * + * @throws ConfigurationException if the configuration file has errors. + * @throws IllegalArgumentException if individual tokens cannot be parsed. + */ + RuleSet load() throws ConfigurationException; + + /** + * Reload this configuration file's contents. + * + * @throws ConfigurationException if the configuration file has errors. + * @throws IllegalArgumentException if individual tokens cannot be parsed. + */ + RuleSet reload() throws ConfigurationException; + + RuleSet getConfiguration(); + + /** + * TODO document me. + */ + boolean save(RuleSet configuration); +} diff --git a/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/PlainConfiguration.java b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/PlainConfiguration.java new file mode 100644 index 0000000000..3c2955919a --- /dev/null +++ b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/PlainConfiguration.java @@ -0,0 +1,303 @@ +package org.apache.qpid.server.security.access.config; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.StreamTokenizer; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import org.apache.commons.configuration.ConfigurationException; +import org.apache.commons.lang.StringUtils; +import org.apache.qpid.server.security.access.ObjectProperties; +import org.apache.qpid.server.security.access.ObjectType; +import org.apache.qpid.server.security.access.Operation; +import org.apache.qpid.server.security.access.Permission; + +public class PlainConfiguration extends AbstractConfiguration +{ + public static final Character COMMENT = '#'; + public static final Character CONTINUATION = '\\'; + + public static final String GROUP = "group"; + public static final String ACL = "acl"; + public static final String CONFIG = "config"; + + public static final String UNRECOGNISED_INITIAL_MSG = "Unrecognised initial token '%s' at line %d"; + public static final String NOT_ENOUGH_TOKENS_MSG = "Not enough tokens at line %d"; + public static final String NUMBER_NOT_ALLOWED_MSG = "Number not allowed before '%s' at line %d"; + public static final String CANNOT_LOAD_MSG = "Cannot load config file %s"; + public static final String PREMATURE_CONTINUATION_MSG = "Premature continuation character at line %d"; + public static final String PREMATURE_EOF_MSG = "Premature end of file reached at line %d"; + public static final String PARSE_TOKEN_FAILED_MSG = "Failed to parse token at line %d"; + public static final String CONFIG_NOT_FOUND_MSG = "Cannot find config file %s"; + public static final String NOT_ENOUGH_GROUP_MSG = "Not enough data for a group at line %d"; + public static final String NOT_ENOUGH_ACL_MSG = "Not enough data for an acl at line %d"; + public static final String NOT_ENOUGH_CONFIG_MSG = "Not enough data for config at line %d"; + public static final String BAD_ACL_RULE_NUMBER_MSG = "Invalid rule number at line %d"; + public static final String PROPERTY_KEY_ONLY_MSG = "Incomplete property (key only) at line %d"; + public static final String PROPERTY_NO_EQUALS_MSG = "Incomplete property (no equals) at line %d"; + public static final String PROPERTY_NO_VALUE_MSG = "Incomplete property (no value) at line %d"; + + private StreamTokenizer _st; + + public PlainConfiguration(File file) + { + super(file); + } + + @Override + public RuleSet load() throws ConfigurationException + { + RuleSet ruleSet = super.load(); + + try + { + _st = new StreamTokenizer(new BufferedReader(new FileReader(_file))); + _st.resetSyntax(); // setup the tokenizer + + _st.commentChar(COMMENT); // single line comments + _st.eolIsSignificant(true); // return EOL as a token + _st.lowerCaseMode(true); // case insensitive tokens + _st.ordinaryChar('='); // equals is a token + _st.ordinaryChar(CONTINUATION); // continuation character (when followed by EOL) + _st.quoteChar('"'); // double quote + _st.quoteChar('\''); // single quote + _st.whitespaceChars('\u0000', '\u0020'); // whitespace (to be ignored) TODO properly + _st.wordChars('a', 'z'); // unquoted token characters [a-z] + _st.wordChars('A', 'Z'); // [A-Z] + _st.wordChars('0', '9'); // [0-9] + _st.wordChars('_', '_'); // underscore + _st.wordChars('-', '-'); // dash + _st.wordChars('.', '.'); // dot + _st.wordChars('*', '*'); // star + _st.wordChars('@', '@'); // at + _st.wordChars(':', ':'); // colon + + // parse the acl file lines + Stack<String> stack = new Stack<String>(); + int current; + do { + current = _st.nextToken(); + switch (current) + { + case StreamTokenizer.TT_EOF: + case StreamTokenizer.TT_EOL: + if (stack.isEmpty()) + { + break; // blank line + } + + // pull out the first token from the bottom of the stack and check arguments exist + String first = stack.firstElement(); + stack.removeElementAt(0); + if (stack.isEmpty()) + { + throw new ConfigurationException(String.format(NOT_ENOUGH_TOKENS_MSG, getLine())); + } + + // check for and parse optional initial number for ACL lines + Integer number = null; + if (StringUtils.isNumeric(first)) + { + // set the acl number and get the next element + number = Integer.valueOf(first); + first = stack.firstElement(); + stack.removeElementAt(0); + } + + if (StringUtils.equalsIgnoreCase(ACL, first)) + { + parseAcl(number, stack); + } + else if (number == null) + { + if (StringUtils.equalsIgnoreCase(GROUP, first)) + { + parseGroup(stack); + } + else if (StringUtils.equalsIgnoreCase(CONFIG, first)) + { + parseConfig(stack); + } + else + { + throw new ConfigurationException(String.format(UNRECOGNISED_INITIAL_MSG, first, getLine())); + } + } + else + { + throw new ConfigurationException(String.format(NUMBER_NOT_ALLOWED_MSG, first, getLine())); + } + + // reset stack, start next line + stack.clear(); + break; + case StreamTokenizer.TT_NUMBER: + stack.push(Integer.toString(Double.valueOf(_st.nval).intValue())); + break; + case StreamTokenizer.TT_WORD: + stack.push(_st.sval); // token + break; + default: + if (_st.ttype == CONTINUATION) + { + int next = _st.nextToken(); + if (next == StreamTokenizer.TT_EOL) + { + break; // continue reading next line + } + + // invalid location for continuation character (add one to line beacuse we ate the EOL) + throw new ConfigurationException(String.format(PREMATURE_CONTINUATION_MSG, getLine() + 1)); + } + else if (_st.ttype == '\'' || _st.ttype == '"') + { + stack.push(_st.sval); // quoted token + } + else + { + stack.push(Character.toString((char) _st.ttype)); // single character + } + } + } while (current != StreamTokenizer.TT_EOF); + + if (!stack.isEmpty()) + { + throw new ConfigurationException(String.format(PREMATURE_EOF_MSG, getLine())); + } + } + catch (IllegalArgumentException iae) + { + throw new ConfigurationException(String.format(PARSE_TOKEN_FAILED_MSG, getLine()), iae); + } + catch (FileNotFoundException fnfe) + { + throw new ConfigurationException(String.format(CONFIG_NOT_FOUND_MSG, getFile().getName()), fnfe); + } + catch (IOException ioe) + { + throw new ConfigurationException(String.format(CANNOT_LOAD_MSG, getFile().getName()), ioe); + } + + return ruleSet; + } + + private void parseGroup(List<String> args) throws ConfigurationException + { + if (args.size() < 2) + { + throw new ConfigurationException(String.format(NOT_ENOUGH_GROUP_MSG, getLine())); + } + + getConfiguration().addGroup(args.get(0), args.subList(1, args.size())); + } + + private void parseAcl(Integer number, List<String> args) throws ConfigurationException + { + if (args.size() < 3) + { + throw new ConfigurationException(String.format(NOT_ENOUGH_ACL_MSG, getLine())); + } + + Permission permission = Permission.parse(args.get(0)); + String identity = args.get(1); + Operation operation = Operation.parse(args.get(2)); + + if (number != null && !getConfiguration().isValidNumber(number)) + { + throw new ConfigurationException(String.format(BAD_ACL_RULE_NUMBER_MSG, getLine())); + } + + if (args.size() == 3) + { + getConfiguration().grant(number, identity, permission, operation); + } + else + { + ObjectType object = ObjectType.parse(args.get(3)); + ObjectProperties properties = toObjectProperties(args.subList(4, args.size())); + + getConfiguration().grant(number, identity, permission, operation, object, properties); + } + } + + private void parseConfig(List<String> args) throws ConfigurationException + { + if (args.size() < 3) + { + throw new ConfigurationException(String.format(NOT_ENOUGH_CONFIG_MSG, getLine())); + } + + Map<String, Boolean> properties = toPluginProperties(args); + + getConfiguration().configure(properties); + } + + /** Converts a {@link List} of "name", "=", "value" tokens into a {@link Map}. */ + protected ObjectProperties toObjectProperties(List<String> args) throws ConfigurationException + { + ObjectProperties properties = new ObjectProperties(); + Iterator<String> i = args.iterator(); + while (i.hasNext()) + { + String key = i.next(); + if (!i.hasNext()) + { + throw new ConfigurationException(String.format(PROPERTY_KEY_ONLY_MSG, getLine())); + } + if (!"=".equals(i.next())) + { + throw new ConfigurationException(String.format(PROPERTY_NO_EQUALS_MSG, getLine())); + } + if (!i.hasNext()) + { + throw new ConfigurationException(String.format(PROPERTY_NO_VALUE_MSG, getLine())); + } + String value = i.next(); + + // parse property key + ObjectProperties.Property property = ObjectProperties.Property.parse(key); + properties.put(property, value); + } + return properties; + } + + /** Converts a {@link List} of "name", "=", "value" tokens into a {@link Map}. */ + protected Map<String, Boolean> toPluginProperties(List<String> args) throws ConfigurationException + { + Map<String, Boolean> properties = new HashMap<String, Boolean>(); + Iterator<String> i = args.iterator(); + while (i.hasNext()) + { + String key = i.next().toLowerCase(); + if (!i.hasNext()) + { + throw new ConfigurationException(String.format(PROPERTY_KEY_ONLY_MSG, getLine())); + } + if (!"=".equals(i.next())) + { + throw new ConfigurationException(String.format(PROPERTY_NO_EQUALS_MSG, getLine())); + } + if (!i.hasNext()) + { + throw new ConfigurationException(String.format(PROPERTY_NO_VALUE_MSG, getLine())); + } + + // parse property value and save + Boolean value = Boolean.valueOf(i.next()); + properties.put(key, value); + } + return properties; + } + + protected int getLine() + { + return _st.lineno() - 1; + } +} diff --git a/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/Rule.java b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/Rule.java new file mode 100644 index 0000000000..15d6b67192 --- /dev/null +++ b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/Rule.java @@ -0,0 +1,170 @@ +/* + * 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.qpid.server.security.access.config; + +import org.apache.commons.lang.builder.CompareToBuilder; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; +import org.apache.qpid.server.security.access.Permission; + +/** + * An access control v2 rule. + * + * A rule consists of {@link Permission} for a particular identity to perform an {@link Action}. The identity + * may be either a user or a group. + */ +public class Rule implements Comparable<Rule> +{ + /** String indicating all identitied. */ + public static final String ALL = "all"; + + private Integer _number; + private Boolean _enabled = Boolean.TRUE; + private String _identity; + private Action _action; + private Permission _permission; + + public Rule(Integer number, String identity, Action action, Permission permission) + { + setNumber(number); + setIdentity(identity); + setAction(action); + setPermission(permission); + } + + public Rule(String identity, Action action, Permission permission) + { + this(null, identity, action, permission); + } + + public boolean isEnabled() + { + return _enabled; + } + + public void setEnabled(boolean enabled) + { + _enabled = enabled; + } + + public void enable() + { + _enabled = Boolean.TRUE; + } + + public void disable() + { + _enabled = Boolean.FALSE; + } + + public Integer getNumber() + { + return _number; + } + + public void setNumber(Integer number) + { + _number = number; + } + + public String getIdentity() + { + return _identity; + } + + public void setIdentity(String identity) + { + _identity = identity; + } + + public Action getAction() + { + return _action; + } + + public void setAction(Action action) + { + _action = action; + } + + public Permission getPermission() + { + return _permission; + } + + public void setPermission(Permission permission) + { + _permission = permission; + } + + /** @see Comparable#compareTo(Object) */ + public int compareTo(Rule r) + { + return new CompareToBuilder() + .append(getAction(), r.getAction()) + .append(getIdentity(), r.getIdentity()) + .append(getPermission(), r.getPermission()) + .toComparison(); + } + + /** @see Object#equals(Object) */ + @Override + public boolean equals(Object o) + { + if (!(o instanceof Rule)) + { + return false; + } + Rule r = (Rule) o; + + return new EqualsBuilder() + .append(getIdentity(), r.getIdentity()) + .append(getAction(), r.getAction()) + .append(getPermission(), r.getPermission()) + .isEquals(); + } + + /** @see Object#hashCode() */ + @Override + public int hashCode() + { + return new HashCodeBuilder() + .append(getIdentity()) + .append(getAction()) + .append(getPermission()) + .toHashCode(); + } + + /** @see Object#toString() */ + @Override + public String toString() + { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("#", getNumber()) + .append("identity", getIdentity()) + .append("action", getAction()) + .append("permission", getPermission()) + .append("enabled", isEnabled()) + .toString(); + } +} diff --git a/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/RuleSet.java b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/RuleSet.java new file mode 100644 index 0000000000..3c471f2f55 --- /dev/null +++ b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/RuleSet.java @@ -0,0 +1,471 @@ +/* + * 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.qpid.server.security.access.config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.WeakHashMap; + +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.apache.qpid.exchange.ExchangeDefaults; +import org.apache.qpid.server.security.Result; +import org.apache.qpid.server.security.access.ObjectProperties; +import org.apache.qpid.server.security.access.ObjectType; +import org.apache.qpid.server.security.access.Operation; +import org.apache.qpid.server.security.access.Permission; + +/** + * Models the rule configuration for the access control plugin. + * + * The access control rule definitions are loaded from an external configuration file, passed in as the + * target to the {@link load(ConfigurationFile)} method. The file specified + */ +public class RuleSet +{ + private static final Logger _logger = Logger.getLogger(RuleSet.class); + + private static final String AT = "@"; + private static final String SLASH = "/"; + + public static final String DEFAULT_ALLOW = "defaultallow"; + public static final String DEFAULT_DENY = "defaultdeny"; + public static final String TRANSITIVE = "transitive"; + public static final String EXPAND = "expand"; + public static final String AUTONUMBER = "autonumber"; + public static final String CONTROLLED = "controlled"; + public static final String VALIDATE = "validate"; + + public static final List<String> CONFIG_PROPERTIES = Arrays.asList( + DEFAULT_ALLOW, DEFAULT_DENY, TRANSITIVE, EXPAND, AUTONUMBER, CONTROLLED + ); + + private static final Integer _increment = 10; + + private final Map<String, List<String>> _groups = new HashMap<String, List<String>>(); + private final SortedMap<Integer, Rule> _rules = new TreeMap<Integer, Rule>(); + private final Map<String, Map<Operation, Map<ObjectType, List<Rule>>>> _cache = + new WeakHashMap<String, Map<Operation, Map<ObjectType, List<Rule>>>>(); + private final Map<String, Boolean> _config = new HashMap<String, Boolean>(); + + public RuleSet() + { + // set some default configuration properties + configure(DEFAULT_DENY, Boolean.TRUE); + configure(TRANSITIVE, Boolean.TRUE); + } + + /** + * Clear the contents, invluding groups, rules and configuration. + */ + public void clear() + { + _rules.clear(); + _cache.clear(); + _config.clear(); + _groups.clear(); + } + + public int getRuleCount() + { + return _rules.size(); + } + + /** + * Filtered rules list based on an identity and operation. + * + * Allows only enabled rules with identity equal to all, the same, or a group with identity as a member, + * and operation is either all or the same operation. + */ + public List<Rule> getRules(String identity, Operation operation, ObjectType objectType) + { + // Lookup identity in cache and create empty operation map if required + Map<Operation, Map<ObjectType, List<Rule>>> operations = _cache.get(identity); + if (operations == null) + { + operations = new EnumMap<Operation, Map<ObjectType, List<Rule>>>(Operation.class); + _cache.put(identity, operations); + } + + // Lookup operation and create empty object type map if required + Map<ObjectType, List<Rule>> objects = operations.get(operation); + if (objects == null) + { + objects = new EnumMap<ObjectType, List<Rule>>(ObjectType.class); + operations.put(operation, objects); + } + + // Lookup object type rules for the operation + if (!objects.containsKey(objectType)) + { + boolean controlled = false; + List<Rule> filtered = new LinkedList<Rule>(); + for (Rule rule : _rules.values()) + { + if (rule.isEnabled() + && (rule.getAction().getOperation() == Operation.ALL || rule.getAction().getOperation() == operation) + && (rule.getAction().getObjectType() == ObjectType.ALL || rule.getAction().getObjectType() == objectType)) + { + controlled = true; + + if (rule.getIdentity().equalsIgnoreCase(Rule.ALL) + || rule.getIdentity().equalsIgnoreCase(identity) + || (_groups.containsKey(rule.getIdentity()) && _groups.get(rule.getIdentity()).contains(identity))) + { + filtered.add(rule); + } + } + } + + // Return null if there are no rules at all for this operation and object type + if (filtered.isEmpty() && controlled == false) + { + filtered = null; + } + + // Save the rules we selected + objects.put(objectType, filtered); + } + + // Return the cached rules + return objects.get(objectType); + } + + public boolean isValidNumber(Integer number) + { + return !_rules.containsKey(number); + } + + public void grant(Integer number, String identity, Permission permission, Operation operation) + { + Action action = new Action(operation); + addRule(number, identity, permission, action); + } + + public void grant(Integer number, String identity, Permission permission, Operation operation, ObjectType object, ObjectProperties properties) + { + Action action = new Action(operation, object, properties); + addRule(number, identity, permission, action); + } + + public boolean ruleExists(String identity, Action action) + { + for (Rule rule : _rules.values()) + { + if (rule.getIdentity().equals(identity) && rule.getAction().equals(action)) + { + return true; + } + } + return false; + } + + // TODO make this work when group membership is not known at file parse time + public void addRule(Integer number, String identity, Permission permission, Action action) + { + if (!action.isAllowed()) + { + throw new IllegalArgumentException("Action is not allowd: " + action); + } + if (ruleExists(identity, action)) + { + return; + } + + // expand actions - possibly multiply number by + if (isSet(EXPAND)) + { + if (action.getOperation() == Operation.CREATE && action.getObjectType() == ObjectType.TOPIC) + { + addRule(null, identity, permission, new Action(Operation.BIND, ObjectType.EXCHANGE, + new ObjectProperties("amq.topic", action.getProperties().get(ObjectProperties.Property.NAME)))); + ObjectProperties topicProperties = new ObjectProperties(); + topicProperties.put(ObjectProperties.Property.DURABLE, true); + addRule(null, identity, permission, new Action(Operation.CREATE, ObjectType.QUEUE, topicProperties)); + return; + } + if (action.getOperation() == Operation.DELETE && action.getObjectType() == ObjectType.TOPIC) + { + addRule(null, identity, permission, new Action(Operation.UNBIND, ObjectType.EXCHANGE, + new ObjectProperties("amq.topic", action.getProperties().get(ObjectProperties.Property.NAME)))); + ObjectProperties topicProperties = new ObjectProperties(); + topicProperties.put(ObjectProperties.Property.DURABLE, true); + addRule(null, identity, permission, new Action(Operation.DELETE, ObjectType.QUEUE, topicProperties)); + return; + } + } + + // transitive action dependencies + if (isSet(TRANSITIVE)) + { + if (action.getOperation() == Operation.CREATE && action.getObjectType() == ObjectType.QUEUE) + { + ObjectProperties exchProperties = new ObjectProperties(action.getProperties()); + exchProperties.setName(ExchangeDefaults.DEFAULT_EXCHANGE_NAME); + exchProperties.put(ObjectProperties.Property.ROUTING_KEY, action.getProperties().get(ObjectProperties.Property.NAME)); + addRule(null, identity, permission, new Action(Operation.BIND, ObjectType.EXCHANGE, exchProperties)); + if (action.getProperties().isSet(ObjectProperties.Property.AUTO_DELETE)) + { + addRule(null, identity, permission, new Action(Operation.DELETE, ObjectType.QUEUE, action.getProperties())); + } + } + else if (action.getOperation() == Operation.DELETE && action.getObjectType() == ObjectType.QUEUE) + { + ObjectProperties exchProperties = new ObjectProperties(action.getProperties()); + exchProperties.setName(ExchangeDefaults.DEFAULT_EXCHANGE_NAME); + exchProperties.put(ObjectProperties.Property.ROUTING_KEY, action.getProperties().get(ObjectProperties.Property.NAME)); + addRule(null, identity, permission, new Action(Operation.UNBIND, ObjectType.EXCHANGE, exchProperties)); + } + else if (action.getOperation() != Operation.ACCESS && action.getObjectType() != ObjectType.VIRTUALHOST) + { + addRule(null, identity, permission, new Action(Operation.ACCESS, ObjectType.VIRTUALHOST)); + } + } + + // set rule number if needed + Rule rule = new Rule(number, identity, action, permission); + if (rule.getNumber() == null) + { + if (_rules.isEmpty()) + { + rule.setNumber(0); + } + else + { + rule.setNumber(_rules.lastKey() + _increment); + } + } + + // save rule + _cache.remove(identity); + _rules.put(rule.getNumber(), rule); + } + + public void enableRule(int ruleNumber) + { + _rules.get(Integer.valueOf(ruleNumber)).enable(); + } + + public void disableRule(int ruleNumber) + { + _rules.get(Integer.valueOf(ruleNumber)).disable(); + } + + public boolean addGroup(String group, List<String> constituents) + { + if (_groups.containsKey(group)) + { + // cannot redefine + return false; + } + else + { + _groups.put(group, new ArrayList<String>()); + } + + for (String name : constituents) + { + if (name.equalsIgnoreCase(group)) + { + // recursive definition + return false; + } + + if (!checkName(name)) + { + // invalid name + return false; + } + + if (_groups.containsKey(name)) + { + // is a group + _groups.get(group).addAll(_groups.get(name)); + } + else + { + // is a user + if (!isvalidUserName(name)) + { + // invalid username + return false; + } + _groups.get(group).add(name); + } + } + return true; + } + + /** Return true if the name is well-formed (contains legal characters). */ + protected boolean checkName(String name) + { + for (int i = 0; i < name.length(); i++) + { + Character c = name.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '-' && c != '_' && c != '@' && c != '.' && c != '/') + { + return false; + } + } + return true; + } + + /** Returns true if a username has the name[@domain][/realm] format */ + protected boolean isvalidUserName(String name) + { + // check for '@' and '/' in namne + int atPos = name.indexOf(AT); + int slashPos = name.indexOf(SLASH); + boolean atFound = atPos != StringUtils.INDEX_NOT_FOUND && atPos == name.lastIndexOf(AT); + boolean slashFound = slashPos != StringUtils.INDEX_NOT_FOUND && slashPos == name.lastIndexOf(SLASH); + + // must be at least one character after '@' or '/' + if (atFound && atPos > name.length() - 2) + { + return false; + } + if (slashFound && slashPos > name.length() - 2) + { + return false; + } + + // must be at least one character between '@' and '/' + if (atFound && slashFound) + { + return (atPos < (slashPos - 1)); + } + + // otherwise all good + return true; + } + + // C++ broker authorise function prototype + // virtual bool authorise(const std::string& id, const Action& action, const ObjectType& objType, + // const std::string& name, std::map<Property, std::string>* params=0); + + // Possibly add a String name paramater? + + /** + * Check the authorisation granted to a particular identity for an operation on an object type with + * specific properties. + * + * Looks up the entire ruleset, whcih may be cached, for the user and operation and goes through the rules + * in order to find the first one that matches. Either defers if there are no rules, returns the result of + * the first match found, or denies access if there are no matching rules. Normally, it would be expected + * to have a default deny or allow rule at the end of an access configuration however. + */ + public Result check(String identity, Operation operation, ObjectType objectType, ObjectProperties properties) + { + // Create the action to check + Action action = new Action(operation, objectType, properties); + + // get the list of rules relevant for this request + List<Rule> rules = getRules(identity, operation, objectType); + if (rules == null) + { + if (isSet(CONTROLLED)) + { + // Abstain if there are no rules for this operation + return Result.ABSTAIN; + } + else + { + return getDefault(); + } + } + + // Iterate through a filtered set of rules dealing with this identity and operation + for (Rule current : rules) + { + // Check if action matches + if (action.matches(current.getAction())) + { + Permission permission = current.getPermission(); + + switch (permission) + { + case ALLOW_LOG: + _logger.info("ALLOWED " + action); + case ALLOW: + return Result.ALLOWED; + case DENY_LOG: + _logger.info("DENIED " + action); + case DENY: + return Result.DENIED; + } + + return Result.DENIED; + } + } + + // Defer to the next plugin of this type, if it exists + return Result.DEFER; + } + + /** Default deny. */ + public Result getDefault() + { + if (isSet(DEFAULT_ALLOW)) + { + return Result.ALLOWED; + } + if (isSet(DEFAULT_DENY)) + { + return Result.DENIED; + } + return Result.ABSTAIN; + } + + /** + * Check if a configuration property is set. + */ + protected boolean isSet(String key) + { + return BooleanUtils.isTrue(_config.get(key)); + } + + /** + * Configure properties for the plugin instance. + * + * @param properties + */ + public void configure(Map<String, Boolean> properties) + { + _config.putAll(properties); + } + + /** + * Configure a single property for the plugin instance. + * + * @param key + * @param value + */ + public void configure(String key, Boolean value) + { + _config.put(key, value); + } +} diff --git a/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/XMLConfiguration.java b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/XMLConfiguration.java new file mode 100644 index 0000000000..c3e112fed9 --- /dev/null +++ b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/XMLConfiguration.java @@ -0,0 +1,11 @@ +package org.apache.qpid.server.security.access.config; + +import java.io.File; + +public class XMLConfiguration extends AbstractConfiguration +{ + public XMLConfiguration(File file) + { + super(file); + } +} diff --git a/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AccessControl.java b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AccessControl.java new file mode 100644 index 0000000000..75846b26ef --- /dev/null +++ b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AccessControl.java @@ -0,0 +1,127 @@ +/* + * + * 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.qpid.server.security.access.plugins; + +import java.io.File; +import java.security.Principal; + +import org.apache.commons.configuration.ConfigurationException; +import org.apache.log4j.Logger; +import org.apache.qpid.server.configuration.plugins.ConfigurationPlugin; +import org.apache.qpid.server.security.AbstractPlugin; +import org.apache.qpid.server.security.Result; +import org.apache.qpid.server.security.SecurityManager; +import org.apache.qpid.server.security.SecurityPluginFactory; +import org.apache.qpid.server.security.access.ObjectProperties; +import org.apache.qpid.server.security.access.ObjectType; +import org.apache.qpid.server.security.access.Operation; +import org.apache.qpid.server.security.access.config.ConfigurationFile; +import org.apache.qpid.server.security.access.config.PlainConfiguration; +import org.apache.qpid.server.security.access.config.RuleSet; + +/** + * This access control plugin implements version two plain text access control. + */ +public class AccessControl extends AbstractPlugin +{ + public static final Logger _logger = Logger.getLogger(AccessControl.class); + + private RuleSet _ruleSet; + + public static final SecurityPluginFactory<AccessControl> FACTORY = new SecurityPluginFactory<AccessControl>() + { + public Class<AccessControl> getPluginClass() + { + return AccessControl.class; + } + + public String getPluginName() + { + return AccessControl.class.getName(); + } + + public AccessControl newInstance(ConfigurationPlugin config) throws ConfigurationException + { + AccessControl plugin = new AccessControl(config); + plugin.configure(); + return plugin; + } + }; + + public AccessControl(ConfigurationPlugin config) + { + _config = config.getConfiguration(AccessControlConfiguration.class); + } + + public Result getDefault() + { + return _ruleSet.getDefault(); + } + + /** Parse a version two access control file. */ + private void parseFile(File aclFile) throws ConfigurationException + { + ConfigurationFile configFile = new PlainConfiguration(aclFile); + _ruleSet = configFile.load(); + } + + /** + * Object instance access authorisation. + * + * Delegate to the {@link #authorise(Operation, ObjectType, ObjectProperties)} method, with + * the operation set to ACCESS and no object properties. + */ + public Result access(ObjectType objectType, Object instance) + { + return authorise(Operation.ACCESS, objectType, ObjectProperties.EMPTY); + } + + /** + * Check if an operation is authorised by asking the configuration object about the access + * control rules granted to the current thread's {@link Principal}. If there is no current + * user the plugin will abstain. + */ + public Result authorise(Operation operation, ObjectType objectType, ObjectProperties properties) + { + Principal principal = SecurityManager.getThreadPrincipal(); + + // Abstain if there is no user associated with this thread + if (principal == null) + { + return Result.ABSTAIN; + } + + return _ruleSet.check(principal.getName(), operation, objectType, properties); + } + + @Override + public void configure() throws ConfigurationException + { + AccessControlConfiguration accessConfig = (AccessControlConfiguration) _config; + + if (isConfigured()) + { + String fileName = accessConfig.getFileName(); + File aclFile = new File(fileName); + parseFile(aclFile); + } + } +} diff --git a/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AccessControlActivator.java b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AccessControlActivator.java new file mode 100644 index 0000000000..930e2a6c20 --- /dev/null +++ b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AccessControlActivator.java @@ -0,0 +1,22 @@ +package org.apache.qpid.server.security.access.plugins; + +import org.apache.qpid.server.configuration.plugins.ConfigurationPluginFactory; +import org.apache.qpid.server.security.SecurityPluginActivator; +import org.apache.qpid.server.security.SecurityPluginFactory; +import org.osgi.framework.BundleActivator; + +/** + * The OSGi {@link BundleActivator} for {@link AccessControl}. + */ +public class AccessControlActivator extends SecurityPluginActivator +{ + public SecurityPluginFactory getFactory() + { + return AccessControl.FACTORY; + } + + public ConfigurationPluginFactory getConfigurationFactory() + { + return AccessControlConfiguration.FACTORY; + } +} diff --git a/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AccessControlConfiguration.java b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AccessControlConfiguration.java new file mode 100644 index 0000000000..2d6ac99a98 --- /dev/null +++ b/java/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AccessControlConfiguration.java @@ -0,0 +1,57 @@ +/* + * + * 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.qpid.server.security.access.plugins; + +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.configuration.Configuration; +import org.apache.commons.configuration.ConfigurationException; +import org.apache.qpid.server.configuration.plugins.ConfigurationPlugin; +import org.apache.qpid.server.configuration.plugins.ConfigurationPluginFactory; + +public class AccessControlConfiguration extends ConfigurationPlugin +{ + public static final ConfigurationPluginFactory FACTORY = new ConfigurationPluginFactory() + { + public ConfigurationPlugin newInstance(String path, Configuration config) throws ConfigurationException + { + ConfigurationPlugin instance = new AccessControlConfiguration(); + instance.setConfiguration(path, config); + return instance; + } + + public List<String> getParentPaths() + { + return Arrays.asList("security", "virtualhosts.virtualhost.security"); + } + }; + + public String[] getElementsProcessed() + { + return new String[] { "aclv2" }; + } + + public String getFileName() + { + return _configuration.getString("aclv2"); + } +} diff --git a/java/broker-plugins/access-control/src/main/resources/acl.xsd b/java/broker-plugins/access-control/src/main/resources/acl.xsd new file mode 100644 index 0000000000..39b7bb33fc --- /dev/null +++ b/java/broker-plugins/access-control/src/main/resources/acl.xsd @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xs:schema + xmlns="http://qpid.apache.org/schema/qpid/broker/security/acl.xsd" + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + targetNamespace="http://qpid.apache.org/schema/qpid/broker/security/acl.xsd" + elementFormDefault="qualified"> + <xs:element name="aclv2" type="xs:string" /> +</xs:schema>
\ No newline at end of file |