[java-metadata-aggregator] 01/03: MDA-55 add ability to filter entity attribute values

Ian Young ian at iay.org.uk
Fri Dec 4 10:52:31 EST 2015


This is an automated email from the git hooks/post-receive script.

iay pushed a commit to branch master
in repository java-metadata-aggregator.

commit 8bf78d68d7c51f1286e8310cd7d6b04364a3115f
Author: Ian Young <ian at iay.org.uk>
AuthorDate: Fri Dec 4 08:55:31 2015 +0000

    MDA-55 add ability to filter entity attribute values
    
    Initial import from ukf-mda project, tidied up a little.
---
 .../shibboleth/metadata/dom/saml/SAMLSupport.java  |  67 ++++++
 .../mdattr/AbstractEntityAttributeMatcher.java     |  79 +++++++
 .../dom/saml/mdattr/AbstractExactValueMatcher.java |  90 ++++++++
 .../dom/saml/mdattr/EntityAttributeContext.java    |  64 ++++++
 .../saml/mdattr/EntityAttributeFilteringStage.java | 238 +++++++++++++++++++++
 .../dom/saml/mdattr/EntityCategoryMatcher.java     |  50 +++++
 .../dom/saml/mdattr/EntityCategorySupport.java     |  44 ++++
 .../saml/mdattr/EntityCategorySupportMatcher.java  |  50 +++++
 .../metadata/dom/saml/mdattr/MDAttrSupport.java    |  40 ++++
 .../dom/saml/mdattr/MultiPredicateMatcher.java     | 155 ++++++++++++++
 .../saml/mdattr/RegistrationAuthorityMatcher.java  |  58 +++++
 .../saml/mdattr/SimpleEntityAttributeContext.java  | 112 ++++++++++
 .../metadata/dom/saml/mdattr/package-info.java     |  21 ++
 .../mdattr/EntityAttributeFilteringStageTest.java  | 175 +++++++++++++++
 .../mdattr/EntityCategoryMatcherSpringTest.java    |  71 ++++++
 .../dom/saml/mdattr/EntityCategoryMatcherTest.java |  60 ++++++
 .../mdattr/EntityCategorySupportMatcherTest.java   |  60 ++++++
 .../dom/saml/mdattr/MultiPredicateMatcherTest.java |  57 +++++
 .../mdattr/RegistrationAuthorityMatcherTest.java   |  34 +++
 .../mdattr/SimpleEntityAttributeContextTest.java   |  40 ++++
 .../EntityAttributeFilteringStage-blacklist.xml    |  65 ++++++
 .../mdattr/EntityAttributeFilteringStage-input.xml |  65 ++++++
 .../EntityAttributeFilteringStage-keepcoc.xml      |  60 ++++++
 .../EntityAttributeFilteringStage-keepcoc2.xml     |  63 ++++++
 .../EntityAttributeFilteringStage-keepnone.xml     |  53 +++++
 .../EntityCategoryMatcherSpringTest-config.xml     |  20 ++
 26 files changed, 1891 insertions(+)

diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLSupport.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLSupport.java
new file mode 100644
index 0000000..609e2b4
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/SAMLSupport.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+import javax.xml.namespace.QName;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+
+/** Helper class for dealing with SAML documents. */
+ at ThreadSafe
+public final class SAMLSupport {
+
+    /** Namespace URI for SAML elements. */
+    public static final String SAML_NS = "urn:oasis:names:tc:SAML:2.0:assertion";
+    
+    /** Conventional prefix for SAML elements. */
+    public static final String SAML_PREFIX = "saml";
+    
+    /** saml:Attribute element. */
+    public static final QName ATTRIBUTE_NAME = new QName(SAML_NS, "Attribute", SAML_PREFIX);
+    
+    /** Unspecified default <code>NameFormat</code> value for <code>Attribute</code> elements. */
+    public static final String ATTRNAME_FORMAT_UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified";
+    
+    /** saml:AttributeValue element. */
+    public static final QName ATTRIBUTE_VALUE_NAME = new QName(SAML_NS, "AttributeValue", SAML_PREFIX);
+
+    /** Constructor. */
+    private SAMLSupport() {
+    }
+
+    /**
+     * Extract an <code>Attribute</code> element's <code>NameFormat</code>, applying the
+     * SAML standard's specified default if the XML attribute is not present.
+     *  
+     * @param attribute <code>Attribute</code> {@link Element}
+     * @return <code>NameFormat</code> value, or the "unspecified" default
+     */
+    @Nonnull
+    public static String extractAttributeNameFormat(@Nonnull final Element attribute) {
+        final Attr attr = attribute.getAttributeNode("NameFormat");
+        if (attr == null) {
+            return ATTRNAME_FORMAT_UNSPECIFIED;
+        } else {
+            return attr.getValue();
+        }
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractEntityAttributeMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractEntityAttributeMatcher.java
new file mode 100644
index 0000000..9ae9601
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractEntityAttributeMatcher.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.google.common.base.Predicate;
+
+/**
+ * Abstract implementation of {@link Predicate} over {@link EntityAttributeContext} using
+ * the template method pattern. The {@link #apply} method is broken down into matches
+ * against the four components of the {@link EntityAttributeContext}. All four sub-matches
+ * must succeed for the {@link Predicate} to be <code>true</code>
+ * 
+ * Where an implementation wishes to ignore a component (most commonly,
+ * {@link EntityAttributeContext#getRegistrationAuthority}) it can simply return <code>true</code>
+ * in that template method.
+ */
+ at ThreadSafe
+public abstract class AbstractEntityAttributeMatcher implements Predicate<EntityAttributeContext> {
+
+    /**
+     * Match the attribute value component of the {@link EntityAttributeContext}.
+     * 
+     * @param inputValue value component of the input context
+     * @return <code>true</code> if and only if the value component matches
+     */
+    protected abstract boolean matchAttributeValue(@Nonnull final String inputValue);
+
+    /**
+     * Match the name component of the {@link EntityAttributeContext}.
+     * 
+     * @param inputName name component of the input context
+     * @return <code>true</code> if and only if the name component matches
+     */
+    protected abstract boolean matchAttributeName(@Nonnull final String inputName);
+
+    /**
+     * Match the name format component of the {@link EntityAttributeContext}.
+     * 
+     * @param inputNameFormat name format component of the input context
+     * @return <code>true</code> if and only if the name format component matches
+     */
+    protected abstract boolean matchAttributeNameFormat(@Nonnull final String inputNameFormat);
+
+    /**
+     * Match the registration authority component of the {@link EntityAttributeContext}.
+     * 
+     * @param inputRegistrationAuthority registration authority component of the input context
+     * @return <code>true</code> if and only if the registration authority component matches
+     */
+    protected abstract boolean matchRegistrationAuthority(@Nullable final String inputRegistrationAuthority);
+
+    @Override
+    public boolean apply(@Nonnull final EntityAttributeContext input) {
+        return matchRegistrationAuthority(input.getRegistrationAuthority()) &&
+                matchAttributeNameFormat(input.getNameFormat()) &&
+                matchAttributeName(input.getName()) &&
+                matchAttributeValue(input.getValue());
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractExactValueMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractExactValueMatcher.java
new file mode 100644
index 0000000..a5c5f61
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/AbstractExactValueMatcher.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * An abstract entity attribute matcher implementation that matches an exact
+ * combination of value, name and name format. Optionally, a registration
+ * authority value may also be matched against.
+ */
+ at ThreadSafe
+public abstract class AbstractExactValueMatcher extends AbstractEntityAttributeMatcher {
+
+    /** The attribute value to match. */
+    private final String value;
+
+    /** The attribute <code>Name</code> to match. */
+    private final String name;
+    
+    /** The attribute <code>NameFormat</code> to match. */
+    private final String nameFormat;
+    
+    /** Registration authority to match against, or <code>null</code>. */
+    @Nullable
+    private final String registrationAuthority;
+
+    /**
+     * Constructor.
+     * 
+     * @param matchValue attribute value to match
+     * @param matchName attribute name to match
+     * @param matchNameFormat attribute name format to match
+     * @param matchRegAuth entity registration authority to match, or <code>null</code>
+     */
+    public AbstractExactValueMatcher(@Nonnull final String matchValue,
+            @Nonnull final String matchName, @Nonnull final String matchNameFormat,
+            @Nullable final String matchRegAuth) {
+        super();
+        value = Constraint.isNotNull(matchValue, "value may not be null");
+        name = Constraint.isNotNull(matchName, "name may not be null");
+        nameFormat = Constraint.isNotNull(matchNameFormat, "name format may not be null");
+        registrationAuthority = matchRegAuth;
+    }
+
+    @Override
+    protected boolean matchAttributeValue(@Nonnull final String inputValue) {
+        return value.equals(inputValue);
+    }
+
+    @Override
+    protected boolean matchAttributeName(@Nonnull final String inputName) {
+         return name.equals(inputName);
+    }
+
+    @Override
+    protected boolean matchAttributeNameFormat(@Nonnull final String inputNameFormat) {
+        return nameFormat.equals(inputNameFormat);
+    }
+
+    @Override
+    protected boolean matchRegistrationAuthority(@Nullable final String inputRegistrationAuthority) {
+        if (registrationAuthority == null) {
+            // ignore the context's registration authority value
+            return true;
+        } else {
+            return registrationAuthority.equals(inputRegistrationAuthority);
+        }
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeContext.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeContext.java
new file mode 100644
index 0000000..1ff96e7
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeContext.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * An entity attribute context against which matches can take place. It consists
+ * of the attribute's value, <code>Name</code> and <code>NameFormat</code> attributes,
+ * and the entity's <code>registrationAuthority</code>, if any.
+ * 
+ * A matcher is a {@link com.google.common.base.Predicate} over such a context.
+ */
+public interface EntityAttributeContext {
+
+    /**
+     * Returns the registration authority component, or <code>null</code>.
+     * 
+     * @return the registration authority, or <code>null</code>
+     */
+    @Nullable
+    String getRegistrationAuthority();
+    
+    /**
+     * Returns the attribute's <code>NameFormat</code>.
+     * 
+     * @return the attribute's <code>NameFormat</code>.
+     */
+    @Nonnull
+    String getNameFormat();
+    
+    /**
+     * Returns the attribute's <code>Name</code>.
+     * 
+     * @return the attribute's <code>Name</code>
+     */
+    @Nonnull
+    String getName();
+    
+    /**
+     * Returns the attribute's value.
+     * 
+     * @return the attribute's value
+     */
+    @Nonnull
+    String getValue();
+    
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage.java
new file mode 100644
index 0000000..978a2b2
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.dom.saml.SAMLMetadataSupport;
+import net.shibboleth.metadata.dom.saml.SAMLSupport;
+import net.shibboleth.metadata.dom.saml.mdrpi.RegistrationAuthority;
+import net.shibboleth.metadata.pipeline.BaseStage;
+import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.xml.ElementSupport;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import com.google.common.base.Predicate;
+
+/**
+ * A stage which filters entity attributes from entity definitions according to a supplied
+ * set of rules.
+ * 
+ * For each attribute value under consideration, a {@link EntityAttributeContext} is built
+ * from the components of the attribute and the entity's <code>registrationAuthority</code>,
+ * if any.
+ * 
+ * Note that the <code>registrationAuthority</code> to be used is assumed to have been
+ * extracted out into a {@link RegistrationAuthority} object in the entity's item metadata.
+ * 
+ * The stage can be operated in a whitelisting mode (the default) or in a blacklisting mode
+ * by setting the <code>whitelisting</code> property to <code>false</code>.
+ */
+public class EntityAttributeFilteringStage extends BaseStage<Element> {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(EntityAttributeFilteringStage.class);
+
+    /**
+     * List of matching rules to apply to each attribute value. The list is applied in
+     * order, with the first rule returning <code>true</code> terminating the evaluation.
+     * This amounts to an implicit ORing of the individual rules, with early
+     * termination.
+     */
+    private List<Predicate<EntityAttributeContext>> rules = Collections.emptyList();
+
+    /** Mode of operation: whitelisting or blacklisting. Default: whitelisting. */
+    private boolean whitelisting = true;
+    
+    /**
+     * Sets the {@link List} of rules to be used to match attribute values.
+     * 
+     * @param newRules new {@link List} of rules
+     */
+    public void setRules(@Nonnull final List<Predicate<EntityAttributeContext>> newRules) {
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        rules = Constraint.isNotNull(newRules, "rules property may not be null");
+    }
+    
+    /**
+     * Returns the {@link List} of rules being used to match entity attributes.
+     * 
+     * @return the {@link List} of rules
+     */
+    @Nonnull
+    public List<Predicate<EntityAttributeContext>> getRules() {
+        return Collections.unmodifiableList(rules);
+    }
+    
+    /**
+     * Sets the mode of operation.
+     * 
+     * @param newValue <code>true</code> to whitelist (default),
+     *                 <code>false</code> to blacklist
+     */
+    public void setWhitelisting(final boolean newValue) {
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        whitelisting = newValue;
+    }
+    
+    /**
+     * Indicates whether the stage is set to whitelisting or blacklisting mode.
+     * 
+     * @return <code>true</code> if whitelisting (default),
+     *         <code>false</code> if blacklisting
+     */
+    public boolean isWhitelisting() {
+        return whitelisting;
+    }
+    
+    /**
+     * Apply the rules to a context.
+     * 
+     * @param ctx the context to apply the rules to
+     * @return <code>true</code> if one of the rules returns <code>true</code>;
+     *  otherwise <code>false</code>
+     */
+    private boolean applyRules(final EntityAttributeContext ctx) {
+        for (final Predicate<EntityAttributeContext> rule : rules) {
+            if (rule.apply(ctx)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Extract the registration authority for an entity from its entity metadata.
+     * 
+     * @param item the {@link Item} representing the entity
+     * @return the registration authority URI, or <code>null</code> if not present
+     */
+    private String extractRegistrationAuthority(@Nonnull final Item<Element> item) {
+        final List<RegistrationAuthority> regAuthList = item.getItemMetadata().get(RegistrationAuthority.class);
+        if (regAuthList.isEmpty()) {
+            return null;
+        } else {
+            return regAuthList.get(0).getRegistrationAuthority();
+        }
+    }
+    
+    /**
+     * Filter an <code>Attribute</code> element.
+     * 
+     * @param attribute an <code>Attribute</code> element to filter
+     * @param registrationAuthority the registration authority associated with the entity
+     */
+    private void filterAttribute(@Nonnull final Element attribute, @Nullable final String registrationAuthority) {
+        // Determine the attribute's name; this will default to the empty string if not present
+        final String attributeName = attribute.getAttribute("Name");
+        
+        // Determine the attribute's NameFormat
+        final String attributeNameFormat = SAMLSupport.extractAttributeNameFormat(attribute);
+        
+        // Locate the AttributeValue elements to filter
+        final List<Element> attributeValues =
+                ElementSupport.getChildElements(attribute, SAMLSupport.ATTRIBUTE_VALUE_NAME);
+        
+        // Filter each AttributeValue in turn
+        for (final Element value : attributeValues) {
+            final String attributeValue = value.getTextContent();
+
+            // Construct an entity attribute context to be matched against
+            final EntityAttributeContext ctx =
+                    new SimpleEntityAttributeContext(attributeValue, attributeName,
+                            attributeNameFormat, registrationAuthority);            
+            final boolean matched = applyRules(ctx);
+            if (matched ^ whitelisting) {
+                log.debug("removing {}", ctx);
+                attribute.removeChild(value);
+            }
+        }
+    }
+    
+    /**
+     * Filter an <code>EntityAttributes</code> extension element.
+     * 
+     * @param entityAttributes the <code>EntityAttributes</code> extension element
+     * @param registrationAuthority the registration authority associated with the entity
+     */
+    private void filterEntityAttributes(@Nonnull final Element entityAttributes,
+            @Nullable final String registrationAuthority) {
+        // Locate the Attribute elements to filter
+        final List<Element> attributes =
+                ElementSupport.getChildElements(entityAttributes, SAMLSupport.ATTRIBUTE_NAME);
+        
+        // Filter each Attribute in turn
+        for (final Element attribute : attributes) {
+            filterAttribute(attribute, registrationAuthority);
+            
+            // remove the Attribute container if it is now empty
+            if (ElementSupport.getFirstChildElement(attribute) == null) {
+                log.debug("removing empty Attribute");
+                entityAttributes.removeChild(attribute);
+            }
+        }
+    }
+    
+    @Override
+    protected void doExecute(final Collection<Item<Element>> itemCollection) throws StageProcessingException {
+        for (final Item<Element> item : itemCollection) {
+            final Element entity = item.unwrap();
+            
+            // Establish the item's registrationAuthority, if any
+            final String registrationAuthority = extractRegistrationAuthority(item);
+
+            // Locate mdattr:EntityAttributes element
+            final Element entityAttributes = SAMLMetadataSupport.getDescriptorExtensions(entity,
+                    MDAttrSupport.ENTITY_ATTRIBUTES_NAME);
+            if (entityAttributes != null) {
+                filterEntityAttributes(entityAttributes, registrationAuthority);
+                
+                // remove the EntityAttributes container if it is now empty
+                if (ElementSupport.getFirstChildElement(entityAttributes) == null) {
+                    log.debug("removing empty EntityAttributes");
+                    final Node extensions = entityAttributes.getParentNode();
+                    extensions.removeChild(entityAttributes);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void doDestroy() {
+        rules = null;
+
+        super.doDestroy();
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcher.java
new file mode 100644
index 0000000..a5d13c6
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcher.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An entity attribute matcher which matches a given entity category.
+ */
+ at ThreadSafe
+public class EntityCategoryMatcher extends AbstractExactValueMatcher {
+    
+    /**
+     * Constructor.
+     * 
+     * @param category entity category to match
+     * @param regAuth registration authority to match, or <code>null</code>
+     */
+    public EntityCategoryMatcher(@Nonnull final String category, @Nullable final String regAuth) {
+        super(category, EntityCategorySupport.EC_CATEGORY_ATTR_NAME,
+                EntityCategorySupport.EC_ATTR_NAME_FORMAT, regAuth);
+    }
+    
+    /**
+     * Constructor.
+     * 
+     * @param category entity category to match
+     */
+    public EntityCategoryMatcher(@Nonnull final String category) {
+        this(category, null);
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupport.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupport.java
new file mode 100644
index 0000000..886d607
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupport.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Helper class for dealing with entity categories.
+ * 
+ * @see <a href="https://datatracker.ietf.org/doc/draft-young-entity-category/">
+ * The Entity Category SAML Attribute Types</a>
+ */
+ at ThreadSafe
+public final class EntityCategorySupport {
+
+    /** The attribute <code>NameFormat</code> for all entity category attributes. */
+    public static final String EC_ATTR_NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri";
+    
+    /** The attribute <code>Name</code> for the Entity Category Attribute. */
+    public static final String EC_CATEGORY_ATTR_NAME = "http://macedir.org/entity-category";
+    
+    /** The attribute <code>Name</code> for the Entity Category Support Attribute. */
+    public static final String EC_SUPPORT_ATTR_NAME = "http://macedir.org/entity-category-support";
+
+    /** Constructor. */
+    private EntityCategorySupport() {
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupportMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupportMatcher.java
new file mode 100644
index 0000000..7be1ada
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupportMatcher.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An entity attribute matcher which matches a given entity support category.
+ */
+ at ThreadSafe
+public class EntityCategorySupportMatcher extends AbstractExactValueMatcher {
+    
+    /**
+     * Constructor.
+     * 
+     * @param category entity category to match
+     * @param regAuth registration authority to match, or <code>null</code>
+     */
+    public EntityCategorySupportMatcher(@Nonnull final String category, @Nullable final String regAuth) {
+        super(category, EntityCategorySupport.EC_SUPPORT_ATTR_NAME,
+                EntityCategorySupport.EC_ATTR_NAME_FORMAT, regAuth);
+    }
+    
+    /**
+     * Constructor.
+     * 
+     * @param category entity category to match
+     */
+    public EntityCategorySupportMatcher(@Nonnull final String category) {
+        this(category, null);
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/MDAttrSupport.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/MDAttrSupport.java
new file mode 100644
index 0000000..1f456cf
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/MDAttrSupport.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import javax.annotation.concurrent.ThreadSafe;
+import javax.xml.namespace.QName;
+
+/** Helper class for dealing with MDAttr metadata. */
+ at ThreadSafe
+public final class MDAttrSupport {
+
+    /** MDAttr namespace. */
+    public static final String MDATTR_NS = "urn:oasis:names:tc:SAML:metadata:attribute";
+    
+    /** MDAttr conventional prefix. */
+    public static final String MDATTR_PREFIX = "mdattr";
+
+    /** mdattr:EntityAttributes element. */
+    public static final QName ENTITY_ATTRIBUTES_NAME = new QName(MDATTR_NS, "EntityAttributes", MDATTR_PREFIX);
+
+    /** Constructor. */
+    private MDAttrSupport() {
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/MultiPredicateMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/MultiPredicateMatcher.java
new file mode 100644
index 0000000..42e0073
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/MultiPredicateMatcher.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+
+/**
+ * An entity attribute matcher implementation that delegates each
+ * component match to a different @{link Predicate}. Each such
+ * {@link Predicate} defaults to {@link Predicates#alwaysTrue} so that
+ * in most cases a minimum number of properties need to be set.
+ * 
+ * The individual {@link Predicate}s operate over {@link CharSequence}
+ * rather than {@link String} both for generality and to allow the use
+ * of instances produced by {@link Predicates#containsPattern}.
+ */
+ at ThreadSafe
+public class MultiPredicateMatcher extends AbstractEntityAttributeMatcher {
+
+    /** {@link Predicate} to use to match the context's attribute value. */
+    @Nonnull
+    private Predicate<CharSequence> valuePredicate = Predicates.alwaysTrue();
+    
+    /** {@link Predicate} to use to match the context's attribute name. */
+    @Nonnull
+    private Predicate<CharSequence> namePredicate = Predicates.alwaysTrue();
+    
+    /** {@link Predicate} to use to match the context's attribute name format. */
+    @Nonnull
+    private Predicate<CharSequence> nameFormatPredicate = Predicates.alwaysTrue();
+    
+    /** {@link Predicate} to use to match the context's registration authority. */
+    @Nonnull
+    private Predicate<CharSequence> registrationAuthorityPredicate = Predicates.alwaysTrue();
+    
+    /**
+     * Gets the {@link Predicate} being used to match the context's attribute value.
+     * 
+     * @return the {@link Predicate} being used to match the context's attribute value
+     */
+    @Nonnull
+    public Predicate<CharSequence> getValuePredicate() {
+        return valuePredicate;
+    }
+    
+    /**
+     * Sets the {@link Predicate} to use to match the context's attribute value.
+     * 
+     * @param predicate new {@link Predicate} to use to match the context's attribute value
+     */
+    public void setValuePredicate(@Nonnull final Predicate<CharSequence> predicate) {
+        valuePredicate = Constraint.isNotNull(predicate, "value predicate may not be null");
+    }
+    
+    /**
+     * Gets the {@link Predicate} being used to match the context's attribute name.
+     * 
+     * @return the {@link Predicate} being used to match the context's attribute name
+     */
+    @Nonnull
+    public Predicate<CharSequence> getNamePredicate() {
+        return namePredicate;
+    }
+    
+    /**
+     * Sets the {@link Predicate} to use to match the context's attribute name.
+     * 
+     * @param predicate new {@link Predicate} to use to match the context's attribute name
+     */
+    public void setNamePredicate(@Nonnull final Predicate<CharSequence> predicate) {
+        namePredicate = Constraint.isNotNull(predicate, "name predicate may not be null");
+    }
+    
+    /**
+     * Gets the {@link Predicate} being used to match the context's attribute name format.
+     * 
+     * @return the {@link Predicate} being used to match the context's attribute name format
+     */
+    @Nonnull
+    public Predicate<CharSequence> getNameFormatPredicate() {
+        return nameFormatPredicate;
+    }
+    
+    /**
+     * Sets the {@link Predicate} to use to match the context's attribute name format.
+     * 
+     * @param predicate new {@link Predicate} to use to match the context's attribute name format
+     */
+    public void setNameFormatPredicate(@Nonnull final Predicate<CharSequence> predicate) {
+        nameFormatPredicate = Constraint.isNotNull(predicate, "name format predicate may not be null");
+    }
+    
+    /**
+     * Gets the {@link Predicate} being used to match the context's registration authority.
+     * 
+     * @return the {@link Predicate} being used to match the context's registration authority
+     */
+    @Nonnull
+    public Predicate<CharSequence> getRegistrationAuthorityPredicate() {
+        return valuePredicate;
+    }
+    
+    /**
+     * Sets the {@link Predicate} to use to match the context's registration authority.
+     * 
+     * @param predicate new {@link Predicate} to use to match the context's registration authority
+     */
+    public void setRegistrationAuthorityPredicate(@Nonnull final Predicate<CharSequence> predicate) {
+        registrationAuthorityPredicate = Constraint.isNotNull(predicate,
+                "registration authority predicate may not be null");
+    }
+    
+    @Override
+    protected boolean matchAttributeValue(@Nonnull final String inputValue) {
+        return valuePredicate.apply(inputValue);
+    }
+
+    @Override
+    protected boolean matchAttributeName(@Nonnull final String inputName) {
+         return namePredicate.apply(inputName);
+    }
+
+    @Override
+    protected boolean matchAttributeNameFormat(@Nonnull final String inputNameFormat) {
+        return nameFormatPredicate.apply(inputNameFormat);
+    }
+
+    @Override
+    protected boolean matchRegistrationAuthority(@Nullable final String inputRegistrationAuthority) {
+        return registrationAuthorityPredicate.apply(inputRegistrationAuthority);
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/RegistrationAuthorityMatcher.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/RegistrationAuthorityMatcher.java
new file mode 100644
index 0000000..dd19680
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/RegistrationAuthorityMatcher.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.google.common.base.Predicate;
+
+/**
+ * An entity attribute matcher which matches a given registration authority.
+ * 
+ * It can match against a specific registration authority, or against the absence of
+ * one.
+ */
+ at ThreadSafe
+public class RegistrationAuthorityMatcher implements Predicate<EntityAttributeContext> {
+    
+    /** Registration authority to match against. */
+    @Nullable
+    private final String registrationAuthority;
+    
+    /**
+     * Constructor.
+     * 
+     * @param regAuth registration authority to match, or <code>null</code>
+     */
+    public RegistrationAuthorityMatcher(@Nullable final String regAuth) {
+        registrationAuthority = regAuth;
+    }
+
+    @Override
+    public boolean apply(@Nonnull final EntityAttributeContext input) {
+        if (registrationAuthority == null) {
+            // match entities *without* a registration authority
+            return null == input.getRegistrationAuthority();
+        } else {
+            return registrationAuthority.equals(input.getRegistrationAuthority());
+        }
+    }
+
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/SimpleEntityAttributeContext.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/SimpleEntityAttributeContext.java
new file mode 100644
index 0000000..545f56d
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/SimpleEntityAttributeContext.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.metadata.dom.saml.mdattr;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * A simple immutable implementation of {@link EntityAttributeContext}.
+ */
+public class SimpleEntityAttributeContext implements EntityAttributeContext {
+
+    /** The attribute's value. */
+    @Nonnull
+    private final String value;
+    
+    /** The attribute's <code>Name</code>. */
+    @Nonnull
+    private final String name;
+    
+    /** The attribute's <code>NameFormat</code>. */
+    @Nonnull
+    private final String nameFormat;
+    
+    /** The entity's registration authority, or <code>null</code>. */
+    @Nullable
+    private final String registrationAuthority;
+    
+    /**
+     * Constructor.
+     * 
+     * @param attributeValue attribute value
+     * @param attributeName attribute <code>Name</code>
+     * @param attributeNameFormat attribute <code>NameFormat</code>
+     * @param registrar entity's registration authority, or <code>null</code>
+     */
+    public SimpleEntityAttributeContext(@Nonnull final String attributeValue,
+            @Nonnull final String attributeName,
+            @Nonnull final String attributeNameFormat,
+            @Nullable final String registrar) {
+        value = Constraint.isNotNull(attributeValue, "value may not be null");
+        name = Constraint.isNotNull(attributeName, "name may not be null");
+        nameFormat = Constraint.isNotNull(attributeNameFormat, "name format may not be null");
+        registrationAuthority = registrar;
+    }
+    
+    /**
+     * Shorthand three-argument constructor.
+     * 
+     * @param attributeValue attribute value
+     * @param attributeName attribute <code>Name</code>
+     * @param attributeNameFormat attribute <code>NameFormat</code>
+     */
+    public SimpleEntityAttributeContext(@Nonnull final String attributeValue,
+            @Nonnull final String attributeName,
+            @Nonnull final String attributeNameFormat) {
+        this(attributeValue, attributeName, attributeNameFormat, null);
+    }    
+
+    @Override
+    public String getRegistrationAuthority() {
+        return registrationAuthority;
+    }
+
+    @Override
+    public String getNameFormat() {
+        return nameFormat;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder b = new StringBuilder();
+        b.append("{v=").append(getValue());
+        b.append(", n=").append(getName());
+        b.append(", f=").append(getNameFormat());
+        b.append(", r=");
+        if (getRegistrationAuthority() == null) {
+            b.append("(none)");
+        } else {
+            b.append(getRegistrationAuthority());
+        }
+        b.append('}');
+        return b.toString();
+    }
+}
diff --git a/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/package-info.java b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/package-info.java
new file mode 100644
index 0000000..4015b89
--- /dev/null
+++ b/aggregator-pipeline/src/main/java/net/shibboleth/metadata/dom/saml/mdattr/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development, 
+ * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.
+ */
+
+/**
+ * Aggregator classes for the entity attributes specification.
+ */
+package net.shibboleth.metadata.dom.saml.mdattr;
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStageTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStageTest.java
new file mode 100644
index 0000000..6a71d6e
--- /dev/null
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStageTest.java
@@ -0,0 +1,175 @@
+
+package net.shibboleth.metadata.dom.saml.mdattr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.dom.BaseDOMTest;
+import net.shibboleth.metadata.dom.DOMElementItem;
+import net.shibboleth.metadata.dom.saml.mdrpi.RegistrationAuthorityPopulationStage;
+
+import org.testng.annotations.Test;
+import org.w3c.dom.Element;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+
+public class EntityAttributeFilteringStageTest extends BaseDOMTest {
+
+    protected EntityAttributeFilteringStageTest() {
+        super(EntityAttributeFilteringStage.class);
+    }
+    
+    private Element makeInputDocument() throws Exception {
+        return readXMLData("input.xml");
+    }
+    
+    private List<Item<Element>> makeItems(final Element inputElement) throws Exception {
+        final Item<Element> item = new DOMElementItem(inputElement);
+        final List<Item<Element>> items = new ArrayList<>();
+        items.add(item);
+        
+        // Extract the registration authority
+        final RegistrationAuthorityPopulationStage ras = new RegistrationAuthorityPopulationStage();
+        ras.setId("id");
+        ras.initialize();
+        ras.execute(items);
+        ras.destroy();
+
+        return items;
+    }
+
+    private List<Item<Element>> makeInputItems() throws Exception {
+        return makeItems(makeInputDocument());
+    }
+    
+    @Test
+    public void testNoPredicates() throws Exception {
+        final List<Item<Element>> items = makeInputItems();
+        
+        final EntityAttributeFilteringStage stage = new EntityAttributeFilteringStage();
+        stage.setId("id");
+        stage.initialize();
+        stage.execute(items);
+        stage.destroy();
+        
+        final Element result = items.get(0).unwrap();
+        final Element expected = readXMLData("keepnone.xml");
+        assertXMLIdentical(expected, result);
+    }
+    
+    @Test
+    public void testKeepCoC() throws Exception {
+        final List<Item<Element>> items = makeInputItems();
+        final List<Predicate<EntityAttributeContext>> rules = new ArrayList<>();
+        rules.add(new EntityCategoryMatcher("http://www.geant.net/uri/dataprotection-code-of-conduct/v1"));
+        
+        final EntityAttributeFilteringStage stage = new EntityAttributeFilteringStage();
+        stage.setId("id");
+        stage.setRules(rules);
+        stage.initialize();
+        stage.execute(items);
+        stage.destroy();
+        
+        final Element result = items.get(0).unwrap();
+        final Element expected = readXMLData("keepcoc.xml");
+        assertXMLIdentical(expected, result);
+    }
+
+    @Test
+    public void testKeepCoCRightRegistrar() throws Exception {
+        final List<Item<Element>> items = makeInputItems();
+        final List<Predicate<EntityAttributeContext>> rules = new ArrayList<>();
+        rules.add(new EntityCategoryMatcher("http://www.geant.net/uri/dataprotection-code-of-conduct/v1",
+                "http://ukfederation.org.uk"));
+        
+        final EntityAttributeFilteringStage stage = new EntityAttributeFilteringStage();
+        stage.setId("id");
+        stage.setRules(rules);
+        stage.initialize();
+        stage.execute(items);
+        stage.destroy();
+        
+        final Element result = items.get(0).unwrap();
+        final Element expected = readXMLData("keepcoc.xml");
+        assertXMLIdentical(expected, result);
+    }
+
+    @Test
+    public void testKeepCoCWrongRegistrar() throws Exception {
+        final List<Item<Element>> items = makeInputItems();
+        final List<Predicate<EntityAttributeContext>> rules = new ArrayList<>();
+        rules.add(new EntityCategoryMatcher("http://www.geant.net/uri/dataprotection-code-of-conduct/v1",
+                "http://not.ukfederation.org.uk"));
+        
+        final EntityAttributeFilteringStage stage = new EntityAttributeFilteringStage();
+        stage.setId("id");
+        stage.setRules(rules);
+        stage.initialize();
+        stage.execute(items);
+        stage.destroy();
+        
+        final Element result = items.get(0).unwrap();
+        final Element expected = readXMLData("keepnone.xml");
+        assertXMLIdentical(expected, result);
+    }
+
+    @Test
+    public void testKeepCoC2() throws Exception {
+        final List<Item<Element>> items = makeInputItems();
+        final List<Predicate<EntityAttributeContext>> rules = new ArrayList<>();
+        rules.add(new EntityCategoryMatcher("http://www.geant.net/uri/dataprotection-code-of-conduct/v1"));
+        rules.add(new EntityCategorySupportMatcher("http://www.geant.net/uri/dataprotection-code-of-conduct/v1"));
+        
+        final EntityAttributeFilteringStage stage = new EntityAttributeFilteringStage();
+        stage.setId("id");
+        stage.setRules(rules);
+        stage.initialize();
+        stage.execute(items);
+        stage.destroy();
+        
+        final Element result = items.get(0).unwrap();
+        final Element expected = readXMLData("keepcoc2.xml");
+        assertXMLIdentical(expected, result);
+    }
+
+    @Test
+    public void testKeepEverything() throws Exception {
+        final List<Item<Element>> items = makeInputItems();
+        final List<Predicate<EntityAttributeContext>> rules = new ArrayList<>();
+        rules.add(Predicates.<EntityAttributeContext>alwaysTrue());
+        
+        final EntityAttributeFilteringStage stage = new EntityAttributeFilteringStage();
+        stage.setId("id");
+        stage.setRules(rules);
+        stage.initialize();
+        stage.execute(items);
+        stage.destroy();
+        
+        final Element result = items.get(0).unwrap();
+        final Element expected = readXMLData("input.xml");
+        assertXMLIdentical(expected, result);
+    }
+
+    @Test
+    public void testBlacklist() throws Exception {
+        final List<Item<Element>> items = makeInputItems();
+        final List<Predicate<EntityAttributeContext>> rules = new ArrayList<>();
+        rules.add(new EntityCategoryMatcher("http://www.geant.net/uri/dataprotection-code-of-conduct/v1",
+                "http://ukfederation.org.uk"));
+        
+        final EntityAttributeFilteringStage stage = new EntityAttributeFilteringStage();
+        stage.setId("id");
+        stage.setWhitelisting(false);
+        stage.setRules(rules);
+        stage.initialize();
+        stage.execute(items);
+        stage.destroy();
+        
+        final Element result = items.get(0).unwrap();
+        final Element expected = readXMLData("blacklist.xml");
+        assertXMLIdentical(expected, result);
+    }
+
+}
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcherSpringTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcherSpringTest.java
new file mode 100644
index 0000000..4b6fb9e
--- /dev/null
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcherSpringTest.java
@@ -0,0 +1,71 @@
+
+package net.shibboleth.metadata.dom.saml.mdattr;
+
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+
+/**
+ * This is the same as {@link EntityCategoryMatcherTest}, but pulls the matcher beans
+ * from a configured application context. This is just to make sure that Spring can
+ * distinguish between the two constructor signatures propertly, so we don't need an
+ * equivalent for every class under test.
+ */
+ at ContextConfiguration("EntityCategoryMatcherSpringTest-config.xml")
+public class EntityCategoryMatcherSpringTest extends AbstractTestNGSpringContextTests {
+
+    private void test(final boolean expected, final Predicate<EntityAttributeContext> matcher,
+            final EntityAttributeContext context) {
+        Assert.assertEquals(matcher.apply(context), expected, context.toString());
+    }
+    
+    @Test
+    public void testNoRA() {
+        final Predicate<EntityAttributeContext> matcher =
+                (Predicate<EntityAttributeContext>)applicationContext.getBean("categoryMatcherNoRA");
+        
+        // all four components match
+        test(true, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "whatever"));
+
+        // context has no RA
+        test(true, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+
+        // these matches should fail because one component differs
+        test(false, matcher, new SimpleEntityAttributeContext("category2", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified", null));
+    }
+
+    @Test
+    public void testWithRA() {
+        final Predicate<EntityAttributeContext> matcher =
+                (Predicate<EntityAttributeContext>)applicationContext.getBean("categoryMatcherWithRA");
+        
+        // all four components match
+        test(true, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar"));
+
+        // context has no RA
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+
+        // these matches should fail because one component differs
+        test(false, matcher, new SimpleEntityAttributeContext("category2", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar2"));
+    }
+    
+}
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcherTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcherTest.java
new file mode 100644
index 0000000..e55dba0
--- /dev/null
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcherTest.java
@@ -0,0 +1,60 @@
+
+package net.shibboleth.metadata.dom.saml.mdattr;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+
+public class EntityCategoryMatcherTest {
+
+    private void test(final boolean expected, final Predicate<EntityAttributeContext> matcher,
+            final EntityAttributeContext context) {
+        Assert.assertEquals(matcher.apply(context), expected, context.toString());
+    }
+    
+    @Test
+    public void testNoRA() {
+        final Predicate<EntityAttributeContext> matcher = new EntityCategoryMatcher("category");
+        
+        // all four components match
+        test(true, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "whatever"));
+
+        // context has no RA
+        test(true, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+
+        // these matches should fail because one component differs
+        test(false, matcher, new SimpleEntityAttributeContext("category2", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified", null));
+    }
+
+    @Test
+    public void testWithRA() {
+        final Predicate<EntityAttributeContext> matcher = new EntityCategoryMatcher("category", "registrar");
+        
+        // all four components match
+        test(true, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar"));
+
+        // context has no RA
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+
+        // these matches should fail because one component differs
+        test(false, matcher, new SimpleEntityAttributeContext("category2", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar2"));
+    }
+
+}
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupportMatcherTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupportMatcherTest.java
new file mode 100644
index 0000000..e89e5db
--- /dev/null
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/EntityCategorySupportMatcherTest.java
@@ -0,0 +1,60 @@
+
+package net.shibboleth.metadata.dom.saml.mdattr;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+
+public class EntityCategorySupportMatcherTest {
+
+    private void test(final boolean expected, final Predicate<EntityAttributeContext> matcher,
+            final EntityAttributeContext context) {
+        Assert.assertEquals(matcher.apply(context), expected, context.toString());
+    }
+    
+    @Test
+    public void testNoRA() {
+        final Predicate<EntityAttributeContext> matcher = new EntityCategorySupportMatcher("category");
+        
+        // all four components match
+        test(true, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "whatever"));
+
+        // context has no RA
+        test(true, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+
+        // these matches should fail because one component differs
+        test(false, matcher, new SimpleEntityAttributeContext("category2", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified", null));
+    }
+
+    @Test
+    public void testWithRA() {
+        final Predicate<EntityAttributeContext> matcher = new EntityCategorySupportMatcher("category", "registrar");
+        
+        // all four components match
+        test(true, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar"));
+
+        // context has no RA
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", null));
+
+        // these matches should fail because one component differs
+        test(false, matcher, new SimpleEntityAttributeContext("category2", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("category", "http://macedir.org/entity-category-support",
+                "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "registrar2"));
+    }
+
+}
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/MultiPredicateMatcherTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/MultiPredicateMatcherTest.java
new file mode 100644
index 0000000..6e0d72d
--- /dev/null
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/MultiPredicateMatcherTest.java
@@ -0,0 +1,57 @@
+
+package net.shibboleth.metadata.dom.saml.mdattr;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+
+public class MultiPredicateMatcherTest {
+
+    private final EntityAttributeContext context =
+            new SimpleEntityAttributeContext("valuevalue", "namename", "fmtfmt", "regreg");
+
+    @Test
+    public void testNothing() {
+        final Predicate<EntityAttributeContext> matcher = new MultiPredicateMatcher();
+        Assert.assertTrue(matcher.apply(context));
+    }
+    
+    @Test
+    public void setNameFormatPredicate() {
+        final MultiPredicateMatcher matcher = new MultiPredicateMatcher();
+        matcher.setNameFormatPredicate(Predicates.containsPattern("tfm"));
+        Assert.assertTrue(matcher.apply(context));
+        matcher.setNameFormatPredicate(Predicates.containsPattern("xxx"));
+        Assert.assertFalse(matcher.apply(context));
+    }
+
+    @Test
+    public void setNamePredicate() {
+        final MultiPredicateMatcher matcher = new MultiPredicateMatcher();
+        matcher.setNamePredicate(Predicates.containsPattern("ena"));
+        Assert.assertTrue(matcher.apply(context));
+        matcher.setNamePredicate(Predicates.containsPattern("xxx"));
+        Assert.assertFalse(matcher.apply(context));
+    }
+
+    @Test
+    public void setRegistrationAuthorityPredicate() {
+        final MultiPredicateMatcher matcher = new MultiPredicateMatcher();
+        matcher.setRegistrationAuthorityPredicate(Predicates.containsPattern("egr"));
+        Assert.assertTrue(matcher.apply(context));
+        matcher.setRegistrationAuthorityPredicate(Predicates.containsPattern("xxx"));
+        Assert.assertFalse(matcher.apply(context));
+    }
+
+    @Test
+    public void setValuePredicate() {
+        final MultiPredicateMatcher matcher = new MultiPredicateMatcher();
+        matcher.setValuePredicate(Predicates.containsPattern("eva"));
+        Assert.assertTrue(matcher.apply(context));
+        matcher.setValuePredicate(Predicates.containsPattern("xxx"));
+        Assert.assertFalse(matcher.apply(context));
+    }
+
+}
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/RegistrationAuthorityMatcherTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/RegistrationAuthorityMatcherTest.java
new file mode 100644
index 0000000..f4abeee
--- /dev/null
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/RegistrationAuthorityMatcherTest.java
@@ -0,0 +1,34 @@
+
+package net.shibboleth.metadata.dom.saml.mdattr;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+
+public class RegistrationAuthorityMatcherTest {
+
+    private void test(final boolean expected, final Predicate<EntityAttributeContext> matcher,
+            final EntityAttributeContext context) {
+        Assert.assertEquals(matcher.apply(context), expected, context.toString());
+    }
+    
+    @Test
+    public void testWithRA() {
+        final Predicate<EntityAttributeContext> matcher = new RegistrationAuthorityMatcher("registrar");
+        
+        test(true, matcher, new SimpleEntityAttributeContext("a", "b", "c", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("a", "b", "c", "registrar2"));
+        test(false, matcher, new SimpleEntityAttributeContext("a", "b", "c", null));
+    }
+
+    @Test
+    public void testNoRA() {
+        final Predicate<EntityAttributeContext> matcher = new RegistrationAuthorityMatcher(null);
+        
+        test(false, matcher, new SimpleEntityAttributeContext("a", "b", "c", "registrar"));
+        test(false, matcher, new SimpleEntityAttributeContext("a", "b", "c", "registrar2"));
+        test(true, matcher, new SimpleEntityAttributeContext("a", "b", "c", null));
+    }
+
+}
diff --git a/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/SimpleEntityAttributeContextTest.java b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/SimpleEntityAttributeContextTest.java
new file mode 100644
index 0000000..e4fe919
--- /dev/null
+++ b/aggregator-pipeline/src/test/java/net/shibboleth/metadata/dom/saml/mdattr/SimpleEntityAttributeContextTest.java
@@ -0,0 +1,40 @@
+
+package net.shibboleth.metadata.dom.saml.mdattr;
+
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class SimpleEntityAttributeContextTest {
+
+    @Test
+    public void testFour() {
+        final EntityAttributeContext ctx = new SimpleEntityAttributeContext("a", "b", "c", "d");
+        Assert.assertEquals(ctx.getValue(), "a");
+        Assert.assertEquals(ctx.getName(), "b");
+        Assert.assertEquals(ctx.getNameFormat(), "c");
+        Assert.assertEquals(ctx.getRegistrationAuthority(), "d");
+    }
+    
+    @Test
+    public void testThree() {
+        final EntityAttributeContext ctx = new SimpleEntityAttributeContext("a", "b", "c");
+        Assert.assertEquals(ctx.getValue(), "a");
+        Assert.assertEquals(ctx.getName(), "b");
+        Assert.assertEquals(ctx.getNameFormat(), "c");
+        Assert.assertNull(ctx.getRegistrationAuthority());
+    }
+
+    @Test
+    public void stringFour() {
+        final EntityAttributeContext ctx = new SimpleEntityAttributeContext("a", "b", "c", "d");
+        Assert.assertEquals(ctx.toString(), "{v=a, n=b, f=c, r=d}");
+    }
+
+    @Test
+    public void stringThree() {
+        final EntityAttributeContext ctx = new SimpleEntityAttributeContext("a", "b", "c");
+        Assert.assertEquals(ctx.toString(), "{v=a, n=b, f=c, r=(none)}");
+    }
+    
+}
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-blacklist.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-blacklist.xml
new file mode 100644
index 0000000..0e65371
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-blacklist.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    entityID="http://example.org/entity">
+    <Extensions>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2014-03-18T15:23:31Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes>
+            <saml:Attribute Name="http://macedir.org/entity-category" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                
+                <saml:AttributeValue>http://example.org/category2</saml:AttributeValue>
+            </saml:Attribute>
+            <saml:Attribute Name="http://macedir.org/entity-category-support" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://example.org/category2support</saml:AttributeValue>
+                <saml:AttributeValue>http://www.geant.net/uri/dataprotection-code-of-conduct/v1</saml:AttributeValue>
+            </saml:Attribute>
+            <saml:Attribute Name="anotherAttributeName" NameFormat="anotherNameFormat">
+                <saml:AttributeValue>anotherValue</saml:AttributeValue>
+            </saml:Attribute>
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIEfzCCA2egAwIBAgIQQSSnV5Mk/EXZxgrsbnU7ajANBgkqhkiG9w0BAQUFADA2
+                        MQswCQYDVQQGEwJOTDEPMA0GA1UEChMGVEVSRU5BMRYwFAYDVQQDEw1URVJFTkEg
+                        U1NMIENBMB4XDTEzMDMwNjAwMDAwMFoXDTE2MDMwNTIzNTk1OVowRzEhMB8GA1UE
+                        CxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMSIwIAYDVQQDExlzcDIwMTMtaGEt
+                        YWRmczIuZ2VhbnQubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+                        qzeOIPfgr/SneQUtjqDRGLbG4YKxmo2zfAIF+wjaYXnDTq/yWt852nImchvCgjWu
+                        eXnHJ6oZZGdaN4kMgPh1oxqJ4UE6dXY9HEoru/Awp2P/CGiYOQ4Q5u5+AyFznGRx
+                        y2uNYrD85C6uTIgiKytH6Lai8f6dFxHKO/u8o+kbrl2Z1CkAf6wePu2H5a44tGnF
+                        SP/s7lW4ScSthNz1OF8BoPFKpWUmJk/584rZZjR+fqB9fQBwxRoRBCFZTduXP0G9
+                        861BS6Nt6Dfuli9jdCBC0L8ai4fH43a94EQVmMfcjImrkYBg7v6lVS4IY75ONiOt
+                        sObamvbT+yNEuqw0jBjmKwIDAQABo4IBdjCCAXIwHwYDVR0jBBgwFoAUDL2TaAzz
+                        3qujSWsrN1dH6pDjue0wHQYDVR0OBBYEFBQXWZZ8RdkPMtC2o0RF+T+gH7VsMA4G
+                        A1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMB
+                        BggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEBAgIdMAgGBmeBDAECATA6
+                        BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY3JsLnRjcy50ZXJlbmEub3JnL1RFUkVO
+                        QVNTTENBLmNybDBtBggrBgEFBQcBAQRhMF8wNQYIKwYBBQUHMAKGKWh0dHA6Ly9j
+                        cnQudGNzLnRlcmVuYS5vcmcvVEVSRU5BU1NMQ0EuY3J0MCYGCCsGAQUFBzABhhpo
+                        dHRwOi8vb2NzcC50Y3MudGVyZW5hLm9yZzAkBgNVHREEHTAbghlzcDIwMTMtaGEt
+                        YWRmczIuZ2VhbnQubmV0MA0GCSqGSIb3DQEBBQUAA4IBAQCRJqxXjeInIqlPLH5+
+                        iFqL92oLPKiJDqlDWpzR9P0xF7IS4oBApWZ3f2Rgcx1dtwUf7TakMpAuxIxzMjfP
+                        xaik/AuInMjxKU9AJS/lRlLogT3YXZ2aAGZbt2P/hYn4mdn5ryB9WF/w7mnyrpBb
+                        o7vllsYXYmRX/c0MEPYfSMoKxomRAY2ViIh83m4sYk8+Nkm+3I9t0O3wBqaQYqpc
+                        Mg48AG2JwLeuKUIC+faPOckeMXdrknL0Ra7Vb+eRoEKCqw3L9ka5/rloFNEN+hMZ
+                        zubyfY3feeIV+kq3YNmjCwhDqckv5kXAdnkFprjB3X/xNfML2Jum02hKkff4AYfP
+                        2IOi
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://example.org/whatever" index="0" isDefault="true"/>
+    </SPSSODescriptor>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-input.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-input.xml
new file mode 100644
index 0000000..dab899a
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-input.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    entityID="http://example.org/entity">
+    <Extensions>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2014-03-18T15:23:31Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes>
+            <saml:Attribute Name="http://macedir.org/entity-category" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://www.geant.net/uri/dataprotection-code-of-conduct/v1</saml:AttributeValue>
+                <saml:AttributeValue>http://example.org/category2</saml:AttributeValue>
+            </saml:Attribute>
+            <saml:Attribute Name="http://macedir.org/entity-category-support" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://example.org/category2support</saml:AttributeValue>
+                <saml:AttributeValue>http://www.geant.net/uri/dataprotection-code-of-conduct/v1</saml:AttributeValue>
+            </saml:Attribute>
+            <saml:Attribute Name="anotherAttributeName" NameFormat="anotherNameFormat">
+                <saml:AttributeValue>anotherValue</saml:AttributeValue>
+            </saml:Attribute>
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIEfzCCA2egAwIBAgIQQSSnV5Mk/EXZxgrsbnU7ajANBgkqhkiG9w0BAQUFADA2
+                        MQswCQYDVQQGEwJOTDEPMA0GA1UEChMGVEVSRU5BMRYwFAYDVQQDEw1URVJFTkEg
+                        U1NMIENBMB4XDTEzMDMwNjAwMDAwMFoXDTE2MDMwNTIzNTk1OVowRzEhMB8GA1UE
+                        CxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMSIwIAYDVQQDExlzcDIwMTMtaGEt
+                        YWRmczIuZ2VhbnQubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+                        qzeOIPfgr/SneQUtjqDRGLbG4YKxmo2zfAIF+wjaYXnDTq/yWt852nImchvCgjWu
+                        eXnHJ6oZZGdaN4kMgPh1oxqJ4UE6dXY9HEoru/Awp2P/CGiYOQ4Q5u5+AyFznGRx
+                        y2uNYrD85C6uTIgiKytH6Lai8f6dFxHKO/u8o+kbrl2Z1CkAf6wePu2H5a44tGnF
+                        SP/s7lW4ScSthNz1OF8BoPFKpWUmJk/584rZZjR+fqB9fQBwxRoRBCFZTduXP0G9
+                        861BS6Nt6Dfuli9jdCBC0L8ai4fH43a94EQVmMfcjImrkYBg7v6lVS4IY75ONiOt
+                        sObamvbT+yNEuqw0jBjmKwIDAQABo4IBdjCCAXIwHwYDVR0jBBgwFoAUDL2TaAzz
+                        3qujSWsrN1dH6pDjue0wHQYDVR0OBBYEFBQXWZZ8RdkPMtC2o0RF+T+gH7VsMA4G
+                        A1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMB
+                        BggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEBAgIdMAgGBmeBDAECATA6
+                        BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY3JsLnRjcy50ZXJlbmEub3JnL1RFUkVO
+                        QVNTTENBLmNybDBtBggrBgEFBQcBAQRhMF8wNQYIKwYBBQUHMAKGKWh0dHA6Ly9j
+                        cnQudGNzLnRlcmVuYS5vcmcvVEVSRU5BU1NMQ0EuY3J0MCYGCCsGAQUFBzABhhpo
+                        dHRwOi8vb2NzcC50Y3MudGVyZW5hLm9yZzAkBgNVHREEHTAbghlzcDIwMTMtaGEt
+                        YWRmczIuZ2VhbnQubmV0MA0GCSqGSIb3DQEBBQUAA4IBAQCRJqxXjeInIqlPLH5+
+                        iFqL92oLPKiJDqlDWpzR9P0xF7IS4oBApWZ3f2Rgcx1dtwUf7TakMpAuxIxzMjfP
+                        xaik/AuInMjxKU9AJS/lRlLogT3YXZ2aAGZbt2P/hYn4mdn5ryB9WF/w7mnyrpBb
+                        o7vllsYXYmRX/c0MEPYfSMoKxomRAY2ViIh83m4sYk8+Nkm+3I9t0O3wBqaQYqpc
+                        Mg48AG2JwLeuKUIC+faPOckeMXdrknL0Ra7Vb+eRoEKCqw3L9ka5/rloFNEN+hMZ
+                        zubyfY3feeIV+kq3YNmjCwhDqckv5kXAdnkFprjB3X/xNfML2Jum02hKkff4AYfP
+                        2IOi
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://example.org/whatever" index="0" isDefault="true"/>
+    </SPSSODescriptor>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-keepcoc.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-keepcoc.xml
new file mode 100644
index 0000000..5ff0b4a
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-keepcoc.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    entityID="http://example.org/entity">
+    <Extensions>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2014-03-18T15:23:31Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes>
+            <saml:Attribute Name="http://macedir.org/entity-category" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://www.geant.net/uri/dataprotection-code-of-conduct/v1</saml:AttributeValue>
+                
+            </saml:Attribute>
+            
+            
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIEfzCCA2egAwIBAgIQQSSnV5Mk/EXZxgrsbnU7ajANBgkqhkiG9w0BAQUFADA2
+                        MQswCQYDVQQGEwJOTDEPMA0GA1UEChMGVEVSRU5BMRYwFAYDVQQDEw1URVJFTkEg
+                        U1NMIENBMB4XDTEzMDMwNjAwMDAwMFoXDTE2MDMwNTIzNTk1OVowRzEhMB8GA1UE
+                        CxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMSIwIAYDVQQDExlzcDIwMTMtaGEt
+                        YWRmczIuZ2VhbnQubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+                        qzeOIPfgr/SneQUtjqDRGLbG4YKxmo2zfAIF+wjaYXnDTq/yWt852nImchvCgjWu
+                        eXnHJ6oZZGdaN4kMgPh1oxqJ4UE6dXY9HEoru/Awp2P/CGiYOQ4Q5u5+AyFznGRx
+                        y2uNYrD85C6uTIgiKytH6Lai8f6dFxHKO/u8o+kbrl2Z1CkAf6wePu2H5a44tGnF
+                        SP/s7lW4ScSthNz1OF8BoPFKpWUmJk/584rZZjR+fqB9fQBwxRoRBCFZTduXP0G9
+                        861BS6Nt6Dfuli9jdCBC0L8ai4fH43a94EQVmMfcjImrkYBg7v6lVS4IY75ONiOt
+                        sObamvbT+yNEuqw0jBjmKwIDAQABo4IBdjCCAXIwHwYDVR0jBBgwFoAUDL2TaAzz
+                        3qujSWsrN1dH6pDjue0wHQYDVR0OBBYEFBQXWZZ8RdkPMtC2o0RF+T+gH7VsMA4G
+                        A1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMB
+                        BggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEBAgIdMAgGBmeBDAECATA6
+                        BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY3JsLnRjcy50ZXJlbmEub3JnL1RFUkVO
+                        QVNTTENBLmNybDBtBggrBgEFBQcBAQRhMF8wNQYIKwYBBQUHMAKGKWh0dHA6Ly9j
+                        cnQudGNzLnRlcmVuYS5vcmcvVEVSRU5BU1NMQ0EuY3J0MCYGCCsGAQUFBzABhhpo
+                        dHRwOi8vb2NzcC50Y3MudGVyZW5hLm9yZzAkBgNVHREEHTAbghlzcDIwMTMtaGEt
+                        YWRmczIuZ2VhbnQubmV0MA0GCSqGSIb3DQEBBQUAA4IBAQCRJqxXjeInIqlPLH5+
+                        iFqL92oLPKiJDqlDWpzR9P0xF7IS4oBApWZ3f2Rgcx1dtwUf7TakMpAuxIxzMjfP
+                        xaik/AuInMjxKU9AJS/lRlLogT3YXZ2aAGZbt2P/hYn4mdn5ryB9WF/w7mnyrpBb
+                        o7vllsYXYmRX/c0MEPYfSMoKxomRAY2ViIh83m4sYk8+Nkm+3I9t0O3wBqaQYqpc
+                        Mg48AG2JwLeuKUIC+faPOckeMXdrknL0Ra7Vb+eRoEKCqw3L9ka5/rloFNEN+hMZ
+                        zubyfY3feeIV+kq3YNmjCwhDqckv5kXAdnkFprjB3X/xNfML2Jum02hKkff4AYfP
+                        2IOi
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://example.org/whatever" index="0" isDefault="true"/>
+    </SPSSODescriptor>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-keepcoc2.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-keepcoc2.xml
new file mode 100644
index 0000000..6be18e8
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-keepcoc2.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    entityID="http://example.org/entity">
+    <Extensions>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2014-03-18T15:23:31Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        <mdattr:EntityAttributes>
+            <saml:Attribute Name="http://macedir.org/entity-category" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>http://www.geant.net/uri/dataprotection-code-of-conduct/v1</saml:AttributeValue>
+                
+            </saml:Attribute>
+            <saml:Attribute Name="http://macedir.org/entity-category-support" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                
+                <saml:AttributeValue>http://www.geant.net/uri/dataprotection-code-of-conduct/v1</saml:AttributeValue>
+            </saml:Attribute>
+            
+        </mdattr:EntityAttributes>
+    </Extensions>
+    <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIEfzCCA2egAwIBAgIQQSSnV5Mk/EXZxgrsbnU7ajANBgkqhkiG9w0BAQUFADA2
+                        MQswCQYDVQQGEwJOTDEPMA0GA1UEChMGVEVSRU5BMRYwFAYDVQQDEw1URVJFTkEg
+                        U1NMIENBMB4XDTEzMDMwNjAwMDAwMFoXDTE2MDMwNTIzNTk1OVowRzEhMB8GA1UE
+                        CxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMSIwIAYDVQQDExlzcDIwMTMtaGEt
+                        YWRmczIuZ2VhbnQubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+                        qzeOIPfgr/SneQUtjqDRGLbG4YKxmo2zfAIF+wjaYXnDTq/yWt852nImchvCgjWu
+                        eXnHJ6oZZGdaN4kMgPh1oxqJ4UE6dXY9HEoru/Awp2P/CGiYOQ4Q5u5+AyFznGRx
+                        y2uNYrD85C6uTIgiKytH6Lai8f6dFxHKO/u8o+kbrl2Z1CkAf6wePu2H5a44tGnF
+                        SP/s7lW4ScSthNz1OF8BoPFKpWUmJk/584rZZjR+fqB9fQBwxRoRBCFZTduXP0G9
+                        861BS6Nt6Dfuli9jdCBC0L8ai4fH43a94EQVmMfcjImrkYBg7v6lVS4IY75ONiOt
+                        sObamvbT+yNEuqw0jBjmKwIDAQABo4IBdjCCAXIwHwYDVR0jBBgwFoAUDL2TaAzz
+                        3qujSWsrN1dH6pDjue0wHQYDVR0OBBYEFBQXWZZ8RdkPMtC2o0RF+T+gH7VsMA4G
+                        A1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMB
+                        BggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEBAgIdMAgGBmeBDAECATA6
+                        BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY3JsLnRjcy50ZXJlbmEub3JnL1RFUkVO
+                        QVNTTENBLmNybDBtBggrBgEFBQcBAQRhMF8wNQYIKwYBBQUHMAKGKWh0dHA6Ly9j
+                        cnQudGNzLnRlcmVuYS5vcmcvVEVSRU5BU1NMQ0EuY3J0MCYGCCsGAQUFBzABhhpo
+                        dHRwOi8vb2NzcC50Y3MudGVyZW5hLm9yZzAkBgNVHREEHTAbghlzcDIwMTMtaGEt
+                        YWRmczIuZ2VhbnQubmV0MA0GCSqGSIb3DQEBBQUAA4IBAQCRJqxXjeInIqlPLH5+
+                        iFqL92oLPKiJDqlDWpzR9P0xF7IS4oBApWZ3f2Rgcx1dtwUf7TakMpAuxIxzMjfP
+                        xaik/AuInMjxKU9AJS/lRlLogT3YXZ2aAGZbt2P/hYn4mdn5ryB9WF/w7mnyrpBb
+                        o7vllsYXYmRX/c0MEPYfSMoKxomRAY2ViIh83m4sYk8+Nkm+3I9t0O3wBqaQYqpc
+                        Mg48AG2JwLeuKUIC+faPOckeMXdrknL0Ra7Vb+eRoEKCqw3L9ka5/rloFNEN+hMZ
+                        zubyfY3feeIV+kq3YNmjCwhDqckv5kXAdnkFprjB3X/xNfML2Jum02hKkff4AYfP
+                        2IOi
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://example.org/whatever" index="0" isDefault="true"/>
+    </SPSSODescriptor>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-keepnone.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-keepnone.xml
new file mode 100644
index 0000000..f4be251
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityAttributeFilteringStage-keepnone.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+    xmlns:mdrpi="urn:oasis:names:tc:SAML:metadata:rpi"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    entityID="http://example.org/entity">
+    <Extensions>
+        <mdrpi:RegistrationInfo registrationAuthority="http://ukfederation.org.uk"
+            registrationInstant="2014-03-18T15:23:31Z">
+            <mdrpi:RegistrationPolicy xml:lang="en"
+                >http://ukfederation.org.uk/doc/mdrps-20130902</mdrpi:RegistrationPolicy>
+        </mdrpi:RegistrationInfo>
+        
+    </Extensions>
+    <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+        <KeyDescriptor>
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>
+                        MIIEfzCCA2egAwIBAgIQQSSnV5Mk/EXZxgrsbnU7ajANBgkqhkiG9w0BAQUFADA2
+                        MQswCQYDVQQGEwJOTDEPMA0GA1UEChMGVEVSRU5BMRYwFAYDVQQDEw1URVJFTkEg
+                        U1NMIENBMB4XDTEzMDMwNjAwMDAwMFoXDTE2MDMwNTIzNTk1OVowRzEhMB8GA1UE
+                        CxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMSIwIAYDVQQDExlzcDIwMTMtaGEt
+                        YWRmczIuZ2VhbnQubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+                        qzeOIPfgr/SneQUtjqDRGLbG4YKxmo2zfAIF+wjaYXnDTq/yWt852nImchvCgjWu
+                        eXnHJ6oZZGdaN4kMgPh1oxqJ4UE6dXY9HEoru/Awp2P/CGiYOQ4Q5u5+AyFznGRx
+                        y2uNYrD85C6uTIgiKytH6Lai8f6dFxHKO/u8o+kbrl2Z1CkAf6wePu2H5a44tGnF
+                        SP/s7lW4ScSthNz1OF8BoPFKpWUmJk/584rZZjR+fqB9fQBwxRoRBCFZTduXP0G9
+                        861BS6Nt6Dfuli9jdCBC0L8ai4fH43a94EQVmMfcjImrkYBg7v6lVS4IY75ONiOt
+                        sObamvbT+yNEuqw0jBjmKwIDAQABo4IBdjCCAXIwHwYDVR0jBBgwFoAUDL2TaAzz
+                        3qujSWsrN1dH6pDjue0wHQYDVR0OBBYEFBQXWZZ8RdkPMtC2o0RF+T+gH7VsMA4G
+                        A1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMB
+                        BggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEBAgIdMAgGBmeBDAECATA6
+                        BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY3JsLnRjcy50ZXJlbmEub3JnL1RFUkVO
+                        QVNTTENBLmNybDBtBggrBgEFBQcBAQRhMF8wNQYIKwYBBQUHMAKGKWh0dHA6Ly9j
+                        cnQudGNzLnRlcmVuYS5vcmcvVEVSRU5BU1NMQ0EuY3J0MCYGCCsGAQUFBzABhhpo
+                        dHRwOi8vb2NzcC50Y3MudGVyZW5hLm9yZzAkBgNVHREEHTAbghlzcDIwMTMtaGEt
+                        YWRmczIuZ2VhbnQubmV0MA0GCSqGSIb3DQEBBQUAA4IBAQCRJqxXjeInIqlPLH5+
+                        iFqL92oLPKiJDqlDWpzR9P0xF7IS4oBApWZ3f2Rgcx1dtwUf7TakMpAuxIxzMjfP
+                        xaik/AuInMjxKU9AJS/lRlLogT3YXZ2aAGZbt2P/hYn4mdn5ryB9WF/w7mnyrpBb
+                        o7vllsYXYmRX/c0MEPYfSMoKxomRAY2ViIh83m4sYk8+Nkm+3I9t0O3wBqaQYqpc
+                        Mg48AG2JwLeuKUIC+faPOckeMXdrknL0Ra7Vb+eRoEKCqw3L9ka5/rloFNEN+hMZ
+                        zubyfY3feeIV+kq3YNmjCwhDqckv5kXAdnkFprjB3X/xNfML2Jum02hKkff4AYfP
+                        2IOi
+                    </ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+            Location="https://example.org/whatever" index="0" isDefault="true"/>
+    </SPSSODescriptor>
+</EntityDescriptor>
diff --git a/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcherSpringTest-config.xml b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcherSpringTest-config.xml
new file mode 100644
index 0000000..dbc2480
--- /dev/null
+++ b/aggregator-pipeline/src/test/resources/net/shibboleth/metadata/dom/saml/mdattr/EntityCategoryMatcherSpringTest-config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="
+        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
+
+    <bean id="categoryMatcherNoRA" class="net.shibboleth.metadata.dom.saml.mdattr.EntityCategoryMatcher">
+        <constructor-arg value="category"/>
+    </bean>    
+
+    <bean id="categoryMatcherWithRA" class="net.shibboleth.metadata.dom.saml.mdattr.EntityCategoryMatcher">
+        <constructor-arg value="category"/>
+        <constructor-arg value="registrar"/>
+    </bean>    
+
+</beans>

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list