This is an automated email from the ASF dual-hosted git repository.

vavrtom pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git


The following commit(s) were added to refs/heads/main by this push:
     new 0b91ded  QPID-8488: [Broker-J] Enhance ACL rule with multi-value 
properties
0b91ded is described below

commit 0b91ded6e2c4587ff4ce2324636e2e5f22645295
Author: Marek Laca <mkl...@users.noreply.github.com>
AuthorDate: Wed Feb 23 07:58:41 2022 +0100

    QPID-8488: [Broker-J] Enhance ACL rule with multi-value properties
    
    This closes #116
---
 .../security/access/config/AclFileParser.java      |  46 +-
 .../security/access/config/AclRulePredicates.java  |  34 +-
 .../access/config/AclRulePredicatesBuilder.java    | 112 ++-
 .../qpid/server/security/access/config/Rule.java   |  11 +-
 .../access/config/predicates/MultiValue.java       |  54 ++
 .../config/predicates/RulePredicateBuilder.java    |   9 +-
 ...stractCommonRuleBasedAccessControlProvider.java |  26 +-
 .../server/security/access/plugins/AclRule.java    |   6 +-
 .../security/access/util/AbstractTreeBranch.java   |  98 +++
 .../qpid/server/security/access/util/Any.java      |  59 ++
 .../qpid/server/security/access/util/Empty.java    |  57 ++
 .../server/security/access/util/FinalBranch.java   |  70 ++
 .../server/security/access/util/PrefixTree.java    | 111 +++
 .../server/security/access/util/PrefixTreeSet.java |  33 +
 .../server/security/access/util/TreeBranch.java    | 236 ++++++
 .../qpid/server/security/access/util/TreeRoot.java |  76 ++
 .../security/access/util/WildCardBranch.java       | 102 +++
 .../server/security/access/util/WildCardSet.java   |  48 ++
 .../access/config/AclRulePredicatesTest.java       |  43 +-
 .../predicates/RulePredicateBuilderTest.java       |  67 ++
 .../config/predicates/RulePredicateTest.java       |  57 ++
 .../security/access/util/PrefixTreeTest.java       | 942 +++++++++++++++++++++
 .../management/accesscontrolprovider/RuleBased.js  |  18 +-
 ...Java-Broker-Security-AccessControlProviders.xml |  17 +-
 24 files changed, 2251 insertions(+), 81 deletions(-)

diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclFileParser.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclFileParser.java
index a964af7..bb1c54c 100644
--- 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclFileParser.java
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclFileParser.java
@@ -32,26 +32,30 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayDeque;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Queue;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import org.apache.qpid.server.configuration.IllegalConfigurationException;
 import org.apache.qpid.server.logging.EventLoggerProvider;
 import org.apache.qpid.server.security.Result;
 import org.apache.qpid.server.security.access.config.Rule.Builder;
 import org.apache.qpid.server.security.access.plugins.RuleOutcome;
 
+import com.google.common.collect.Iterables;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 public final class AclFileParser
 {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(AclFileParser.class);
@@ -81,6 +85,8 @@ public final class AclFileParser
     static final String PROPERTY_NO_VALUE_MSG = "Incomplete property (no 
value) at line %d";
     static final String GROUP_NOT_SUPPORTED = "GROUP keyword not supported at 
line %d." +
             " Groups should defined via a Group Provider, not in the ACL 
file.";
+    static final String PROPERTY_NO_CLOSE_BRACKET_MSG = "Incomplete property 
(no close bracket) at line %d";
+
     private static final String INVALID_ENUM = "Not a valid %s: %s";
     private static final String INVALID_URL = "Cannot convert %s to a readable 
resource";
 
@@ -322,7 +328,8 @@ public final class AclFileParser
         while (i.hasNext())
         {
             final String key = i.next().toLowerCase(Locale.ENGLISH);
-            final Boolean value = Boolean.valueOf(readValue(i, line));
+            final Set<String> values = readValue(i, line);
+            final Boolean value = 
Boolean.valueOf(Iterables.getOnlyElement(values));
 
             if (Boolean.TRUE.equals(value))
             {
@@ -343,7 +350,7 @@ public final class AclFileParser
         }
     }
 
-    private static String readValue(Iterator<String> tokenIterator, int line)
+    private static Set<String> readValue(Iterator<String> tokenIterator, int 
line)
     {
         if (!tokenIterator.hasNext())
         {
@@ -357,7 +364,27 @@ public final class AclFileParser
         {
             throw new 
IllegalConfigurationException(String.format(PROPERTY_NO_VALUE_MSG, line));
         }
-        return tokenIterator.next();
+        final String value = tokenIterator.next();
+        if ("[".equals(value))
+        {
+            final Set<String> values = new HashSet<>();
+            String next;
+            while (tokenIterator.hasNext())
+            {
+                next = tokenIterator.next();
+                if (AclRulePredicates.SEPARATOR.equals(next))
+                {
+                    continue;
+                }
+                if ("]".equals(next))
+                {
+                    return values;
+                }
+                values.add(next);
+            }
+            throw new 
IllegalConfigurationException(String.format(PROPERTY_NO_CLOSE_BRACKET_MSG, 
line));
+        }
+        return Collections.singleton(value);
     }
 
     private RuleOutcome parsePermission(final String text, final int line)
@@ -381,11 +408,8 @@ public final class AclFileParser
                                             final String typeDescription)
     {
         return Optional.ofNullable(map.get(text.toUpperCase(Locale.ENGLISH)))
-                       .orElseThrow(() -> new 
IllegalConfigurationException(String.format(PARSE_TOKEN_FAILED_MSG, line),
-                                                                            
new IllegalArgumentException(String.format(
-                                                                               
     INVALID_ENUM,
-                                                                               
     typeDescription,
-                                                                               
     text))));
+                .orElseThrow(() -> new 
IllegalConfigurationException(String.format(PARSE_TOKEN_FAILED_MSG, line),
+                        new 
IllegalArgumentException(String.format(INVALID_ENUM, typeDescription, text))));
     }
 
     private Reader getReaderFromURLString(String urlString)
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicates.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicates.java
index de222fd..d1ad844 100644
--- 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicates.java
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicates.java
@@ -22,6 +22,7 @@ import java.util.AbstractMap;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumMap;
+import java.util.EnumSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -37,7 +38,10 @@ import com.google.common.collect.Iterables;
 public final class AclRulePredicates extends AbstractMap<Property, Set<Object>>
         implements RulePredicate
 {
-    static final String SEPARATOR = ",";
+    public static final String SEPARATOR = ",";
+
+    private static final Set<Property> JOIN_PROPERTIES =
+            EnumSet.of(Property.ATTRIBUTES, Property.FROM_HOSTNAME, 
Property.FROM_NETWORK);
 
     private final Map<Property, Set<Object>> _properties;
 
@@ -64,9 +68,9 @@ public final class AclRulePredicates extends 
AbstractMap<Property, Set<Object>>
         _rulePredicate = 
Objects.requireNonNull(builder.newRulePredicate(factory));
     }
 
-    public Map<Property, String> getParsedProperties()
+    public Map<Property, Object> getParsedProperties()
     {
-        final Map<Property, String> parsed = new EnumMap<>(Property.class);
+        final Map<Property, Object> parsed = new EnumMap<>(Property.class);
         for (final Map.Entry<Property, Set<Object>> entry : 
_properties.entrySet())
         {
             final Set<Object> values = entry.getValue();
@@ -76,16 +80,30 @@ public final class AclRulePredicates extends 
AbstractMap<Property, Set<Object>>
             }
             else
             {
-                parsed.put(entry.getKey(),
-                        values.stream()
-                                .map(Object::toString)
-                                .sorted()
-                                .collect(Collectors.joining(SEPARATOR)));
+                parsed.put(entry.getKey(), collect(entry.getKey(), values));
             }
         }
         return parsed;
     }
 
+    private Object collect(Property property, Set<Object> values)
+    {
+        if (JOIN_PROPERTIES.contains(property))
+        {
+            return values.stream()
+                    .map(Object::toString)
+                    .sorted()
+                    .collect(Collectors.joining(SEPARATOR));
+        }
+        else
+        {
+            return values.stream()
+                    .map(Object::toString)
+                    .sorted()
+                    .collect(Collectors.toList());
+        }
+    }
+
     @Override
     public boolean matches(LegacyOperation operation, ObjectProperties 
objectProperties, Subject subject)
     {
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicatesBuilder.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicatesBuilder.java
index bd9d57a..7a6054d 100644
--- 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicatesBuilder.java
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicatesBuilder.java
@@ -19,6 +19,7 @@
 package org.apache.qpid.server.security.access.config;
 
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumMap;
 import java.util.HashSet;
@@ -29,6 +30,8 @@ import java.util.Set;
 
 import 
org.apache.qpid.server.security.access.config.predicates.RulePredicateBuilder;
 import org.apache.qpid.server.security.access.firewall.FirewallRuleFactory;
+import org.apache.qpid.server.security.access.util.PrefixTreeSet;
+import org.apache.qpid.server.security.access.util.WildCardSet;
 
 import com.google.common.collect.ImmutableSet;
 import org.slf4j.Logger;
@@ -63,7 +66,7 @@ public final class AclRulePredicatesBuilder
         {
             for (final Map.Entry<Property, ?> entry : values.entrySet())
             {
-                addPropertyValue(entry.getKey(), 
Objects.toString(entry.getValue(), null));
+                addPropertyValues(entry.getKey(), entry.getValue());
             }
         }
     }
@@ -71,16 +74,32 @@ public final class AclRulePredicatesBuilder
     public AclRulePredicatesBuilder()
     {
         super();
-        for (final Property property : Property.values())
-        {
-            _parsedProperties.put(property, Collections.emptySet());
-        }
         _hostNames = new HashSet<>();
-        _parsedProperties.put(Property.FROM_HOSTNAME, _hostNames);
         _networks = new HashSet<>();
-        _parsedProperties.put(Property.FROM_NETWORK, _networks);
         _attributeNames = new HashSet<>();
-        _parsedProperties.put(Property.ATTRIBUTES, _attributeNames);
+        for (final Property property : Property.values())
+        {
+            if (property == Property.FROM_HOSTNAME)
+            {
+                _parsedProperties.put(property, _hostNames);
+            }
+            else if (property == Property.FROM_NETWORK)
+            {
+                _parsedProperties.put(property, _networks);
+            }
+            else if (property == Property.ATTRIBUTES)
+            {
+                _parsedProperties.put(property, _attributeNames);
+            }
+            else if (Property.isBooleanType(property))
+            {
+                _parsedProperties.put(property, new HashSet<>());
+            }
+            else
+            {
+                _parsedProperties.put(property, new PrefixTreeSet());
+            }
+        }
     }
 
     public AclRulePredicates build()
@@ -103,22 +122,50 @@ public final class AclRulePredicatesBuilder
         return this;
     }
 
+    public AclRulePredicatesBuilder parse(String key, Set<String> values)
+    {
+        final Property property = Property.parse(key);
+        for (final String value : values)
+        {
+            if (addPropertyValue(property, value))
+            {
+                LOGGER.debug("Parsed {} with value {}", property, value);
+            }
+        }
+        return this;
+    }
+
     public AclRulePredicatesBuilder put(Property property, String value)
     {
         addPropertyValue(property, value);
         return this;
     }
 
-    private boolean addPropertyValue(final Property property, final String 
value)
+    private void addPropertyValues(Property property, Object value)
+    {
+        if (value instanceof Collection)
+        {
+            for (final Object v : (Collection<?>) value)
+            {
+                addPropertyValues(property, v);
+            }
+        }
+        else
+        {
+            addPropertyValue(property, Objects.toString(value, null));
+        }
+    }
+
+    private boolean addPropertyValue(Property property, String value)
     {
         if (property == Property.FROM_HOSTNAME)
         {
-            checkFirewallRuleNotAlreadyDefined(property, value, 
Property.FROM_NETWORK);
+            checkFirewallRule(property, value, Property.FROM_NETWORK);
             _hostNames.addAll(splitToSet(value));
         }
         else if (property == Property.FROM_NETWORK)
         {
-            checkFirewallRuleNotAlreadyDefined(property, value, 
Property.FROM_HOSTNAME);
+            checkFirewallRule(property, value, Property.FROM_HOSTNAME);
             _networks.addAll(splitToSet(value));
         }
         else if (property == Property.ATTRIBUTES)
@@ -137,27 +184,40 @@ public final class AclRulePredicatesBuilder
         }
         else
         {
-            _parsedProperties.put(property, 
Collections.singleton(sanitiseValue(property, value)));
+            addPropertyValueImpl(property, value);
         }
         return true;
     }
 
-    private Object sanitiseValue(Property property, String value)
+    private void addPropertyValueImpl(Property property, String value)
     {
         if (value == null)
         {
-            return WILD_CARD;
+            _parsedProperties.put(property, WildCardSet.newSet());
+            return;
         }
         value = value.trim();
         if (value.isEmpty() || WILD_CARD.equals(value))
         {
-            return WILD_CARD;
+            _parsedProperties.put(property, WildCardSet.newSet());
+            return;
         }
+        final Set<?> values = _parsedProperties.get(property);
         if (Property.isBooleanType(property))
         {
-            return parseBoolean(value);
+            ((Set<Object>) values).add(parseBoolean(value));
+        }
+        else
+        {
+            if (values instanceof PrefixTreeSet)
+            {
+                ((PrefixTreeSet) values).add(value);
+            }
+            else
+            {
+                ((Set<Object>) values).add(value);
+            }
         }
-        return value;
     }
 
     private Boolean parseBoolean(String value)
@@ -178,14 +238,17 @@ public final class AclRulePredicatesBuilder
         return Boolean.parseBoolean(value);
     }
 
-    private HashSet<String> splitToSet(String value)
+    private Set<String> splitToSet(String value)
     {
+        if (value == null)
+        {
+            return Collections.emptySet();
+        }
         return new 
HashSet<>(Arrays.asList(value.split(AclRulePredicates.SEPARATOR)));
     }
 
-    private void checkFirewallRuleNotAlreadyDefined(Property property, String 
value, Property exclusiveProperty)
+    private void checkFirewallRule(Property property, String value, Property 
exclusiveProperty)
     {
-        checkPropertyAlreadyDefined(property);
         if (!_parsedProperties.get(exclusiveProperty).isEmpty())
         {
             throw new IllegalStateException(
@@ -195,15 +258,6 @@ public final class AclRulePredicatesBuilder
         }
     }
 
-    private void checkPropertyAlreadyDefined(Property property)
-    {
-        if (!_parsedProperties.get(property).isEmpty())
-        {
-            throw new IllegalStateException(String.format("Property '%s' has 
already been defined",
-                    property.toString().toLowerCase(Locale.ENGLISH)));
-        }
-    }
-
     Map<Property, Set<Object>> newProperties()
     {
         final Map<Property, Set<Object>> properties = new 
EnumMap<>(Property.class);
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/Rule.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/Rule.java
index 13c9874..d7d3b66 100644
--- 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/Rule.java
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/Rule.java
@@ -22,6 +22,7 @@ package org.apache.qpid.server.security.access.config;
 
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 
 import javax.security.auth.Subject;
 
@@ -138,7 +139,7 @@ public class Rule
         return _object;
     }
 
-    public Map<Property, String> getAttributes()
+    public Map<Property, Object> getAttributes()
     {
         return _predicates.getParsedProperties();
     }
@@ -213,7 +214,7 @@ public class Rule
         }
 
         @Override
-        public Map<Property, String> getAttributes()
+        public Map<Property, Object> getAttributes()
         {
             return _rule.getAttributes();
         }
@@ -276,6 +277,12 @@ public class Rule
             return this;
         }
 
+        public Builder withPredicate(String key, Set<String> values)
+        {
+            _aclRulePredicatesBuilder.parse(key, values);
+            return this;
+        }
+
         public Builder withOwner()
         {
             _identity = OWNER;
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/predicates/MultiValue.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/predicates/MultiValue.java
new file mode 100644
index 0000000..fa766ba
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/predicates/MultiValue.java
@@ -0,0 +1,54 @@
+/*
+ * 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.predicates;
+
+import java.util.Objects;
+
+import javax.security.auth.Subject;
+
+import org.apache.qpid.server.security.access.config.LegacyOperation;
+import org.apache.qpid.server.security.access.config.ObjectProperties;
+import org.apache.qpid.server.security.access.config.Property;
+import org.apache.qpid.server.security.access.config.RulePredicate;
+import org.apache.qpid.server.security.access.util.PrefixTree;
+
+class MultiValue implements RulePredicate
+{
+    private final Property _property;
+
+    private final PrefixTree _tree;
+
+    static RulePredicate newInstance(Property property, PrefixTree tree)
+    {
+        return tree == null ? RulePredicate.any() : new MultiValue(property, 
tree);
+    }
+
+    private MultiValue(Property property, PrefixTree tree)
+    {
+        _property = Objects.requireNonNull(property);
+        _tree = Objects.requireNonNull(tree);
+    }
+
+    @Override
+    public boolean matches(LegacyOperation operation, ObjectProperties 
objectProperties, Subject subject)
+    {
+        final Object value = objectProperties.get(_property);
+        return (value instanceof String) && _tree.match((String) value);
+    }
+}
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateBuilder.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateBuilder.java
index b7c5332..410066d 100644
--- 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateBuilder.java
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateBuilder.java
@@ -28,6 +28,7 @@ import 
org.apache.qpid.server.security.access.config.AclRulePredicatesBuilder;
 import org.apache.qpid.server.security.access.config.Property;
 import org.apache.qpid.server.security.access.config.RulePredicate;
 import org.apache.qpid.server.security.access.firewall.FirewallRuleFactory;
+import org.apache.qpid.server.security.access.util.PrefixTreeSet;
 
 public final class RulePredicateBuilder
 {
@@ -74,13 +75,17 @@ public final class RulePredicateBuilder
         }
     }
 
-    private Set<String> toSet(Collection<?> hostnames)
+    private Set<String> toSet(Collection<?> values)
     {
-        return 
hostnames.stream().map(Object::toString).collect(Collectors.toSet());
+        return 
values.stream().map(Object::toString).collect(Collectors.toSet());
     }
 
     private RulePredicate buildGenericPredicate(Property property, 
Collection<?> values)
     {
+        if (values instanceof PrefixTreeSet && values.size() > 2)
+        {
+            return MultiValue.newInstance(property, ((PrefixTreeSet) 
values).toPrefixTree());
+        }
         RulePredicate predicate = RulePredicate.none();
         for (final Object value : values)
         {
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AbstractCommonRuleBasedAccessControlProvider.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AbstractCommonRuleBasedAccessControlProvider.java
index f5a888b..a831907 100644
--- 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AbstractCommonRuleBasedAccessControlProvider.java
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AbstractCommonRuleBasedAccessControlProvider.java
@@ -29,10 +29,12 @@ import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import org.apache.qpid.server.logging.EventLoggerProvider;
 import org.apache.qpid.server.model.CommonAccessControlProvider;
@@ -139,13 +141,25 @@ abstract class 
AbstractCommonRuleBasedAccessControlProvider<X extends AbstractCo
                 .append(rule.getOperation().name())
                 .append(' ')
                 .append(rule.getObjectType().name());
-        for (final Map.Entry<Property, String> entry : 
rule.getAttributes().entrySet())
+        for (final Map.Entry<Property, Object> entry : 
rule.getAttributes().entrySet())
         {
-            sb.append(' ')
-                    .append(entry.getKey().getCanonicalName())
-                    .append("=\"")
-                    .append(entry.getValue())
-                    .append("\"");
+            if (entry.getValue() == null)
+            {
+                continue;
+            }
+            sb.append(' ').append(entry.getKey().getCanonicalName());
+
+            if (entry.getValue() instanceof Collection)
+            {
+                final Collection<?> values = (Collection<?>) entry.getValue();
+                sb.append("=[\"")
+                        
.append(values.stream().map(Object::toString).collect(Collectors.joining("\",\"")))
+                        .append("\"]");
+            }
+            else
+            {
+                sb.append("=\"").append(entry.getValue()).append("\"");
+            }
         }
     }
 
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AclRule.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AclRule.java
index bb1bf1e..7451c01 100644
--- 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AclRule.java
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/plugins/AclRule.java
@@ -32,8 +32,12 @@ import 
org.apache.qpid.server.security.access.config.Property;
 public interface AclRule extends ManagedAttributeValue
 {
     String getIdentity();
+
     ObjectType getObjectType();
+
     LegacyOperation getOperation();
-    Map<Property,String> getAttributes();
+
+    Map<Property, Object> getAttributes();
+
     RuleOutcome getOutcome();
 }
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/AbstractTreeBranch.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/AbstractTreeBranch.java
new file mode 100644
index 0000000..7b489d1
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/AbstractTreeBranch.java
@@ -0,0 +1,98 @@
+/*
+ *  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.util;
+
+import java.util.Objects;
+
+abstract class AbstractTreeBranch implements PrefixTree
+{
+    private static final String NO_PREFIX = "Tree root does not have a prefix";
+    private static final String EMPTY_INPUT_PREFIX = "The input prefix can not 
be empty";
+    private static final String EMPTY_INPUT_STRING = "The input string can not 
be empty";
+
+    final String _prefix;
+
+    final int _length;
+
+    abstract AbstractTreeBranch mergeString(String str);
+
+    abstract AbstractTreeBranch mergeWildCard(String prefix);
+
+    abstract boolean contains(String str);
+
+    AbstractTreeBranch(String prefix)
+    {
+        super();
+        _prefix = Objects.requireNonNull(prefix, "The prefix can not be 
null!");
+        _length = _prefix.length();
+    }
+
+    AbstractTreeBranch()
+    {
+        super();
+        _prefix = "";
+        _length = 0;
+    }
+
+    @Override
+    public String prefix()
+    {
+        return _prefix;
+    }
+
+    @Override
+    public char firstPrefixCharacter()
+    {
+        if (_prefix.isEmpty())
+        {
+            throw new UnsupportedOperationException(NO_PREFIX);
+        }
+        return _prefix.charAt(0);
+    }
+
+    @Override
+    public PrefixTree mergeWithFinalValue(String str)
+    {
+        if (str == null || str.isEmpty())
+        {
+            throw new IllegalArgumentException(EMPTY_INPUT_STRING);
+        }
+        return mergeString(str);
+    }
+
+    @Override
+    public PrefixTree mergeWithPrefix(String prefix)
+    {
+        if (prefix == null || prefix.isEmpty())
+        {
+            throw new IllegalArgumentException(EMPTY_INPUT_PREFIX);
+        }
+        return mergeWildCard(prefix);
+    }
+
+    @Override
+    public boolean match(String str)
+    {
+        if (str == null || str.length() == 0)
+        {
+            return false;
+        }
+        return contains(str);
+    }
+}
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/Any.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/Any.java
new file mode 100644
index 0000000..9b60ca2
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/Any.java
@@ -0,0 +1,59 @@
+package org.apache.qpid.server.security.access.util;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.google.common.collect.Iterators;
+
+final class Any extends AbstractTreeBranch
+{
+    static Any INSTANCE = new Any();
+
+    private Any()
+    {
+        super();
+    }
+
+    @Override
+    AbstractTreeBranch mergeString(String str)
+    {
+        return this;
+    }
+
+    @Override
+    AbstractTreeBranch mergeWildCard(String prefix)
+    {
+        return this;
+    }
+
+    @Override
+    public boolean match(String str)
+    {
+        return str != null;
+    }
+
+    @Override
+    boolean contains(String str)
+    {
+        return true;
+    }
+
+    @Override
+    public Map<Character, PrefixTree> branches()
+    {
+        return Collections.emptyMap();
+    }
+
+    @Override
+    public int size()
+    {
+        return 1;
+    }
+
+    @Override
+    public Iterator<String> iterator()
+    {
+        return Iterators.singletonIterator("*");
+    }
+}
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/Empty.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/Empty.java
new file mode 100644
index 0000000..cc0fc2c
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/Empty.java
@@ -0,0 +1,57 @@
+package org.apache.qpid.server.security.access.util;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+
+final class Empty extends AbstractTreeBranch
+{
+    static Empty INSTANCE = new Empty();
+
+    private Empty()
+    {
+        super();
+    }
+
+    @Override
+    public Map<Character, PrefixTree> branches()
+    {
+        return Collections.emptyMap();
+    }
+
+    @Override
+    public int size()
+    {
+        return 0;
+    }
+
+    @Override
+    public boolean match(String str)
+    {
+        return false;
+    }
+
+    @Override
+    AbstractTreeBranch mergeString(String str)
+    {
+        return new FinalBranch(str);
+    }
+
+    @Override
+    AbstractTreeBranch mergeWildCard(String prefix)
+    {
+        return new WildCardBranch(prefix);
+    }
+
+    @Override
+    boolean contains(String str)
+    {
+        return false;
+    }
+
+    @Override
+    public Iterator<String> iterator()
+    {
+        return Collections.emptyIterator();
+    }
+}
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/FinalBranch.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/FinalBranch.java
new file mode 100644
index 0000000..913d107
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/FinalBranch.java
@@ -0,0 +1,70 @@
+/*
+ *  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.util;
+
+import java.util.Iterator;
+import java.util.Map;
+
+final class FinalBranch extends TreeBranch
+{
+    FinalBranch(String prefix)
+    {
+        super(prefix);
+    }
+
+    FinalBranch(String prefix, Map<Character, AbstractTreeBranch> branches)
+    {
+        super(prefix, branches);
+    }
+
+    FinalBranch(String prefix, AbstractTreeBranch first)
+    {
+        super(prefix, first);
+    }
+
+    @Override
+    public int size()
+    {
+        return 1 + super.size();
+    }
+
+    @Override
+    boolean contains(String str)
+    {
+        return _prefix.equals(str) || super.contains(str);
+    }
+
+    @Override
+    public Iterator<String> iterator()
+    {
+        return new IteratorImpl(this, "");
+    }
+
+    @Override
+    FinalBranch newFinalBranch()
+    {
+        return this;
+    }
+
+    @Override
+    TreeBranch newTree(String substring, Map<Character, AbstractTreeBranch> 
branches)
+    {
+        return new FinalBranch(substring, branches);
+    }
+}
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/PrefixTree.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/PrefixTree.java
new file mode 100644
index 0000000..331467f
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/PrefixTree.java
@@ -0,0 +1,111 @@
+/*
+ *  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.util;
+
+import java.util.Collection;
+import java.util.Map;
+
+public interface PrefixTree extends Iterable<String>
+{
+    String prefix();
+
+    char firstPrefixCharacter();
+
+    Map<Character, PrefixTree> branches();
+
+    int size();
+
+    boolean match(String str);
+
+    default PrefixTree mergeWith(String str)
+    {
+        if (str == null || str.isEmpty())
+        {
+            throw new IllegalArgumentException("Prefix tree can not be merged 
with an empty value");
+        }
+        if ("*".equals(str))
+        {
+            return Any.INSTANCE;
+        }
+        if (str.endsWith("*"))
+        {
+            return mergeWithPrefix(str.substring(0, str.length() - 1));
+        }
+        return mergeWithFinalValue(str);
+    }
+
+    default PrefixTree mergeWith(Collection<String> collection)
+    {
+        PrefixTree tree = this;
+        for (final String str : collection)
+        {
+            tree = tree.mergeWith(str);
+        }
+        return tree;
+    }
+
+    PrefixTree mergeWithFinalValue(String str);
+
+    PrefixTree mergeWithPrefix(String prefix);
+
+    static PrefixTree empty()
+    {
+        return Empty.INSTANCE;
+    }
+
+    static PrefixTree from(String str)
+    {
+        if (str == null || str.isEmpty())
+        {
+            throw new IllegalArgumentException("A non null string is 
required");
+        }
+        if ("*".equals(str))
+        {
+            return Any.INSTANCE;
+        }
+        if (str.endsWith("*"))
+        {
+            return fromPrefixWithWildCard(str.substring(0, str.length() - 1));
+        }
+        return fromFinalValue(str);
+    }
+
+    static PrefixTree from(Collection<String> collection)
+    {
+        return empty().mergeWith(collection);
+    }
+
+    static PrefixTree fromFinalValue(String value)
+    {
+        if (value == null || value.isEmpty())
+        {
+            throw new IllegalArgumentException("A non empty value is 
required");
+        }
+        return new FinalBranch(value);
+    }
+
+    static PrefixTree fromPrefixWithWildCard(String prefix)
+    {
+        if (prefix == null || prefix.isEmpty())
+        {
+            throw new IllegalArgumentException("A non empty prefix is 
required");
+        }
+        return new WildCardBranch(prefix);
+    }
+}
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/PrefixTreeSet.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/PrefixTreeSet.java
new file mode 100644
index 0000000..1a5fafa
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/PrefixTreeSet.java
@@ -0,0 +1,33 @@
+package org.apache.qpid.server.security.access.util;
+
+import java.util.AbstractSet;
+import java.util.Iterator;
+
+public class PrefixTreeSet extends AbstractSet<String>
+{
+    private PrefixTree _tree = PrefixTree.empty();
+
+    @Override
+    public boolean add(String s)
+    {
+        _tree = _tree.mergeWith(s);
+        return true;
+    }
+
+    @Override
+    public Iterator<String> iterator()
+    {
+        return _tree.iterator();
+    }
+
+    @Override
+    public int size()
+    {
+        return _tree.size();
+    }
+
+    public PrefixTree toPrefixTree()
+    {
+        return _tree;
+    }
+}
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/TreeBranch.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/TreeBranch.java
new file mode 100644
index 0000000..529daa2
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/TreeBranch.java
@@ -0,0 +1,236 @@
+/*
+ *  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.util;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterators;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+class TreeBranch extends AbstractTreeBranch
+{
+    final Map<Character, AbstractTreeBranch> _branches;
+
+    TreeBranch(String prefix)
+    {
+        super(prefix);
+        _branches = Collections.emptyMap();
+    }
+
+    TreeBranch(Map<Character, AbstractTreeBranch> branches)
+    {
+        super();
+        _branches = new HashMap<>(branches);
+    }
+
+    TreeBranch(String prefix, Map<Character, AbstractTreeBranch> branches)
+    {
+        super(prefix);
+        _branches = new HashMap<>(branches);
+    }
+
+    TreeBranch(String prefix, AbstractTreeBranch first)
+    {
+        super(prefix);
+        _branches = Collections.singletonMap(first.firstPrefixCharacter(), 
first);
+    }
+
+    TreeBranch(String prefix, AbstractTreeBranch first, AbstractTreeBranch 
second)
+    {
+        super(prefix);
+        _branches = new HashMap<>(2);
+        _branches.put(first.firstPrefixCharacter(), first);
+        _branches.put(second.firstPrefixCharacter(), second);
+    }
+
+    TreeBranch(AbstractTreeBranch first, AbstractTreeBranch second)
+    {
+        super();
+        _branches = new HashMap<>(2);
+        _branches.put(first.firstPrefixCharacter(), first);
+        _branches.put(second.firstPrefixCharacter(), second);
+    }
+
+    @Override
+    public Map<Character, PrefixTree> branches()
+    {
+        return Collections.unmodifiableMap(_branches);
+    }
+
+    @Override
+    public int size()
+    {
+        return _branches.values().stream().mapToInt(PrefixTree::size).sum();
+    }
+
+    @Override
+    boolean contains(String str)
+    {
+        final int length = str.length();
+        if (length > _length && str.startsWith(_prefix))
+        {
+            final String subString = str.substring(_length, length);
+            final PrefixTree subTree = _branches.get(subString.charAt(0));
+            if (subTree != null)
+            {
+                return subTree.match(subString);
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public Iterator<String> iterator()
+    {
+        return new IteratorImpl(this);
+    }
+
+    @Override
+    AbstractTreeBranch mergeString(String str)
+    {
+        final String common = Strings.commonPrefix(str, _prefix);
+        if (common.isEmpty())
+        {
+            return new TreeRoot(this, new FinalBranch(str));
+        }
+        final int commonLength = common.length();
+        if (commonLength == str.length())
+        {
+            if (commonLength == _length)
+            {
+                return newFinalBranch();
+            }
+            return new FinalBranch(str, 
newTree(_prefix.substring(commonLength), _branches));
+        }
+        if (commonLength == _length)
+        {
+            final Map<Character, AbstractTreeBranch> branches = new 
HashMap<>(_branches);
+            final String subString = str.substring(commonLength);
+            final char key = subString.charAt(0);
+            final AbstractTreeBranch branch = branches.get(key);
+            if (branch != null)
+            {
+                branches.put(key, branch.mergeString(subString));
+            }
+            else
+            {
+                branches.put(key, new FinalBranch(subString));
+            }
+            return newTree(common, branches);
+        }
+
+        return new TreeBranch(common,
+                newTree(_prefix.substring(commonLength), _branches),
+                new FinalBranch(str.substring(commonLength)));
+    }
+
+    @Override
+    AbstractTreeBranch mergeWildCard(String prefix)
+    {
+        final String common = Strings.commonPrefix(prefix, _prefix);
+        if (common.isEmpty())
+        {
+            return new TreeRoot(this, new WildCardBranch(prefix));
+        }
+        final int commonLength = common.length();
+        if (commonLength == prefix.length())
+        {
+            return new WildCardBranch(prefix);
+        }
+        if (commonLength == _length)
+        {
+            final Map<Character, AbstractTreeBranch> branches = new 
HashMap<>(_branches);
+            final String subString = prefix.substring(commonLength);
+            final char key = subString.charAt(0);
+            final AbstractTreeBranch branch = branches.get(key);
+            if (branch != null)
+            {
+                branches.put(key, branch.mergeWildCard(subString));
+            }
+            else
+            {
+                branches.put(key, new WildCardBranch(subString));
+            }
+            return newTree(_prefix, branches);
+        }
+
+        return new TreeBranch(common,
+                newTree(_prefix.substring(commonLength), _branches),
+                new WildCardBranch(prefix.substring(commonLength)));
+    }
+
+    FinalBranch newFinalBranch()
+    {
+        return new FinalBranch(_prefix, _branches);
+    }
+
+    TreeBranch newTree(String prefix, Map<Character, AbstractTreeBranch> 
branches)
+    {
+        return new TreeBranch(prefix, branches);
+    }
+
+    static final class IteratorImpl implements Iterator<String>
+    {
+        private final String _prefix;
+
+        private final Iterator<AbstractTreeBranch> _tree;
+
+        private Iterator<String> _branch;
+
+        IteratorImpl(TreeBranch root)
+        {
+            _prefix = root.prefix();
+            _tree = new TreeMap<>(root._branches).values().iterator();
+            _branch = Collections.emptyIterator();
+        }
+
+        IteratorImpl(TreeBranch root, String firstValue)
+        {
+            _prefix = root.prefix();
+            _tree = new TreeMap<>(root._branches).values().iterator();
+            _branch = Iterators.singletonIterator(firstValue);
+        }
+
+        @Override
+        public boolean hasNext()
+        {
+            boolean hasBranch = _branch.hasNext();
+            while (!hasBranch && _tree.hasNext())
+            {
+                _branch = _tree.next().iterator();
+                hasBranch = _branch.hasNext();
+            }
+            return hasBranch;
+        }
+
+        @Override
+        public String next()
+        {
+            while (!_branch.hasNext() && _tree.hasNext())
+            {
+                _branch = _tree.next().iterator();
+            }
+            return _prefix + _branch.next();
+        }
+    }
+}
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/TreeRoot.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/TreeRoot.java
new file mode 100644
index 0000000..19235a8
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/TreeRoot.java
@@ -0,0 +1,76 @@
+/*
+ *  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.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+final class TreeRoot extends TreeBranch
+{
+    TreeRoot(AbstractTreeBranch first, AbstractTreeBranch second)
+    {
+        super(first, second);
+    }
+
+    TreeRoot(Map<Character, AbstractTreeBranch> map)
+    {
+        super(map);
+    }
+
+    @Override
+    boolean contains(String str)
+    {
+        final PrefixTree subTree = _branches.get(str.charAt(0));
+        return subTree != null && subTree.match(str);
+    }
+
+    @Override
+    AbstractTreeBranch mergeString(String str)
+    {
+        final Map<Character, AbstractTreeBranch> branches = new 
HashMap<>(_branches);
+        final char key = str.charAt(0);
+        final AbstractTreeBranch branch = _branches.get(key);
+        if (branch == null)
+        {
+            branches.put(key, new FinalBranch(str));
+        }
+        else
+        {
+            branches.put(key, branch.mergeString(str));
+        }
+        return new TreeRoot(branches);
+    }
+
+    @Override
+    AbstractTreeBranch mergeWildCard(String prefix)
+    {
+        final Map<Character, AbstractTreeBranch> branches = new 
HashMap<>(_branches);
+        final char key = prefix.charAt(0);
+        final AbstractTreeBranch branch = _branches.get(key);
+        if (branch == null)
+        {
+            branches.put(key, new WildCardBranch(prefix));
+        }
+        else
+        {
+            branches.put(key, branch.mergeWildCard(prefix));
+        }
+        return new TreeRoot(branches);
+    }
+}
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/WildCardBranch.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/WildCardBranch.java
new file mode 100644
index 0000000..0854cb6
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/WildCardBranch.java
@@ -0,0 +1,102 @@
+/*
+ *  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.util;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterators;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+
+final class WildCardBranch extends AbstractTreeBranch
+{
+    WildCardBranch(String prefix)
+    {
+        super(prefix);
+    }
+
+    @Override
+    public int size()
+    {
+        return 1;
+    }
+
+    @Override
+    public Map<Character, PrefixTree> branches()
+    {
+        return Collections.emptyMap();
+    }
+
+    @Override
+    boolean contains(String str)
+    {
+        return str.startsWith(_prefix);
+    }
+
+    @Override
+    public Iterator<String> iterator()
+    {
+        return Iterators.singletonIterator(_prefix + "*");
+    }
+
+    @Override
+    AbstractTreeBranch mergeString(String str)
+    {
+        final String common = Strings.commonPrefix(str, _prefix);
+        if (common.isEmpty())
+        {
+            return new TreeRoot(this, new FinalBranch(str));
+        }
+        final int commonLength = common.length();
+        if (commonLength == _length)
+        {
+            return this;
+        }
+        if (commonLength == str.length())
+        {
+            return new FinalBranch(common, new 
WildCardBranch(_prefix.substring(commonLength)));
+        }
+        return new TreeBranch(common,
+                new WildCardBranch(_prefix.substring(commonLength)),
+                new FinalBranch(str.substring(commonLength)));
+    }
+
+    @Override
+    AbstractTreeBranch mergeWildCard(String prefix)
+    {
+        final String common = Strings.commonPrefix(prefix, _prefix);
+        if (common.isEmpty())
+        {
+            return new TreeRoot(this, new WildCardBranch(prefix));
+        }
+        final int commonLength = common.length();
+        if (commonLength == _length)
+        {
+            return this;
+        }
+        if (commonLength == prefix.length())
+        {
+            return new WildCardBranch(prefix);
+        }
+        return new TreeBranch(common,
+                new WildCardBranch(_prefix.substring(commonLength)),
+                new WildCardBranch(prefix.substring(commonLength)));
+    }
+}
diff --git 
a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/WildCardSet.java
 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/WildCardSet.java
new file mode 100644
index 0000000..cf76d0f
--- /dev/null
+++ 
b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/util/WildCardSet.java
@@ -0,0 +1,48 @@
+package org.apache.qpid.server.security.access.util;
+
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.apache.qpid.server.security.access.config.AclRulePredicatesBuilder;
+
+import com.google.common.collect.Iterators;
+
+public class WildCardSet extends AbstractSet<Object>
+{
+    private static final WildCardSet INSTANCE = new WildCardSet();
+
+    public static WildCardSet newSet()
+    {
+        return INSTANCE;
+    }
+
+    private WildCardSet()
+    {
+        super();
+    }
+
+    @Override
+    public boolean add(Object o)
+    {
+        return false;
+    }
+
+    @Override
+    public boolean addAll(Collection<?> c)
+    {
+        return false;
+    }
+
+    @Override
+    public Iterator<Object> iterator()
+    {
+        return Iterators.singletonIterator(AclRulePredicatesBuilder.WILD_CARD);
+    }
+
+    @Override
+    public int size()
+    {
+        return 1;
+    }
+}
diff --git 
a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclRulePredicatesTest.java
 
b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclRulePredicatesTest.java
index 48b2a8e..76dbac5 100644
--- 
a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclRulePredicatesTest.java
+++ 
b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclRulePredicatesTest.java
@@ -18,8 +18,10 @@
  */
 package org.apache.qpid.server.security.access.config;
 
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -116,21 +118,6 @@ public class AclRulePredicatesTest extends UnitTestBase
     }
 
     @Test
-    public void testParseThrowsExceptionIfHostnameSpecified2Times()
-    {
-        _builder.parse(FROM_NETWORK.name(), "network");
-        try
-        {
-            _builder.parse(FROM_NETWORK.name(), "network2");
-            fail("Exception not thrown");
-        }
-        catch (IllegalStateException e)
-        {
-            // pass
-        }
-    }
-
-    @Test
     public void testParseAttributesRule()
     {
         final String attributes = "attribute1,attribute2";
@@ -163,7 +150,7 @@ public class AclRulePredicatesTest extends UnitTestBase
                 .parse(FROM_NETWORK.name(), "network")
                 .build(_firewallRuleFactory);
 
-        final Map<Property, String> properties = 
predicates.getParsedProperties();
+        final Map<Property, Object> properties = 
predicates.getParsedProperties();
 
         assertEquals(3, properties.size());
         assertEquals(properties.get(NAME), "name");
@@ -171,7 +158,6 @@ public class AclRulePredicatesTest extends UnitTestBase
         assertEquals(properties.get(FROM_NETWORK), "network");
     }
 
-
     @Test
     public void testAttributeNames()
     {
@@ -263,6 +249,29 @@ public class AclRulePredicatesTest extends UnitTestBase
     }
 
     @Test
+    public void testParse_MultiValue()
+    {
+        final String nameA = "name.A";
+        AclRulePredicates predicates = _builder.parse(NAME.name(), 
nameA).build(_firewallRuleFactory);
+        assertEquals(Collections.singleton(nameA), predicates.get(NAME));
+
+        final String nameB = "name.B";
+        predicates = _builder.parse(NAME.name(), 
nameB).build(_firewallRuleFactory);
+        assertEquals(new HashSet<>(Arrays.asList(nameA, nameB)), 
predicates.get(NAME));
+
+        final String nameX = "name.*";
+        predicates = _builder.parse(NAME.name(), 
nameX).build(_firewallRuleFactory);
+        assertEquals(Collections.singleton(nameX), predicates.get(NAME));
+
+        final String nameC = "name.C";
+        predicates = _builder.parse(NAME.name(), 
nameC).build(_firewallRuleFactory);
+        assertEquals(Collections.singleton(nameX), predicates.get(NAME));
+
+        predicates = _builder.parse(NAME.name(), 
"*").build(_firewallRuleFactory);
+        assertEquals(Collections.singleton("*"), predicates.get(NAME));
+    }
+
+    @Test
     public void testEqualsHashCode()
     {
         _builder.parse(NAME.name(), "name");
diff --git 
a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateBuilderTest.java
 
b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateBuilderTest.java
index bfbebe4..0418299 100644
--- 
a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateBuilderTest.java
+++ 
b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateBuilderTest.java
@@ -31,6 +31,8 @@ import 
org.apache.qpid.server.security.access.config.ObjectProperties;
 import org.apache.qpid.server.security.access.config.Property;
 import org.apache.qpid.server.security.access.config.RulePredicate;
 import org.apache.qpid.server.security.access.firewall.FirewallRuleFactory;
+import org.apache.qpid.server.security.access.util.PrefixTreeSet;
+import org.apache.qpid.server.security.access.util.WildCardSet;
 import org.apache.qpid.server.security.auth.TestPrincipalUtils;
 import org.apache.qpid.test.utils.UnitTestBase;
 
@@ -305,4 +307,69 @@ public class RulePredicateBuilderTest extends UnitTestBase
 
         assertFalse(predicate.matches(LegacyOperation.PUBLISH, new 
ObjectProperties(), _subject));
     }
+
+    @Test
+    public void testMatch_PrefixTree()
+    {
+        final PrefixTreeSet tree = new PrefixTreeSet();
+        tree.addAll(Arrays.asList("Exchange.public.*", "Exchange.private.A", 
"Exchange.private.B"));
+        final RulePredicate predicate = 
_builder.build(Collections.singletonMap(NAME, tree));
+
+        ObjectProperties op = new ObjectProperties(NAME, "Exchange.private.A");
+        assertTrue(predicate.matches(LegacyOperation.PUBLISH, op, _subject));
+
+        op = new ObjectProperties(NAME, "Exchange.private.B");
+        assertTrue(predicate.matches(LegacyOperation.PUBLISH, op, _subject));
+
+        op = new ObjectProperties(NAME, "Exchange.public.ABC");
+        assertTrue(predicate.matches(LegacyOperation.PUBLISH, op, _subject));
+    }
+
+    @Test
+    public void testDoesNotMatch_PrefixTree()
+    {
+        final PrefixTreeSet tree = new PrefixTreeSet();
+        tree.addAll(Arrays.asList("Exchange.public.*", "Exchange.private.A", 
"Exchange.private.B"));
+        final RulePredicate predicate = 
_builder.build(Collections.singletonMap(NAME, tree));
+
+        final ObjectProperties op = new ObjectProperties(NAME, 
"Exchange.private.xyz");
+        assertFalse(predicate.matches(LegacyOperation.PUBLISH, op, _subject));
+    }
+
+    @Test
+    public void testMatch_PrefixTree_single()
+    {
+        final PrefixTreeSet tree = new PrefixTreeSet();
+        tree.add("Exchange.public.*");
+        final RulePredicate predicate = 
_builder.build(Collections.singletonMap(NAME, tree));
+
+        final ObjectProperties op = new ObjectProperties(NAME, 
"Exchange.public.A");
+        assertTrue(predicate.matches(LegacyOperation.PUBLISH, op, _subject));
+    }
+
+    @Test
+    public void testDoesNotMatch_PrefixTree_single()
+    {
+        final PrefixTreeSet tree = new PrefixTreeSet();
+        tree.add("Exchange.public.*");
+        final RulePredicate predicate = 
_builder.build(Collections.singletonMap(NAME, tree));
+
+        final ObjectProperties op = new ObjectProperties(NAME, 
"Exchange.private.xyz");
+        assertFalse(predicate.matches(LegacyOperation.PUBLISH, op, _subject));
+    }
+
+    @Test
+    public void testMatch_WildcardSet()
+    {
+        final RulePredicate predicate = 
_builder.build(Collections.singletonMap(NAME, WildCardSet.newSet()));
+
+        ObjectProperties op = new ObjectProperties(NAME, "Exchange.private.A");
+        assertTrue(predicate.matches(LegacyOperation.PUBLISH, op, _subject));
+
+        op = new ObjectProperties(NAME, "Exchange.private.B");
+        assertTrue(predicate.matches(LegacyOperation.PUBLISH, op, _subject));
+
+        op = new ObjectProperties(NAME, "Exchange.public.ABC");
+        assertTrue(predicate.matches(LegacyOperation.PUBLISH, op, _subject));
+    }
 }
diff --git 
a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateTest.java
 
b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateTest.java
index a9baaea..1f8478e 100644
--- 
a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateTest.java
+++ 
b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/predicates/RulePredicateTest.java
@@ -416,4 +416,61 @@ public class RulePredicateTest extends UnitTestBase
         assertFalse(rule.matches(LegacyOperation.PUBLISH, ObjectType.EXCHANGE, 
new ObjectProperties(), _subject));
         assertFalse(rule.anyPropertiesMatch());
     }
+
+    @Test
+    public void testMatch_Properties_MultiValue()
+    {
+        final Rule rule = _builder
+                .withPredicate(Property.ROUTING_KEY, "broadcast.public")
+                .withPredicate(Property.NAME, "broadcast.A")
+                .withPredicate(Property.NAME, "broadcast.B")
+                .withPredicate(Property.NAME, "broadcast.X.*")
+                .withOperation(LegacyOperation.PUBLISH)
+                .withObject(ObjectType.EXCHANGE)
+                .withOutcome(RuleOutcome.ALLOW)
+                .build(_firewallRuleFactory);
+
+        ObjectProperties action = new ObjectProperties();
+        action.put(Property.ROUTING_KEY, "broadcast.public");
+        action.put(Property.NAME, "broadcast.A");
+        action.put(Property.METHOD_NAME, "publish");
+
+        assertTrue(rule.matches(LegacyOperation.PUBLISH, ObjectType.EXCHANGE, 
action, _subject));
+
+        action = new ObjectProperties();
+        action.put(Property.ROUTING_KEY, "broadcast.public");
+        action.put(Property.NAME, "broadcast.B");
+        action.put(Property.METHOD_NAME, "publish");
+
+        assertTrue(rule.matches(LegacyOperation.PUBLISH, ObjectType.EXCHANGE, 
action, _subject));
+
+        action = new ObjectProperties();
+        action.put(Property.ROUTING_KEY, "broadcast.public");
+        action.put(Property.NAME, "broadcast.X.new");
+        action.put(Property.METHOD_NAME, "publish");
+
+        assertTrue(rule.matches(LegacyOperation.PUBLISH, ObjectType.EXCHANGE, 
action, _subject));
+    }
+
+    @Test
+    public void testDoesNotMatch_Properties_MultiValue()
+    {
+        final Rule rule = _builder
+                .withPredicate(Property.ROUTING_KEY, "generic.public")
+                .withPredicate(Property.NAME, "broadcast.A")
+                .withPredicate(Property.NAME, "broadcast.B")
+                .withPredicate(Property.NAME, "broadcast.X.*")
+                .withOperation(LegacyOperation.PUBLISH)
+                .withObject(ObjectType.EXCHANGE)
+                .withOutcome(RuleOutcome.ALLOW)
+                .build(_firewallRuleFactory);
+
+        final ObjectProperties action = new ObjectProperties();
+        action.put(Property.ROUTING_KEY, "broadcast.public");
+        action.setName("broadcast");
+        action.put(Property.METHOD_NAME, "publish");
+
+        assertFalse(rule.matches(LegacyOperation.PUBLISH, ObjectType.EXCHANGE, 
action, _subject));
+        assertFalse(rule.matches(LegacyOperation.PUBLISH, ObjectType.EXCHANGE, 
new ObjectProperties(), _subject));
+    }
 }
\ No newline at end of file
diff --git 
a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/util/PrefixTreeTest.java
 
b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/util/PrefixTreeTest.java
new file mode 100644
index 0000000..41473c4
--- /dev/null
+++ 
b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/util/PrefixTreeTest.java
@@ -0,0 +1,942 @@
+/*
+ * 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.util;
+
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import com.google.common.collect.Streams;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class PrefixTreeTest extends UnitTestBase
+{
+    @Test
+    public void testPrefixWithWildcard_Single()
+    {
+        
testPrefixWithWildcard_Single(PrefixTree.fromPrefixWithWildCard("abcd"));
+        
testPrefixWithWildcard_Single(PrefixTree.fromPrefixWithWildCard("abcd").mergeWithPrefix("abcd"));
+        
testPrefixWithWildcard_Single(PrefixTree.fromPrefixWithWildCard("abcdXYZ").mergeWithPrefix("abcd"));
+        
testPrefixWithWildcard_Single(PrefixTree.fromPrefixWithWildCard("abcd").mergeWithPrefix("abcdXYZ"));
+
+        
testPrefixWithWildcard_Single(PrefixTree.fromPrefixWithWildCard("abcd").mergeWithFinalValue("abcd"));
+        
testPrefixWithWildcard_Single(PrefixTree.fromFinalValue("abcd").mergeWithPrefix("abcd"));
+        
testPrefixWithWildcard_Single(PrefixTree.fromPrefixWithWildCard("abcd").mergeWithFinalValue("abcdXYZ"));
+        
testPrefixWithWildcard_Single(PrefixTree.fromFinalValue("abcdXYZ").mergeWithPrefix("abcd"));
+    }
+
+    private void testPrefixWithWildcard_Single(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(1, tree.size());
+        for (final String str : tree)
+        {
+            assertEquals("abcd*", str);
+        }
+        assertEquals("abcd", tree.prefix());
+        assertEquals('a', tree.firstPrefixCharacter());
+        assertNotNull(tree.branches());
+        assertTrue(tree.branches().isEmpty());
+
+        assertTrue(tree.match("abcd"));
+        assertTrue(tree.match("abcd.e"));
+        assertFalse(tree.match("Abcdx"));
+        assertFalse(tree.match("abc"));
+        assertFalse(tree.match("ab"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testPrefixWithWildcard()
+    {
+        final String[] strings = new String[]{"exchange.public.*", 
"exchange.private.*", "response.public.*", "response.private.*", "response.*"};
+        for (final String[] strs : permute(strings, 0))
+        {
+            final PrefixTree tree = PrefixTree.from(strs[0])
+                    .mergeWith(strs[1])
+                    .mergeWith(strs[2])
+                    .mergeWith(strs[3])
+                    .mergeWith(strs[4]);
+            testPrefixWithWildcard(tree);
+            testPrefixWithWildcard(PrefixTree.from(Arrays.asList(strs)));
+        }
+    }
+
+    private void testPrefixWithWildcard(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(3, tree.size());
+        final String[] array = new String[3];
+        int i = 0;
+        for (final String str : tree)
+        {
+            array[i++] = str;
+        }
+        assertArrayEquals(new String[]{"exchange.private.*", 
"exchange.public.*", "response.*"}, array);
+        assertNotNull(tree.branches());
+        assertEquals(2, tree.branches().size());
+
+        PrefixTree branch = tree.branches().get('e');
+        assertNotNull(branch);
+        assertEquals('e', branch.firstPrefixCharacter());
+        assertEquals("exchange.p", branch.prefix());
+        assertEquals(2, branch.size());
+        assertEquals(2, branch.branches().size());
+        assertNotNull(branch.branches().get('r'));
+        assertEquals("rivate.", branch.branches().get('r').prefix());
+        assertNotNull(branch.branches().get('u'));
+        assertEquals("ublic.", branch.branches().get('u').prefix());
+
+        branch = tree.branches().get('r');
+        assertNotNull(branch);
+        assertEquals('r', branch.firstPrefixCharacter());
+        assertEquals("response.", branch.prefix());
+        assertEquals(1, branch.size());
+        assertEquals(0, branch.branches().size());
+
+        assertTrue(tree.match("response.x"));
+        assertTrue(tree.match("response."));
+        assertTrue(tree.match("exchange.private.A"));
+        assertTrue(tree.match("exchange.private."));
+        assertTrue(tree.match("exchange.public.B"));
+        assertTrue(tree.match("exchange.public."));
+        assertFalse(tree.match("response"));
+        assertFalse(tree.match("exchange.private"));
+        assertFalse(tree.match("exchange.public"));
+
+        assertFalse(tree.match("exchange.rest"));
+        assertFalse(tree.match("reg"));
+        assertFalse(tree.match("error"));
+        assertFalse(tree.match("warning"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testPrefixWithWildcard_RootWith3Branches()
+    {
+        final String[] strings = new String[]{"A", "B", "C"};
+        for (final String[] strs : permute(strings, 0))
+        {
+            final PrefixTree tree = PrefixTree.fromPrefixWithWildCard(strs[0])
+                    .mergeWithPrefix(strs[1])
+                    .mergeWithPrefix(strs[2]);
+            testPrefixWithWildcard_RootWith3Branches(tree);
+        }
+    }
+
+    private void testPrefixWithWildcard_RootWith3Branches(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(3, tree.size());
+        final String[] array = new String[3];
+        int i = 0;
+        for (final String str : tree)
+        {
+            array[i++] = str;
+        }
+        assertArrayEquals(new String[]{"A*", "B*", "C*"}, array);
+        assertNotNull(tree.branches());
+        assertEquals(3, tree.branches().size());
+
+        PrefixTree branch = tree.branches().get('A');
+        assertNotNull(branch);
+        assertEquals('A', branch.firstPrefixCharacter());
+        assertEquals("A", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        branch = tree.branches().get('B');
+        assertNotNull(branch);
+        assertEquals('B', branch.firstPrefixCharacter());
+        assertEquals("B", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        branch = tree.branches().get('C');
+        assertNotNull(branch);
+        assertEquals('C', branch.firstPrefixCharacter());
+        assertEquals("C", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        assertTrue(tree.match("A"));
+        assertTrue(tree.match("Ax"));
+        assertTrue(tree.match("B"));
+        assertTrue(tree.match("Bx"));
+        assertTrue(tree.match("C"));
+        assertTrue(tree.match("Cx"));
+
+        assertFalse(tree.match("x"));
+        assertFalse(tree.match("b"));
+        assertFalse(tree.match("cC"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testPrefixWithWildcard_BranchSplit()
+    {
+        final String[] strings = new String[]{"AB*", "AC*"};
+        for (final String[] strs : permute(strings, 0))
+        {
+            final PrefixTree tree = PrefixTree.from(strs[0])
+                    .mergeWith(strs[1]);
+            testPrefixWithWildcard_BranchSplit(tree);
+            
testPrefixWithWildcard_BranchSplit(PrefixTree.from(Arrays.asList(strs)));
+        }
+    }
+
+    private void testPrefixWithWildcard_BranchSplit(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(2, tree.size());
+        final String[] array = new String[2];
+        int i = 0;
+        for (final String str : tree)
+        {
+            array[i++] = str;
+        }
+        assertArrayEquals(new String[]{"AB*", "AC*"}, array);
+        assertEquals(2, tree.size());
+        assertNotNull(tree.branches());
+        assertEquals(2, tree.branches().size());
+
+        assertEquals('A', tree.firstPrefixCharacter());
+        assertEquals("A", tree.prefix());
+
+        PrefixTree branch = tree.branches().get('B');
+        assertNotNull(branch);
+        assertEquals('B', branch.firstPrefixCharacter());
+        assertEquals("B", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        branch = tree.branches().get('C');
+        assertNotNull(branch);
+        assertEquals('C', branch.firstPrefixCharacter());
+        assertEquals("C", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        assertTrue(tree.match("AB"));
+        assertTrue(tree.match("ABx"));
+        assertTrue(tree.match("AC"));
+        assertTrue(tree.match("ACx"));
+
+        assertFalse(tree.match("A"));
+        assertFalse(tree.match("Ab"));
+        assertFalse(tree.match("Ac"));
+        assertFalse(tree.match("b"));
+        assertFalse(tree.match("cC"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testExactString_Single()
+    {
+        testExactString_Single(PrefixTree.fromFinalValue("abcd"));
+        
testExactString_Single(PrefixTree.fromFinalValue("abcd").mergeWithFinalValue("abcd"));
+    }
+
+    private void testExactString_Single(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(1, tree.size());
+        for (final String str : tree)
+        {
+            assertEquals("abcd", str);
+        }
+        assertEquals("abcd", tree.prefix());
+        assertEquals('a', tree.firstPrefixCharacter());
+        assertNotNull(tree.branches());
+        assertTrue(tree.branches().isEmpty());
+
+        assertTrue(tree.match("abcd"));
+        assertFalse(tree.match("aBcd"));
+        assertFalse(tree.match("abcd."));
+        assertFalse(tree.match("abc"));
+        assertFalse(tree.match("x"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testExactString()
+    {
+        final String[] strings = new String[]{"exchange.public", 
"exchange.private", "response.public", "response.private", "response"};
+        for (final String[] strs : permute(strings, 0))
+        {
+            final PrefixTree tree = PrefixTree.from(strs[0])
+                    .mergeWith(strs[1])
+                    .mergeWith(strs[2])
+                    .mergeWith(strs[3])
+                    .mergeWith(strs[4]);
+            testExactString(tree);
+            testExactString(PrefixTree.from(Arrays.asList(strs)));
+        }
+    }
+
+    private void testExactString(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(5, tree.size());
+        final String[] array = new String[5];
+        int i = 0;
+        for (final String str : tree)
+        {
+            array[i++] = str;
+        }
+        assertArrayEquals(new String[]{"exchange.private", "exchange.public", 
"response", "response.private", "response.public"}, array);
+        assertNotNull(tree.branches());
+        assertEquals(2, tree.branches().size());
+
+        PrefixTree branch = tree.branches().get('e');
+        assertNotNull(branch);
+        assertEquals('e', branch.firstPrefixCharacter());
+        assertEquals("exchange.p", branch.prefix());
+        assertEquals(2, branch.size());
+        assertEquals(2, branch.branches().size());
+        assertNotNull(branch.branches().get('r'));
+        assertEquals("rivate", branch.branches().get('r').prefix());
+        assertNotNull(branch.branches().get('u'));
+        assertEquals("ublic", branch.branches().get('u').prefix());
+
+        branch = tree.branches().get('r');
+        assertNotNull(branch);
+        assertEquals('r', branch.firstPrefixCharacter());
+        assertEquals("response", branch.prefix());
+        assertEquals(3, branch.size());
+        assertEquals(1, branch.branches().size());
+
+        branch = branch.branches().get('.');
+        assertNotNull(branch);
+        assertEquals('.', branch.firstPrefixCharacter());
+        assertEquals(".p", branch.prefix());
+        assertEquals(2, branch.size());
+        assertEquals(2, branch.branches().size());
+
+        assertNotNull(branch.branches().get('r'));
+        assertEquals("rivate", branch.branches().get('r').prefix());
+        assertNotNull(branch.branches().get('u'));
+        assertEquals("ublic", branch.branches().get('u').prefix());
+
+        assertTrue(tree.match("exchange.private"));
+        assertTrue(tree.match("exchange.public"));
+        assertTrue(tree.match("response"));
+        assertTrue(tree.match("response.private"));
+        assertTrue(tree.match("response.public"));
+
+        assertFalse(tree.match("exchange.privat"));
+        assertFalse(tree.match("exchange.privateX"));
+        assertFalse(tree.match("exchange.publi"));
+        assertFalse(tree.match("exchange.publicX"));
+        assertFalse(tree.match("respons"));
+        assertFalse(tree.match("response.p"));
+        assertFalse(tree.match("response.privat"));
+        assertFalse(tree.match("response.privateX"));
+        assertFalse(tree.match("response.publi"));
+        assertFalse(tree.match("response.publicX"));
+
+        assertFalse(tree.match("exchange.rest"));
+        assertFalse(tree.match("reg"));
+        assertFalse(tree.match("error"));
+        assertFalse(tree.match("warning"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testExactString_RootWith3Branches()
+    {
+        final String[] strings = new String[]{"A", "B", "C"};
+        for (final String[] strs : permute(strings, 0))
+        {
+            final PrefixTree tree = PrefixTree.fromFinalValue(strs[0])
+                    .mergeWithFinalValue(strs[1])
+                    .mergeWithFinalValue(strs[2]);
+            testExactString_RootWith3Branches(tree);
+        }
+    }
+
+    private void testExactString_RootWith3Branches(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(3, tree.size());
+        final String[] array = new String[3];
+        int i = 0;
+        for (final String str : tree)
+        {
+            array[i++] = str;
+        }
+        assertArrayEquals(new String[]{"A", "B", "C"}, array);
+        assertNotNull(tree.branches());
+        assertEquals(3, tree.branches().size());
+
+        PrefixTree branch = tree.branches().get('A');
+        assertNotNull(branch);
+        assertEquals('A', branch.firstPrefixCharacter());
+        assertEquals("A", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        branch = tree.branches().get('B');
+        assertNotNull(branch);
+        assertEquals('B', branch.firstPrefixCharacter());
+        assertEquals("B", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        branch = tree.branches().get('C');
+        assertNotNull(branch);
+        assertEquals('C', branch.firstPrefixCharacter());
+        assertEquals("C", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        assertTrue(tree.match("A"));
+        assertTrue(tree.match("B"));
+        assertTrue(tree.match("C"));
+
+        assertFalse(tree.match("Ax"));
+        assertFalse(tree.match("b"));
+        assertFalse(tree.match("Cc"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testExactString_BranchSpit()
+    {
+        final String[] strings = new String[]{"A", "AB", "AC"};
+        for (final String[] strs : permute(strings, 0))
+        {
+            final PrefixTree tree = PrefixTree.from(strs[0])
+                    .mergeWith(strs[1])
+                    .mergeWith(strs[2]);
+            testExactString_BranchSpit(tree);
+            testExactString_BranchSpit(PrefixTree.from(Arrays.asList(strs)));
+        }
+    }
+
+    private void testExactString_BranchSpit(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(3, tree.size());
+        final String[] array = new String[3];
+        int i = 0;
+        for (final String str : tree)
+        {
+            array[i++] = str;
+        }
+        assertArrayEquals(new String[]{"A", "AB", "AC"}, array);
+        assertNotNull(tree.branches());
+        assertEquals(2, tree.branches().size());
+        assertEquals("A", tree.prefix());
+        assertEquals('A', tree.firstPrefixCharacter());
+
+        PrefixTree branch = tree.branches().get('B');
+        assertNotNull(branch);
+        assertEquals('B', branch.firstPrefixCharacter());
+        assertEquals("B", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        branch = tree.branches().get('C');
+        assertNotNull(branch);
+        assertEquals('C', branch.firstPrefixCharacter());
+        assertEquals("C", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        assertTrue(tree.match("A"));
+        assertTrue(tree.match("AB"));
+        assertTrue(tree.match("AC"));
+
+        assertFalse(tree.match("Ax"));
+        assertFalse(tree.match("Ab"));
+        assertFalse(tree.match("Ac"));
+        assertFalse(tree.match("aa"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testMixing()
+    {
+        final String[] strings = new String[]{"exchange.public", 
"exchange.private.A", "exchange.private.*", "response.public", 
"response.private", "response.p*", "response"};
+        for (final String[] strs : permute(strings, 0))
+        {
+            final PrefixTree tree = PrefixTree.from(strs[0])
+                    .mergeWith(strs[1])
+                    .mergeWith(strs[2])
+                    .mergeWith(strs[3])
+                    .mergeWith(strs[4])
+                    .mergeWith(strs[5])
+                    .mergeWith(strs[6]);
+            testMixing(tree);
+            testMixing(PrefixTree.from(Arrays.asList(strs)));
+        }
+    }
+
+    private void testMixing(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(4, tree.size());
+        final String[] array = new String[4];
+        int i = 0;
+        for (final String str : tree)
+        {
+            array[i++] = str;
+        }
+        assertArrayEquals(new String[]{"exchange.private.*", 
"exchange.public", "response", "response.p*"}, array);
+        assertNotNull(tree.branches());
+        assertEquals(2, tree.branches().size());
+
+        PrefixTree branch = tree.branches().get('e');
+        assertNotNull(branch);
+        assertEquals('e', branch.firstPrefixCharacter());
+        assertEquals("exchange.p", branch.prefix());
+        assertEquals(2, branch.size());
+        assertEquals(2, branch.branches().size());
+        assertNotNull(branch.branches().get('r'));
+        assertEquals("rivate.", branch.branches().get('r').prefix());
+        assertNotNull(branch.branches().get('u'));
+        assertEquals("ublic", branch.branches().get('u').prefix());
+
+        branch = tree.branches().get('r');
+        assertNotNull(branch);
+        assertEquals('r', branch.firstPrefixCharacter());
+        assertEquals("response", branch.prefix());
+        assertEquals(2, branch.size());
+        assertEquals(1, branch.branches().size());
+
+        branch = branch.branches().get('.');
+        assertNotNull(branch);
+        assertEquals('.', branch.firstPrefixCharacter());
+        assertEquals(".p", branch.prefix());
+        assertEquals(1, branch.size());
+        assertTrue(branch.branches().isEmpty());
+
+
+        assertTrue(tree.match("exchange.private.A"));
+        assertTrue(tree.match("exchange.private."));
+        assertTrue(tree.match("exchange.public"));
+        assertTrue(tree.match("response"));
+        assertTrue(tree.match("response.private"));
+        assertTrue(tree.match("response.public"));
+        assertTrue(tree.match("response.p"));
+
+        assertFalse(tree.match("exchange.privat"));
+        assertFalse(tree.match("exchange.privateX"));
+        assertFalse(tree.match("exchange.publi"));
+        assertFalse(tree.match("exchange.publicX"));
+        assertFalse(tree.match("respons"));
+        assertFalse(tree.match("response."));
+
+        assertFalse(tree.match("exchange.rest"));
+        assertFalse(tree.match("reg"));
+        assertFalse(tree.match("error"));
+        assertFalse(tree.match("warning"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testMixing_BranchSplit()
+    {
+        final String[] strings = new String[]{"AB*", "AC*", "AD", "AE"};
+        for (final String[] strs : permute(strings, 0))
+        {
+            final PrefixTree tree = PrefixTree.from(strs[0])
+                    .mergeWith(strs[1])
+                    .mergeWith(strs[2])
+                    .mergeWith(strs[3]);
+            testMixing_BranchSplit(tree);
+            testMixing_BranchSplit(PrefixTree.from(Arrays.asList(strs)));
+        }
+    }
+
+    private void testMixing_BranchSplit(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(4, tree.size());
+        final String[] array = new String[4];
+        int i = 0;
+        for (final String str : tree)
+        {
+            array[i++] = str;
+        }
+        assertArrayEquals(new String[]{"AB*", "AC*", "AD", "AE"}, array);
+        assertNotNull(tree.branches());
+        assertEquals(4, tree.branches().size());
+
+        assertEquals('A', tree.firstPrefixCharacter());
+        assertEquals("A", tree.prefix());
+
+        PrefixTree branch = tree.branches().get('B');
+        assertNotNull(branch);
+        assertEquals('B', branch.firstPrefixCharacter());
+        assertEquals("B", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        branch = tree.branches().get('C');
+        assertNotNull(branch);
+        assertEquals('C', branch.firstPrefixCharacter());
+        assertEquals("C", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        branch = tree.branches().get('D');
+        assertNotNull(branch);
+        assertEquals('D', branch.firstPrefixCharacter());
+        assertEquals("D", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        branch = tree.branches().get('E');
+        assertNotNull(branch);
+        assertEquals('E', branch.firstPrefixCharacter());
+        assertEquals("E", branch.prefix());
+        assertTrue(branch.branches().isEmpty());
+
+        assertTrue(tree.match("AB"));
+        assertTrue(tree.match("ABx"));
+        assertTrue(tree.match("AC"));
+        assertTrue(tree.match("ACx"));
+        assertTrue(tree.match("AD"));
+        assertTrue(tree.match("AE"));
+
+        assertFalse(tree.match("A"));
+        assertFalse(tree.match("Ab"));
+        assertFalse(tree.match("Ac"));
+        assertFalse(tree.match("Ad"));
+        assertFalse(tree.match("aE"));
+        assertFalse(tree.match("ADx"));
+        assertFalse(tree.match("b"));
+        assertFalse(tree.match("cC"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testMixing_BranchSplit2()
+    {
+        final String[] strings = new String[]{"AXB*", "AXC*", "AYD", "AYE"};
+        for (final String[] strs : permute(strings, 0))
+        {
+            final PrefixTree tree = PrefixTree.from(strs[0])
+                    .mergeWith(strs[1])
+                    .mergeWith(strs[2])
+                    .mergeWith(strs[3]);
+            testMixing_BranchSplit2(tree);
+            testMixing_BranchSplit2(PrefixTree.from(Arrays.asList(strs)));
+        }
+    }
+
+    private void testMixing_BranchSplit2(PrefixTree tree)
+    {
+        assertNotNull(tree);
+        assertEquals(4, tree.size());
+        final String[] array = new String[4];
+        int i = 0;
+        for (final String str : tree)
+        {
+            array[i++] = str;
+        }
+        assertArrayEquals(new String[]{"AXB*", "AXC*", "AYD", "AYE"}, array);
+        assertNotNull(tree.branches());
+        assertEquals(2, tree.branches().size());
+
+        assertEquals('A', tree.firstPrefixCharacter());
+        assertEquals("A", tree.prefix());
+
+        PrefixTree branch = tree.branches().get('X');
+        assertNotNull(branch);
+        assertEquals('X', branch.firstPrefixCharacter());
+        assertEquals("X", branch.prefix());
+        assertEquals(2, branch.branches().size());
+
+        PrefixTree subBranch = branch.branches().get('B');
+        assertNotNull(subBranch);
+        assertEquals('B', subBranch.firstPrefixCharacter());
+        assertEquals("B", subBranch.prefix());
+        assertTrue(subBranch.branches().isEmpty());
+
+        subBranch = branch.branches().get('C');
+        assertNotNull(branch);
+        assertEquals('C', subBranch.firstPrefixCharacter());
+        assertEquals("C", subBranch.prefix());
+        assertTrue(subBranch.branches().isEmpty());
+
+        branch = tree.branches().get('Y');
+        assertNotNull(branch);
+        assertEquals('Y', branch.firstPrefixCharacter());
+        assertEquals("Y", branch.prefix());
+        assertEquals(2, branch.branches().size());
+
+        subBranch = branch.branches().get('D');
+        assertNotNull(subBranch);
+        assertEquals('D', subBranch.firstPrefixCharacter());
+        assertEquals("D", subBranch.prefix());
+        assertTrue(subBranch.branches().isEmpty());
+
+        subBranch = branch.branches().get('E');
+        assertNotNull(subBranch);
+        assertEquals('E', subBranch.firstPrefixCharacter());
+        assertEquals("E", subBranch.prefix());
+        assertTrue(subBranch.branches().isEmpty());
+
+        assertTrue(tree.match("AXB"));
+        assertTrue(tree.match("AXBx"));
+        assertTrue(tree.match("AXC"));
+        assertTrue(tree.match("AXCx"));
+        assertTrue(tree.match("AYD"));
+        assertTrue(tree.match("AYE"));
+
+        assertFalse(tree.match("A"));
+        assertFalse(tree.match("AXb"));
+        assertFalse(tree.match("AXc"));
+        assertFalse(tree.match("AYd"));
+        assertFalse(tree.match("aYE"));
+        assertFalse(tree.match("AYDx"));
+        assertFalse(tree.match("b"));
+        assertFalse(tree.match("cC"));
+        assertFalse(tree.match(""));
+        assertFalse(tree.match(null));
+    }
+
+    @Test
+    public void testFrom_Exception()
+    {
+        try
+        {
+            PrefixTree.from((String) null);
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+
+        try
+        {
+            PrefixTree.from("");
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    @Test
+    public void testFromFinalValue_Exception()
+    {
+        try
+        {
+            PrefixTree.fromFinalValue(null);
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+
+        try
+        {
+            PrefixTree.from("");
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    @Test
+    public void testFromPrefixWithWildCard_Exception()
+    {
+        try
+        {
+            PrefixTree.fromPrefixWithWildCard(null);
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+
+        try
+        {
+            PrefixTree.fromPrefixWithWildCard("");
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    @Test
+    public void testMergeWith_Exception()
+    {
+        final PrefixTree tree = PrefixTree.from("A");
+        try
+        {
+            tree.mergeWith((String) null);
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+
+        try
+        {
+            tree.mergeWith("");
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    @Test
+    public void testMergeWithPrefix_Exception()
+    {
+        final PrefixTree tree = PrefixTree.from("A");
+        try
+        {
+            tree.mergeWithPrefix(null);
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+
+        try
+        {
+            tree.mergeWithPrefix("");
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    @Test
+    public void testMergeWithFinalValue_Exception()
+    {
+        final PrefixTree tree = PrefixTree.from("A");
+        try
+        {
+            tree.mergeWithFinalValue(null);
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+
+        try
+        {
+            tree.mergeWithFinalValue("");
+            fail();
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    @Test
+    public void testFirstPrefixCharacter()
+    {
+        final PrefixTree tree = 
PrefixTree.fromFinalValue("A").mergeWithFinalValue("B");
+        try
+        {
+            tree.firstPrefixCharacter();
+            fail();
+        }
+        catch (UnsupportedOperationException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    private List<String[]> permute(String[] array, int startIndex)
+    {
+        final List<String[]> result = new ArrayList<>();
+        result.add(array);
+        for (int i = startIndex + 1; i < array.length; i++)
+        {
+            final String[] copy = Arrays.copyOf(array, array.length);
+            final String aux = copy[startIndex];
+            copy[startIndex] = copy[i];
+            copy[i] = aux;
+            result.addAll(permute(copy, startIndex + 1));
+        }
+        return result;
+    }
+
+    @Test
+    public void testIterator()
+    {
+        final String[] strings = new String[]{"AXB*", "AXC*", "AYD", "AYE", 
"D"};
+        for (final String[] strs : permute(strings, 0))
+        {
+            final PrefixTree tree = PrefixTree.from(Arrays.asList(strs));
+            testIterator(tree, strings);
+        }
+    }
+
+    private void testIterator(PrefixTree tree, String[] strings)
+    {
+        assertNotNull(tree);
+
+        final List<String> list = 
Streams.stream(tree).collect(Collectors.toList());
+        assertEquals(Arrays.asList(strings), list);
+
+        final Iterator<String> iterator = tree.iterator();
+        assertTrue(iterator.hasNext());
+        assertTrue(iterator.hasNext());
+        for (final String str : strings)
+        {
+            assertEquals(str, iterator.next());
+        }
+        assertFalse(iterator.hasNext());
+        assertFalse(iterator.hasNext());
+
+        try
+        {
+            iterator.next();
+            fail("An exception is expected");
+        }
+        catch (NoSuchElementException e)
+        {
+            // do nothing
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/accesscontrolprovider/RuleBased.js
 
b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/accesscontrolprovider/RuleBased.js
index 8b32916..e1d7575 100644
--- 
a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/accesscontrolprovider/RuleBased.js
+++ 
b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/accesscontrolprovider/RuleBased.js
@@ -114,12 +114,24 @@ define(["dojo/_base/declare",
                                     if (value)
                                     {
                                         markup = "<div class='keyValuePair'>";
-                                        for (var key in value)
+                                        for (let key in value)
                                         {
                                             if (value.hasOwnProperty(key))
                                             {
-                                                markup += "<div>" + 
entities.encode(String(key)) + "="
-                                                          + 
entities.encode(String(value[key])) + "</div>";
+                                                const valueList = value[key];
+                                                if (Array.isArray(valueList))
+                                                {
+                                                    for (let item of valueList)
+                                                    {
+                                                        markup += "<div>" + 
entities.encode(String(key)) + "="
+                                                            + 
entities.encode(String(item)) + "</div>";
+                                                    }
+                                                }
+                                                else
+                                                {
+                                                    markup += "<div>" + 
entities.encode(String(key)) + "="
+                                                        + 
entities.encode(String(valueList)) + "</div>";
+                                                }
                                             }
                                         }
                                         markup += "</div>"
diff --git 
a/doc/java-broker/src/docbkx/security/Java-Broker-Security-AccessControlProviders.xml
 
b/doc/java-broker/src/docbkx/security/Java-Broker-Security-AccessControlProviders.xml
index 8b92dd2..e700e4a 100644
--- 
a/doc/java-broker/src/docbkx/security/Java-Broker-Security-AccessControlProviders.xml
+++ 
b/doc/java-broker/src/docbkx/security/Java-Broker-Security-AccessControlProviders.xml
@@ -130,10 +130,15 @@
        ACL rules follow this syntax:
     </para>
     <programlisting>
-     ACL {permission} {&lt;group-name&gt;|&lt;user-name&gt;|ALL} {action|ALL} 
[object|ALL] [property="&lt;property-value&gt;"]
+     ACL {permission} {&lt;group-name&gt;|&lt;user-name&gt;|ALL} {action|ALL} 
[object|ALL] [property=&lt;property-values&gt;]
     </programlisting>
 
     <para>
+      The &lt;property-values&gt; can be a single value property="single 
value" or a list of comma separated values in brackets
+      property=["value1", "value2", "value3"]. If a property repeats then it 
will be interpreted as list of values,
+      for example name="n1" name="n2" name="n3" is interpreted as name=["n1", 
"n2", "n3"].
+    </para>
+    <para>
        Comments may be introduced with the hash (#) character and are ignored. 
 Long lines can be broken with the slash (\) character.
     </para>
     <programlisting>
@@ -509,7 +514,7 @@
         The following ACL rules are given to the Broker.
       </para>
       <example>
-        <title>Worked example 2 - Simple Messaging - Broker ACL rules</title>
+        <title>Worked example 2a - Simple Messaging - Broker ACL rules</title>
         <programlisting><![CDATA[
 # This gives the operate permission to delete messages on all queues on all 
virtualhost
 ACL ALLOW operator ACCESS MANAGEMENT
@@ -517,6 +522,14 @@ ACL ALLOW operator INVOKE QUEUE 
method_name="deleteMessages"
 ACL ALLOW operator INVOKE QUEUE method_name="getMessage*"]]>
         </programlisting>
       </example>
+      <example>
+        <title>Worked example 2b - Simple Messaging - Broker ACL rules with 
multi-value property</title>
+        <programlisting><![CDATA[
+# This gives the operate permission to delete messages on all queues on all 
virtualhost
+ACL ALLOW operator ACCESS MANAGEMENT
+ACL ALLOW operator INVOKE QUEUE method_name=["deleteMessages", "getMessage*"] 
]]>
+        </programlisting>
+      </example>
       <para>
         And the following ACL rule-set is applied to the Virtualhost.  The 
default outcome of the
         Access Control Provider must be <literal>DEFERED</literal>.  This 
means that if a request for

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org
For additional commands, e-mail: commits-h...@qpid.apache.org

Reply via email to