[java-metadata-aggregator] 02/02: MDA-287 - Add a stage to validate Shibboleth scopes

Ian Young ian at iay.org.uk
Wed Apr 19 09:57:15 UTC 2023


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

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

View the commit online:
http://git.shibboleth.net/view/?p=java-metadata-aggregator.git;a=commit;h=15e16abec304c52df6c1ccd612a35ebd50ef96e6

commit 15e16abec304c52df6c1ccd612a35ebd50ef96e6
Author: Ian Young <ian at iay.org.uk>
AuthorDate: Wed Apr 19 10:57:05 2023 +0100

    MDA-287 - Add a stage to validate Shibboleth scopes
    
    https://shibboleth.atlassian.net/browse/MDA-287
---
 .../dom/saml/shibboleth/ScopeValidationStage.java  | 138 ++++++++++++++++
 .../saml/shibboleth/ShibbolethMetadataSupport.java |  48 ++++++
 .../metadata/dom/saml/shibboleth/package-info.java |  23 +++
 .../metadata/validate/BaseAsValidator.java         | 157 ++++++++++++++++++
 ...ectDomainNameNotUnderPublicSuffixValidator.java |  54 +++++++
 .../net/RejectDomainNamePublicSuffixValidator.java |  49 ++++++
 .../metadata/validate/net/package-info.java        |  23 +++
 .../string/AsDomainNameStringValidator.java        |  53 ++++++
 .../string/AsLiteralTailStringValidator.java       | 107 ++++++++++++
 .../resources/net/shibboleth/metadata/beans.xml    |  30 ++++
 .../shibboleth/ScopeValidationStageLitmusTest.java | 179 +++++++++++++++++++++
 ...omainNameNotUnderPublicSuffixValidatorTest.java |  91 +++++++++++
 .../RejectDomainNamePublicSuffixValidatorTest.java |  75 +++++++++
 .../string/AsDomainNameStringValidatorTest.java    | 111 +++++++++++++
 .../string/AsLiteralTailStringValidatorTest.java   |  93 +++++++++++
 .../ScopeValidationStageLitmusTest-config.xml      |  70 ++++++++
 16 files changed, 1301 insertions(+)

diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/dom/saml/shibboleth/ScopeValidationStage.java b/mda-framework/src/main/java/net/shibboleth/metadata/dom/saml/shibboleth/ScopeValidationStage.java
new file mode 100644
index 0000000..897e0fd
--- /dev/null
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/dom/saml/shibboleth/ScopeValidationStage.java
@@ -0,0 +1,138 @@
+/*
+ * 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.shibboleth;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.w3c.dom.Element;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.dom.AbstractDOMValidationStage;
+import net.shibboleth.metadata.dom.DOMTraversalContext;
+import net.shibboleth.metadata.dom.SimpleDOMTraversalContext;
+import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.metadata.validate.Validator;
+import net.shibboleth.metadata.validate.ValidatorSequence;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.xml.AttributeSupport;
+import net.shibboleth.shared.xml.ElementSupport;
+
+/**
+ * Stage to apply a collection of validators to Shibboleth <code>shibmd:Scope</code>
+ * values.
+ *
+ * <p>
+ * A separate collection of validators is used for the case of the <code>regexp</code>
+ * attribute being <code>true</code> and <code>false</code>.
+ * </p>
+ *
+ * @since 0.10.0
+ */
+ at ThreadSafe
+public class ScopeValidationStage extends AbstractDOMValidationStage<String, DOMTraversalContext> {
+
+    /** The sequence of validators to apply to <code>regexp</code> scopes. */
+    @GuardedBy("this")
+    private @Nonnull ValidatorSequence<String> regexpValidators = new ValidatorSequence<>();
+
+    /**
+     * Set the sequence of validators to apply to each <code>regexp</code> scope.
+     * 
+     * @param newValidators the list of validators to set
+     */
+    public synchronized void setRegexpValidators(@Nonnull final List<Validator<String>> newValidators) {
+        regexpValidators.setValidators(newValidators);
+    }
+
+    /**
+     * Gets the sequence of validators being applied to each <code>regexp</code> scope.
+     * 
+     * @return list of validators
+     */
+    @Nonnull
+    public synchronized List<Validator<String>> getRegexpValidators() {
+        return regexpValidators.getValidators();
+    }
+
+    @Override
+    protected @Nonnull DOMTraversalContext buildContext(final @Nonnull Item<Element> item) {
+        return new SimpleDOMTraversalContext(item);
+    }
+
+    @Override
+    protected boolean applicable(@Nonnull final Element element, @Nonnull final DOMTraversalContext context) {
+        return ElementSupport.isElementNamed(element, ShibbolethMetadataSupport.SCOPE_NAME);
+    }
+
+    /**
+     * Evaluate the <code>regexp</code> attribute on an {@link Element}.
+     *
+     * <p>Treats a missing attribute or non-boolean string values as <code>false</code></p>8
+     *
+     * @param element {@link Element} hosting the <code>regexp</code> attribute.
+     * @return <code>true</code> if the attribute has a true value (<code>1</code> or
+     *   <code>true</code> or variations), otherwise <code>false</code>
+     */
+    private boolean evaluateRegexpAttribute(final @Nonnull Element element) {
+        final var attr = AttributeSupport.getAttribute(element, ShibbolethMetadataSupport.REGEXP_ATTRIB_NAME);
+        if (attr == null) {
+            return false;
+        }
+
+        final Boolean isRegexp = AttributeSupport.getAttributeValueAsBoolean(attr);
+        if (isRegexp == null) {
+            return false;
+        }
+        
+        return isRegexp.booleanValue();
+    }
+
+    @Override
+    protected void visit(final @Nonnull Element element, final @Nonnull DOMTraversalContext context)
+            throws StageProcessingException {
+
+        final String text = element.getTextContent();
+        assert text != null;
+
+        if (!evaluateRegexpAttribute(element)) {
+            // non-regexp Scope, apply normal validators
+            applyValidators(text, context);
+        } else {
+            // regexp Scope, apply secondary validators
+            regexpValidators.validate(text, context.getItem(), ensureId());
+        }
+    }
+
+    @Override
+    protected void doDestroy() {
+        regexpValidators.destroy();
+        super.doDestroy();
+    }
+
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        regexpValidators.setId(ensureId());
+        regexpValidators.initialize();
+    }
+
+}
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/dom/saml/shibboleth/ShibbolethMetadataSupport.java b/mda-framework/src/main/java/net/shibboleth/metadata/dom/saml/shibboleth/ShibbolethMetadataSupport.java
new file mode 100644
index 0000000..39650e4
--- /dev/null
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/dom/saml/shibboleth/ShibbolethMetadataSupport.java
@@ -0,0 +1,48 @@
+/*
+ * 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.shibboleth;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+import javax.xml.namespace.QName;
+
+/**
+ * Helper class for dealing with Shibboleth metadata.
+ *
+ * @since 0.10.0
+ */
+ at ThreadSafe
+public final class ShibbolethMetadataSupport {
+
+    /** Shibboleth metadata namespace URI. */
+    public static final @Nonnull String SHIBMD_NS = "urn:mace:shibboleth:metadata:1.0";
+
+    /** Default Shibboleth metadata namespace prefix. */
+    public static final @Nonnull String SHIBMD_PREFIX = "shibmd";
+
+    /** Scope element name. */
+    public static final @Nonnull QName SCOPE_NAME = new QName(SHIBMD_NS, "Scope", SHIBMD_PREFIX);
+
+    /** regexp attribute name. */
+    public static final @Nonnull QName REGEXP_ATTRIB_NAME = new QName("regexp");
+
+    /** Constructor. */
+    private ShibbolethMetadataSupport() {
+
+    }
+}
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/dom/saml/shibboleth/package-info.java b/mda-framework/src/main/java/net/shibboleth/metadata/dom/saml/shibboleth/package-info.java
new file mode 100644
index 0000000..7b834c7
--- /dev/null
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/dom/saml/shibboleth/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 Shibboleth SAML metadata.
+ *
+ * @since 0.10.0
+ */
+package net.shibboleth.metadata.dom.saml.shibboleth;
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseAsValidator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseAsValidator.java
new file mode 100644
index 0000000..096e067
--- /dev/null
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/BaseAsValidator.java
@@ -0,0 +1,157 @@
+/*
+ * 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.validate;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * An abstract base class for {@link Validator} implementations which validate a value of
+ * one type "as" another type: for example, to validate a <code>String</code> "as" an
+ * <code>InternetDomainName</code>.
+ *
+ * <p>The implementation calls a template method in the implementation subclass to perform the
+ * conversion. If the conversion succeeds, a sequence of {@link Validator}s are applied to
+ * that new value.</p>
+ *
+ * <p>If the value cannot be converted to the new type, the template method is expected to
+ * throw {@link IllegalArgumentException}. In this case, behaviour depends on the
+ * {@link #conversionRequired} property.</p>
+ *
+ * <p>If {@link #conversionRequired} is <code>true</code> (the default) then an error
+ * status will be applied to the {@link Item}, and the validator will return
+ * {@link net.shibboleth.metadata.validate.Validator.Action#DONE}.</p>
+ *
+ * <p>If {@link #conversionRequired} is <code>false</code> then the validator
+ * will simply return {@link net.shibboleth.metadata.validate.Validator.Action#CONTINUE}
+ * so that subsequent validators may still be applied. This allows several "as" validators
+ * to be applied in sequence, each taking a different approach.</p>
+ *
+ * @param <V> type of the original value
+ * @param <A> type of the new value to which validators should be applied
+ *
+ * @since 0.10.0
+ */
+ at ThreadSafe
+public abstract class BaseAsValidator<V, A> extends BaseValidator implements Validator<V> {
+
+    /** The validator sequence to apply. */
+    @GuardedBy("this")
+    private @Nonnull ValidatorSequence<A> validators = new ValidatorSequence<>();
+
+    /** Whether conversion to the new type must succeed. Default: <code>true</code> */
+    private boolean conversionRequired = true;
+
+    /**
+     * Set the list of validators to apply to each item.
+     * 
+     * @param newValidators the list of validators to set
+     */
+    public synchronized void setValidators(@Nonnull final List<Validator<A>> newValidators) {
+        validators.setValidators(newValidators);
+    }
+
+    /**
+     * Gets the list of validators being applied to each item.
+     * 
+     * @return list of validators
+     */
+    @Nonnull
+    public synchronized List<Validator<A>> getValidators() {
+        return validators.getValidators();
+    }
+
+    /**
+     * Set whether conversion to the new type is required to succeed.
+     *
+     * @param required <code>true</code> if the conversion is required to succeed
+     */
+    public void setConversionRequired(final boolean required) {
+        conversionRequired = required;
+    }
+
+    /**
+     * Returns whether conversion to the new type is required to succeed.
+     *
+     * @return <code>true</code> if the conversion is required to succeed
+     */
+    public boolean isConversionRequired() {
+        return conversionRequired;
+    }
+
+    /**
+     * Apply each of the configured validators in turn to the provided object.
+     *
+     * @param value object to be validated
+     * @param item the {@link Item} context for the validation
+     *
+     * @return the result of applying the validators to the value
+     *
+     * @throws StageProcessingException if errors occur during processing
+     */
+    protected @Nonnull Action applyValidators(@Nonnull final A value, @Nonnull final Item<?> item)
+            throws StageProcessingException {
+        return validators.validate(value, item, ensureId());
+    }
+
+    /**
+     * Convert from the old value type to the new.
+     *
+     * @param from a value of the old type
+     * @return a value of the new type
+     * @throws IllegalArgumentException if a conversion can not be performed
+     */
+    protected abstract @Nonnull A convert(@Nonnull final V from) throws IllegalArgumentException;
+
+    @Override
+    public @Nonnull Action validate(@Nonnull final V t, @Nonnull final Item<?> item, @Nonnull final String stageId)
+            throws StageProcessingException {
+        try {
+            final A v = convert(t);
+            return applyValidators(v, item);
+        } catch (final IllegalArgumentException e) {
+            if (isConversionRequired()) {
+                addErrorMessage(t, item, stageId);
+                return Action.DONE;
+            } else {
+                return Action.CONTINUE;
+            }
+        }
+    }
+
+    @Override
+    protected void doDestroy() {
+        validators.destroy();
+        super.doDestroy();
+    }
+
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        validators.setId(ensureId());
+        validators.initialize();
+    }
+
+}
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/net/RejectDomainNameNotUnderPublicSuffixValidator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/net/RejectDomainNameNotUnderPublicSuffixValidator.java
new file mode 100644
index 0000000..2faffea
--- /dev/null
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/net/RejectDomainNameNotUnderPublicSuffixValidator.java
@@ -0,0 +1,54 @@
+/*
+ * 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.validate.net;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.google.common.net.InternetDomainName;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.validate.BaseValidator;
+import net.shibboleth.metadata.validate.Validator;
+
+/**
+ * A validator that checks whether an {@link InternetDomainName} is under a public suffix.
+ *
+ * <p>
+ * A domain name which is <em>not</em> under a public suffix might be a public suffix itself,
+ * or might terminate in something which is not a public suffix.
+ * </p>
+ *
+ * @since 0.10.0
+ */
+ at ThreadSafe
+public class RejectDomainNameNotUnderPublicSuffixValidator extends BaseValidator
+    implements Validator<InternetDomainName> {
+
+    @Override
+    public @Nonnull Action validate(@Nonnull final InternetDomainName domain, @Nonnull final Item<?> item,
+            @Nonnull final String stageId) {
+        if (domain.isUnderPublicSuffix()) {
+            return Action.CONTINUE;
+        } else {
+            addErrorMessage(domain, item, stageId);
+            return Action.DONE;
+        }
+    }
+
+}
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/net/RejectDomainNamePublicSuffixValidator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/net/RejectDomainNamePublicSuffixValidator.java
new file mode 100644
index 0000000..7b5e849
--- /dev/null
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/net/RejectDomainNamePublicSuffixValidator.java
@@ -0,0 +1,49 @@
+/*
+ * 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.validate.net;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.google.common.net.InternetDomainName;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.validate.BaseValidator;
+import net.shibboleth.metadata.validate.Validator;
+
+/**
+ * A validator that rejects an {@link InternetDomainName} if it is a public suffix.
+ *
+ * @since 0.10.0
+ */
+ at ThreadSafe
+public class RejectDomainNamePublicSuffixValidator extends BaseValidator
+    implements Validator<InternetDomainName> {
+
+    @Override
+    public @Nonnull Action validate(@Nonnull final InternetDomainName domain, @Nonnull final Item<?> item,
+            @Nonnull final String stageId) {
+        if (domain.isPublicSuffix()) {
+            addErrorMessage(domain, item, stageId);
+            return Action.DONE;
+        } else {
+            return Action.CONTINUE;
+        }
+    }
+
+}
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/net/package-info.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/net/package-info.java
new file mode 100644
index 0000000..0202753
--- /dev/null
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/net/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Classes for validation of network-related object types.
+ *
+ * @since 0.10.0
+ */
+package net.shibboleth.metadata.validate.net;
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/string/AsDomainNameStringValidator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/string/AsDomainNameStringValidator.java
new file mode 100644
index 0000000..f307883
--- /dev/null
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/string/AsDomainNameStringValidator.java
@@ -0,0 +1,53 @@
+/*
+ * 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.validate.string;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.net.InternetDomainName;
+
+import net.shibboleth.metadata.validate.BaseAsValidator;
+import net.shibboleth.metadata.validate.Validator;
+
+/**
+ * A <code>Validator</code> that checks {@link String} values as domain names by converting the
+ * value to an {@link InternetDomainName} and applying a sequence of validators to that value.
+ *
+ * <p>
+ * This validator fails (and returns {@link net.shibboleth.metadata.validate.Validator.Action#DONE}) if the
+ * value can not be converted to an {@link InternetDomainName}.
+ * </p>
+ *
+ * <p>
+ * Otherwise, the validator applies the sequence of validators to the {@link InternetDomainName} and returns
+ * the value of that sequence.
+ * </p>
+ *
+ * @since 0.10.0
+ */
+public class AsDomainNameStringValidator extends BaseAsValidator<String, InternetDomainName>
+    implements Validator<String> {
+
+    @Override
+    protected @Nonnull InternetDomainName convert(@Nonnull final String domain) throws IllegalArgumentException {
+        final var result = InternetDomainName.from(domain);
+        assert result != null;
+        return result;
+    }
+
+}
diff --git a/mda-framework/src/main/java/net/shibboleth/metadata/validate/string/AsLiteralTailStringValidator.java b/mda-framework/src/main/java/net/shibboleth/metadata/validate/string/AsLiteralTailStringValidator.java
new file mode 100644
index 0000000..f71eb9e
--- /dev/null
+++ b/mda-framework/src/main/java/net/shibboleth/metadata/validate/string/AsLiteralTailStringValidator.java
@@ -0,0 +1,107 @@
+/*
+ * 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.validate.string;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.metadata.validate.BaseAsValidator;
+import net.shibboleth.metadata.validate.Validator;
+
+/**
+ * A <code>Validator</code> that assists in the validation of regular-expression <shibmd:Scope>
+ * values that include a literal tail.
+ *
+ * <p>A literal tail is:</p>
+ *
+ * <ul>
+ *    <li>a sequence of at least two domain labels</li>
+ *    <li>separated by literal <code>'.'</code> characters (encoded
+ *        in the regular expression as <code>'\.'</code></li>
+ *    <li>explicitly anchored at the end of the regular expression</li>
+ *    <li>preceded by anything terminating with an encoded literal <code>'.'</code>
+ * </ul>
+ *
+ * <p>For example, the literal tail in the regular expression
+ * <code>'^([a-zA-Z0-9-]{1,63}\.){0,2}ddd\.ccc\.bbb\.aa$'</code> is <code>'ccc.bbb.aa'</code>.</p>
+ *
+ * <p>The literal tail is extracted from the regular expression, has its encoded <code>'.'</code>
+ * characters converted to normal ones, and then validated as a {@link String} by a new sequence of
+ * validators.</p>
+ *
+ * <p>This validator fails (and returns {@link net.shibboleth.metadata.validate.Validator.Action#DONE}) if the
+ * value does not possess a literal tail.</p>
+ *
+ * <p>Otherwise, the validator applies the sequence of validators to the new value and returns
+ * the value of that sequence.</p>
+ *
+ * @since 0.10.0
+ */
+public class AsLiteralTailStringValidator extends BaseAsValidator<String, String>
+    implements Validator<String> {
+
+    /**
+     * Regular expression to match and extract the literal tail.
+     *
+     * <p>The component parts of this expression are:</p>
+     *
+     * <ul>
+     *    <li>implicitly, anchored at the start of the string being matched</li>
+     *    <li><code>.*?</code> matches non-greedily any text at the start of the regular expression,
+     *       maximising the size of the later parts of the match</li>
+     *    <li><code>\\\\.</code> matches <code>\.</code> in the regular expression, which in turn
+     *       matches a literal <code>.</code> in the scope</li>
+     *    <li>A group containing:
+     *        <ul>
+     *            <li>At least one DNS label terminated by a literal '.'</li>
+     *            <li>A final DNS label</li>
+     *        </ul>
+     *    </li>
+     *    <li><code>\\$</code> matches an end-string marker in the regular expression being matched</li>
+     *    <li>implicitly, anchored at the end of the string being matched</li>
+     * </ul>
+     *
+     * <p>The matching of DNS labels is not exact. For example, labels starting or ending with hyphens
+     * are accepted as part of a literal tail. This will normally be detected by the nested validator
+     * sequence applied to the result.</p>
+     *
+     * <p>Similarly, upper-case characters are permitted in the literal tail although these would
+     * not normally be permitted in scopes. Again, these characters are permitted so that a more
+     * specific error can be reported, rather than just a generic failure to convert.</p>
+     */
+    private final Pattern pattern = Pattern.compile(".*?\\\\.(([a-zA-Z0-9-]+\\\\.)+[a-zA-Z0-9-]+)\\$");
+
+    @Override
+    protected @Nonnull String convert(@Nonnull final String regex) throws IllegalArgumentException {
+        // Match against the regular expression
+        final Matcher matcher = pattern.matcher(regex);
+
+        // If the pattern does not match, signal that the string does not have a literal tail
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException();
+        }
+
+        // Remove all '\' characters from the result.
+        final var result = matcher.group(1).replaceAll("\\\\", "");
+        assert result != null;
+        return result;
+    }
+
+}
diff --git a/mda-framework/src/main/resources/net/shibboleth/metadata/beans.xml b/mda-framework/src/main/resources/net/shibboleth/metadata/beans.xml
index d71f356..e03c545 100644
--- a/mda-framework/src/main/resources/net/shibboleth/metadata/beans.xml
+++ b/mda-framework/src/main/resources/net/shibboleth/metadata/beans.xml
@@ -213,6 +213,13 @@
     <bean id="mda.IPHintValidationStage" abstract="true" parent="mda.stage_parent"
         class="net.shibboleth.metadata.dom.saml.mdui.IPHintValidationStage"/>
 
+    <!--
+        net.shibboleth.metadata.dom.saml.shibboleth
+    -->
+
+    <bean id="mda.ScopeValidationStage" abstract="true" parent="mda.stage_parent"
+        class="net.shibboleth.metadata.dom.saml.shibboleth.ScopeValidationStage"/>
+
     <!--
         net.shibboleth.metadata.pipeline
     -->
@@ -299,6 +306,16 @@
     <bean id="mda.RejectAllValidator" abstract="true" parent="mda.validator_parent"
         class="net.shibboleth.metadata.validate.RejectAllValidator"/>
 
+    <!--
+        net.shibboleth.metadata.validate.net
+    -->
+
+    <bean id="mda.RejectDomainNameNotUnderPublicSuffixValidator" abstract="true" parent="mda.component_parent"
+        class="net.shibboleth.metadata.validate.net.RejectDomainNameNotUnderPublicSuffixValidator"/>
+
+    <bean id="mda.RejectDomainNamePublicSuffixValidator" abstract="true" parent="mda.component_parent"
+        class="net.shibboleth.metadata.validate.net.RejectDomainNamePublicSuffixValidator"/>
+
     <!--
         net.shibboleth.metadata.validate.string
     -->
@@ -309,6 +326,12 @@
     <bean id="mda.AcceptStringValueValidator" abstract="true" parent="mda.validator_parent"
         class="net.shibboleth.metadata.validate.string.AcceptStringValueValidator"/>
 
+    <bean id="mda.AsDomainNameStringValidator" abstract="true" parent="mda.component_parent"
+        class="net.shibboleth.metadata.validate.string.AsDomainNameStringValidator"/>
+
+    <bean id="mda.AsLiteralTailStringValidator" abstract="true" parent="mda.component_parent"
+        class="net.shibboleth.metadata.validate.string.AsLiteralTailStringValidator"/>
+
     <bean id="mda.RejectStringRegexValidator" abstract="true" parent="mda.validator_parent"
         class="net.shibboleth.metadata.validate.string.RejectStringRegexValidator"/>
 
@@ -405,11 +428,18 @@
 
         <!-- Beans adopted from inc-mda project in MDA 0.10.0. -->
         <entry key="inc.AcceptAllValidator"                 value="mda.AcceptAllValidator"/>
+        <entry key="inc.AsDomainNameStringValidator"        value="mda.AsDomainNameStringValidator"/>
+        <entry key="inc.AsLiteralTailStringValidator"       value="mda.AsLiteralTailStringValidator"/>
         <entry key="inc.RejectAllValidator"                 value="mda.RejectAllValidator"/>
         <entry key="inc.AcceptStringRegexValidator"         value="mda.AcceptStringRegexValidator"/>
         <entry key="inc.AcceptStringValueValidator"         value="mda.AcceptStringValueValidator"/>
+        <entry key="inc.RejectDomainNameNotUnderPublicSuffixValidator"
+                                                            value="mda.RejectDomainNameNotUnderPublicSuffixValidator"/>
+        <entry key="inc.RejectDomainNamePublicSuffixValidator"
+                                                            value="mda.RejectDomainNamePublicSuffixValidator"/>
         <entry key="inc.RejectStringRegexValidator"         value="mda.RejectStringRegexValidator"/>
         <entry key="inc.RejectStringValueValidator"         value="mda.RejectStringValueValidator"/>
+        <entry key="inc.ScopeValidationStage"               value="mda.ScopeValidationStage"/>
 
     </util:map>
 
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/dom/saml/shibboleth/ScopeValidationStageLitmusTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/dom/saml/shibboleth/ScopeValidationStageLitmusTest.java
new file mode 100644
index 0000000..7a9fd9d
--- /dev/null
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/dom/saml/shibboleth/ScopeValidationStageLitmusTest.java
@@ -0,0 +1,179 @@
+
+package net.shibboleth.metadata.dom.saml.shibboleth;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import net.shibboleth.metadata.ErrorStatus;
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.dom.DOMElementItem;
+import net.shibboleth.metadata.pipeline.Stage;
+import net.shibboleth.shared.xml.AttributeSupport;
+import net.shibboleth.shared.xml.ElementSupport;
+
+/**
+ * A litmus test for {@link ScopeValidationStage} involving a set of valid and invalid
+ * scope values, both for regular expression and plain cases.
+ *
+ * <p>
+ * The configuration for the stage is taken from a Spring XML configuration file.
+ * </p>
+ */
+ at ContextConfiguration("ScopeValidationStageLitmusTest-config.xml")
+public class ScopeValidationStageLitmusTest extends AbstractTestNGSpringContextTests {
+
+    /** Build documents using this. */
+    private DocumentBuilder dBuilder;
+
+    /** {@link Stage} to run for each test. */
+    private Stage<Element> stage;
+
+    @BeforeClass
+    private void setUp() throws Exception {
+        final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+        dBuilder = dbFactory.newDocumentBuilder();
+        stage = makeStage();
+    }
+
+    /** Acquire the configured stage from the Spring context. */
+    private Stage<Element> makeStage() throws Exception {
+        assert applicationContext != null;
+        @SuppressWarnings("unchecked")
+        final Stage<Element> stage = applicationContext.getBean("litmusTest", Stage.class);
+        stage.initialize();
+        return stage;
+    }
+
+    /** Build a <code>shibmd:Scope</code> {@link Element}. */
+    private Element buildScope(final @Nonnull Document document, final String value, final boolean isRegex) {
+        final Element element = ElementSupport.constructElement(document, ShibbolethMetadataSupport.SCOPE_NAME);
+        AttributeSupport.appendAttribute(element, ShibbolethMetadataSupport.REGEXP_ATTRIB_NAME,
+                isRegex ? "true" : "false");
+        element.setTextContent(value);
+        return element;
+    }
+
+    /** Build a {@link Document} containing an appropriate <code>shibmd:Scope</code> {@link Element}. */
+    private @Nonnull Document buildDocument(final String value, final boolean isRegex) {
+        final Document document = dBuilder.newDocument();
+        document.appendChild(buildScope(document, value, isRegex));
+        return document;
+    }
+
+    /** Run the test stage on a single {@link Item}. */
+    private List<ErrorStatus> runTest(final Item<Element> item) throws Exception {
+        final List<Item<Element>> coll = new ArrayList<>();
+        coll.add(item);
+        stage.execute(coll);
+        final List<ErrorStatus> errors = item.getItemMetadata().get(ErrorStatus.class);
+        return errors;
+    }
+
+    /** Test a value-regexp combination we expect to be accepted. */
+    private void good(final String value, final boolean isRegex) throws Exception {
+        final Item<Element> item = new DOMElementItem(buildDocument(value, isRegex));
+        final List<ErrorStatus> errors = runTest(item);
+        if (errors.size() != 0) {
+            Assert.fail("expected no errors for '" + value + "'[" + isRegex + "] " +
+                "but saw \"" + errors.get(0).getStatusMessage() + "\"");
+        }
+    }
+
+    /** Test a non-regexp value we expect to be accepted. */
+    private void good(final String value) throws Exception {
+        good(value, false);
+    }
+
+    /** Test a regexp value we expect to be accepted. */
+    private void goodRegexp(final String value) throws Exception {
+        good(value, true);
+    }
+
+    /** Test a value-regexp combination we expect to be rejected. */
+    private void bad(final String value, final boolean isRegex, final String why) throws Exception {
+        final Item<Element> item = new DOMElementItem(buildDocument(value, isRegex));
+        final List<ErrorStatus> errors = runTest(item);
+        Assert.assertEquals(errors.size(), 1, "expected an error for '" + value + "'[" + isRegex + "]");
+        final ErrorStatus error = errors.get(0);
+        final String message = error.getStatusMessage();
+        Assert.assertTrue(message.contains(why), "error '" + message + "' didn't contain '" + why + "'");
+    }
+
+    /** Test a non-regexp value we expect to be rejected. */
+    private void bad(final String value) throws Exception {
+        bad(value, false, "");
+    }
+
+    /** Test a non-regexp value we expect to be rejected. */
+    private void bad(final String value, final String why) throws Exception {
+        bad(value, false, why);
+    }
+
+    /** Test a regexp value we expect to be rejected. */
+    private void badRegexp(final String value, final String why) throws Exception {
+        bad(value, true, why);
+    }
+
+    /** Test a regexp value we expect to be rejected. */
+    private void badRegexp(final String value) throws Exception {
+        bad(value, true, "");
+    }
+
+    @Test
+    public void litmusTests() throws Exception {
+        good("example.org");
+        good("UGent.be");
+        bad("", "empty");
+        bad(" ");
+        bad("  ");
+        bad(" example.org", "white space");
+        bad("example.org ", "white space");
+        bad("example**.org", "scope is not a valid domain name: example**.org");
+        bad("uk", "scope is a public suffix");
+        bad("ac.uk", "scope is a public suffix");
+        bad("random.nonsense", "scope is not under a public suffix");
+        good("example.ac.uk");
+        bad("adm.aau.dk at aau.dk"); // incommon/inc-meta#58
+        bad("example .org", "white space");
+        bad("\nexample.org", "white space");
+
+        badRegexp("", "empty");
+        badRegexp(" ");
+        badRegexp("  ");
+        badRegexp("aaaa$", "does not start with an anchor ('^')");
+        badRegexp("^aaaa", "does not end with an anchor ('$')");
+        goodRegexp("^([a-zA-Z0-9-]{1,63}\\.){0,2}vho\\.aaf\\.edu\\.au$");
+        // don't use literal .s
+        badRegexp("^([a-zA-Z0-9-]{1,63}.){0,2}vho.aaf.edu.au$", "does not end with a literal tail");
+        // bad literal tail: no public suffix
+        badRegexp("^([a-zA-Z0-9-]{1,63}\\.){0,2}vho\\.aaf\\.edu\\.nopublic$", "literal tail is not under a public suffix");
+        // bad literal tail: is a public suffix
+        badRegexp("^.*\\.ac\\.uk$", "literal tail is a public suffix");
+
+        // UK federation examples
+        goodRegexp("^.+\\.atomwide\\.com$");
+        goodRegexp("^.+\\.856\\.eng\\.ukfederation\\.org\\.uk$");
+        goodRegexp("^.+\\.scot\\.nhs\\.uk$");
+        goodRegexp("^.+\\.login\\.groupcall\\.com$");
+        goodRegexp("^.+\\.logintestingthirks\\.groupcall\\.com$");
+        goodRegexp("^.+\\.logintest\\.me\\.e2bn\\.org$");
+        goodRegexp("^.+\\.loginstaging\\.groupcall\\.com$");
+        goodRegexp("^.+\\.identityfor\\.co\\.uk$");
+        goodRegexp("^.+\\.rmunify\\.com$");
+
+        // Combination regexp plus case significance
+        goodRegexp("^.+\\.UGent\\.be$");
+    }
+}
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/net/RejectDomainNameNotUnderPublicSuffixValidatorTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/net/RejectDomainNameNotUnderPublicSuffixValidatorTest.java
new file mode 100644
index 0000000..d06de16
--- /dev/null
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/net/RejectDomainNameNotUnderPublicSuffixValidatorTest.java
@@ -0,0 +1,91 @@
+package net.shibboleth.metadata.validate.net;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.net.InternetDomainName;
+
+import net.shibboleth.metadata.ErrorStatus;
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.testing.MockItem;
+import net.shibboleth.metadata.validate.Validator.Action;
+
+public class RejectDomainNameNotUnderPublicSuffixValidatorTest {
+
+    @Test
+    public void normal() throws Exception {
+        final Item<String> item = new MockItem("content");
+        final RejectDomainNameNotUnderPublicSuffixValidator val =
+                new RejectDomainNameNotUnderPublicSuffixValidator();
+        val.setId("validate");
+        val.initialize();
+
+        final InternetDomainName domain = InternetDomainName.from("example.org");
+        assert domain != null;
+        final Action res = val.validate(domain, item, "stage");
+        Assert.assertNotNull(res);
+        Assert.assertEquals(res, Action.CONTINUE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 0);
+
+        final var domain2 = InternetDomainName.from("ed.ac.uk");
+        assert domain2 != null;
+        Assert.assertEquals(val.validate(domain2, item, "stage"), Action.CONTINUE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 0);
+    }
+
+    @Test
+    public void uk() throws Exception {
+        final Item<String> item = new MockItem("content");
+        final RejectDomainNameNotUnderPublicSuffixValidator val =
+                new RejectDomainNameNotUnderPublicSuffixValidator();
+        val.setId("validate");
+        val.initialize();
+
+        final InternetDomainName domain = InternetDomainName.from("uk");
+        assert domain != null;
+        final Action res = val.validate(domain, item, "stage");
+        Assert.assertNotNull(res);
+        Assert.assertEquals(res, Action.DONE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 1);
+        Assert.assertTrue(item.getItemMetadata().get(ErrorStatus.class).get(0).getStatusMessage().contains("rejected"));
+    }
+
+    @Test
+    public void ac_uk() throws Exception {
+        final Item<String> item = new MockItem("content");
+        final RejectDomainNameNotUnderPublicSuffixValidator val =
+                new RejectDomainNameNotUnderPublicSuffixValidator();
+        val.setId("validate");
+        val.initialize();
+
+        final InternetDomainName domain = InternetDomainName.from("ac.uk");
+        assert domain != null;
+        final Action res = val.validate(domain, item, "stage");
+        Assert.assertNotNull(res);
+        Assert.assertEquals(res, Action.DONE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 1);
+        Assert.assertTrue(item.getItemMetadata().get(ErrorStatus.class).get(0).getStatusMessage().contains("rejected"));
+    }
+
+    @Test
+    public void wibble_wobble() throws Exception {
+        final Item<String> item = new MockItem("content");
+        final RejectDomainNameNotUnderPublicSuffixValidator val =
+                new RejectDomainNameNotUnderPublicSuffixValidator();
+        val.setId("validate");
+        val.setMessage("scope is not under a public suffix: '%s'");
+        val.initialize();
+
+        // This is (currently) just a nonsense value, so it doesn't have a public suffix
+        // and isn't under one either.
+        final InternetDomainName domain = InternetDomainName.from("wibble.wobble");
+        assert domain != null;
+        final Action res = val.validate(domain, item, "stage");
+        Assert.assertNotNull(res);
+        Assert.assertEquals(res, Action.DONE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 1);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).get(0).getStatusMessage(),
+                "scope is not under a public suffix: 'wibble.wobble'");
+    }
+
+}
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/net/RejectDomainNamePublicSuffixValidatorTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/net/RejectDomainNamePublicSuffixValidatorTest.java
new file mode 100644
index 0000000..a35d2f5
--- /dev/null
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/net/RejectDomainNamePublicSuffixValidatorTest.java
@@ -0,0 +1,75 @@
+package net.shibboleth.metadata.validate.net;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.net.InternetDomainName;
+
+import net.shibboleth.metadata.ErrorStatus;
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.testing.MockItem;
+import net.shibboleth.metadata.validate.Validator.Action;
+
+public class RejectDomainNamePublicSuffixValidatorTest {
+
+    @Test
+    public void normal() throws Exception {
+        final Item<String> item = new MockItem("content");
+        final RejectDomainNamePublicSuffixValidator val =
+                new RejectDomainNamePublicSuffixValidator();
+        val.setId("validate");
+        val.initialize();
+
+        final InternetDomainName domain = InternetDomainName.from("example.org");
+        assert domain != null;
+        final Action res = val.validate(domain, item, "stage");
+        Assert.assertNotNull(res);
+        Assert.assertEquals(res, Action.CONTINUE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 0);
+
+        final var domain2 = InternetDomainName.from("ed.ac.uk");
+        assert domain2 != null;
+        Assert.assertEquals(val.validate(domain2, item, "stage"), Action.CONTINUE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 0);
+
+        final var domain3 = InternetDomainName.from("complete.nonsense");
+        assert domain3 != null;
+        Assert.assertEquals(val.validate(domain3, item, "stage"), Action.CONTINUE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 0);
+    }
+
+    @Test
+    public void uk() throws Exception {
+        final Item<String> item = new MockItem("content");
+        final RejectDomainNamePublicSuffixValidator val =
+                new RejectDomainNamePublicSuffixValidator();
+        val.setId("validate");
+        val.initialize();
+
+        final InternetDomainName domain = InternetDomainName.from("uk");
+        assert domain != null;
+        final Action res = val.validate(domain, item, "stage");
+        Assert.assertNotNull(res);
+        Assert.assertEquals(res, Action.DONE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 1);
+        Assert.assertTrue(item.getItemMetadata().get(ErrorStatus.class).get(0).getStatusMessage().contains("rejected"));
+    }
+
+    @Test
+    public void ac_uk() throws Exception {
+        final Item<String> item = new MockItem("content");
+        final RejectDomainNamePublicSuffixValidator val =
+                new RejectDomainNamePublicSuffixValidator();
+        val.setId("validate");
+        val.initialize();
+
+        final InternetDomainName domain = InternetDomainName.from("ac.uk");
+        assert domain != null;
+        final Action res = val.validate(domain, item, "stage");
+        Assert.assertNotNull(res);
+        Assert.assertEquals(res, Action.DONE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 1);
+        Assert.assertTrue(item.getItemMetadata().get(ErrorStatus.class).get(0).getStatusMessage().contains("rejected"));
+    }
+
+}
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/string/AsDomainNameStringValidatorTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/string/AsDomainNameStringValidatorTest.java
new file mode 100644
index 0000000..3025a28
--- /dev/null
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/string/AsDomainNameStringValidatorTest.java
@@ -0,0 +1,111 @@
+
+package net.shibboleth.metadata.validate.string;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.net.InternetDomainName;
+
+import net.shibboleth.metadata.ErrorStatus;
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.metadata.testing.MockItem;
+import net.shibboleth.metadata.validate.BaseValidator;
+import net.shibboleth.metadata.validate.Validator;
+import net.shibboleth.metadata.validate.Validator.Action;
+
+public class AsDomainNameStringValidatorTest {
+
+    private static class CountingValidator extends BaseValidator implements Validator<InternetDomainName> {
+        public int count;
+        private final @Nonnull Action action;
+
+        @Override
+        public @Nonnull Action validate(@Nonnull InternetDomainName e, @Nonnull Item<?> item, @Nonnull String stageId)
+                throws StageProcessingException {
+            count++;
+            return action;
+        }
+
+        /** Constructor. */
+        public CountingValidator(final @Nonnull Action a) {
+            action = a;
+        }
+    }
+
+    @Test
+    public void testOK() throws Exception {
+        final CountingValidator counter = new CountingValidator(Action.CONTINUE);
+        counter.setId("counter");
+        counter.initialize();
+
+        final List<Validator<InternetDomainName>> nestedSequence = new ArrayList<>();
+        nestedSequence.add(counter);
+
+        final Item<String> item = new MockItem("content");
+
+        final AsDomainNameStringValidator val = new AsDomainNameStringValidator();
+        val.setId("id");
+        val.setValidators(nestedSequence);
+        val.initialize();
+
+        final Action res = val.validate("example.org", item, "stage");
+        Assert.assertEquals(res, Action.CONTINUE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 0);
+        Assert.assertEquals(counter.count, 1);
+    }
+
+    @Test
+    public void testNoConvertDefault() throws Exception {
+        final CountingValidator counter = new CountingValidator(Action.CONTINUE);
+        counter.setId("counter");
+        counter.initialize();
+
+        final List<Validator<InternetDomainName>> nestedSequence = new ArrayList<>();
+        nestedSequence.add(counter);
+
+        final Item<String> item = new MockItem("content");
+
+        final AsDomainNameStringValidator val = new AsDomainNameStringValidator();
+        val.setId("id");
+        val.setValidators(nestedSequence);
+        val.setMessage("quick brown %s");
+        val.initialize();
+
+        final Action res = val.validate("example**.org", item, "stage");
+        Assert.assertEquals(res, Action.DONE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 1);
+        final ErrorStatus err = item.getItemMetadata().get(ErrorStatus.class).get(0);
+        Assert.assertTrue(err.getStatusMessage().contains("quick brown example**.org"));
+        Assert.assertEquals(counter.count, 0);
+    }
+
+    @Test
+    public void testNoConvertFalse() throws Exception {
+        final CountingValidator counter = new CountingValidator(Action.CONTINUE);
+        counter.setId("counter");
+        counter.initialize();
+
+        final List<Validator<InternetDomainName>> nestedSequence = new ArrayList<>();
+        nestedSequence.add(counter);
+
+        final Item<String> item = new MockItem("content");
+
+        final AsDomainNameStringValidator val = new AsDomainNameStringValidator();
+        val.setId("id");
+        val.setConversionRequired(false);
+        val.setValidators(nestedSequence);
+        val.initialize();
+
+        final Action res = val.validate("example**.org", item, "stage");
+        Assert.assertEquals(res, Action.CONTINUE);
+        Assert.assertEquals(item.getItemMetadata().get(ErrorStatus.class).size(), 0);
+        Assert.assertEquals(counter.count, 0);
+    }
+
+}
diff --git a/mda-framework/src/test/java/net/shibboleth/metadata/validate/string/AsLiteralTailStringValidatorTest.java b/mda-framework/src/test/java/net/shibboleth/metadata/validate/string/AsLiteralTailStringValidatorTest.java
new file mode 100644
index 0000000..bd7cbc2
--- /dev/null
+++ b/mda-framework/src/test/java/net/shibboleth/metadata/validate/string/AsLiteralTailStringValidatorTest.java
@@ -0,0 +1,93 @@
+
+package net.shibboleth.metadata.validate.string;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nonnull;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import net.shibboleth.metadata.Item;
+import net.shibboleth.metadata.pipeline.StageProcessingException;
+import net.shibboleth.metadata.testing.MockItem;
+import net.shibboleth.metadata.validate.BaseValidator;
+import net.shibboleth.metadata.validate.Validator;
+import net.shibboleth.metadata.validate.Validator.Action;
+
+public class AsLiteralTailStringValidatorTest {
+
+    private static class CountingCapturingValidator extends BaseValidator implements Validator<String> {
+        public int count;
+        public String value;
+        private final @Nonnull Action action;
+
+        @Override
+        public @Nonnull Action validate(@Nonnull String e, @Nonnull Item<?> item, @Nonnull String stageId) throws StageProcessingException {
+            count++;
+            value = e;
+            return action;
+        }
+
+        /** Constructor. */
+        public CountingCapturingValidator(final @Nonnull Action a) {
+            action = a;
+        }
+    }
+
+    @Test
+    public void testAssumptions() throws Exception {
+        final Pattern pattern = Pattern.compile(".*?\\\\.(([a-zA-Z0-9-]+\\\\.)+[a-zA-Z0-9-]+)\\$");
+        final String value = "^([a-zA-Z0-9-]{1,63}\\.){0,2}vho\\.aaf\\.edu\\.au$";
+        final Matcher matcher = pattern.matcher(value);
+        Assert.assertTrue(matcher.matches());
+    }
+
+    @Test
+    public void testExample() throws Exception {
+        final CountingCapturingValidator ccv = new CountingCapturingValidator(Action.CONTINUE);
+        ccv.setId("ccv");
+        ccv.initialize();
+
+        final List<Validator<String>> nvs = new ArrayList<>();
+        nvs.add(ccv);
+
+        final AsLiteralTailStringValidator val = new AsLiteralTailStringValidator();
+        val.setId("val");
+        val.setValidators(nvs);
+        val.initialize();
+
+        final Item<String> item = new MockItem("content");
+        Assert.assertEquals(val.validate("^([a-zA-Z0-9-]{1,63}\\.){0,2}vho\\.aaf\\.edu\\.au$", item, "stage"), Action.CONTINUE);
+        Assert.assertEquals(ccv.count, 1);
+        Assert.assertEquals(ccv.value, "aaf.edu.au");
+    }
+
+    /*
+     * Example from the REFEDS MRPS template document.
+     *
+     * See https://github.com/REFEDS/MRPS/blob/master/MRPS-templatev1.1.pdf
+     */
+    @Test
+    public void testREFEDSExample() throws Exception {
+        final CountingCapturingValidator ccv = new CountingCapturingValidator(Action.CONTINUE);
+        ccv.setId("ccv");
+        ccv.initialize();
+
+        final List<Validator<String>> nvs = new ArrayList<>();
+        nvs.add(ccv);
+
+        final AsLiteralTailStringValidator val = new AsLiteralTailStringValidator();
+        val.setId("val");
+        val.setValidators(nvs);
+        val.initialize();
+
+        final Item<String> item = new MockItem("content");
+        Assert.assertEquals(val.validate("^(foo|bar)\\.example\\.com$", item, "stage"), Action.CONTINUE);
+        Assert.assertEquals(ccv.count, 1);
+        Assert.assertEquals(ccv.value, "example.com");
+    }
+}
diff --git a/mda-framework/src/test/resources/net/shibboleth/metadata/dom/saml/shibboleth/ScopeValidationStageLitmusTest-config.xml b/mda-framework/src/test/resources/net/shibboleth/metadata/dom/saml/shibboleth/ScopeValidationStageLitmusTest-config.xml
new file mode 100644
index 0000000..8ab948f
--- /dev/null
+++ b/mda-framework/src/test/resources/net/shibboleth/metadata/dom/saml/shibboleth/ScopeValidationStageLitmusTest-config.xml
@@ -0,0 +1,70 @@
+<?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">
+
+    <import resource="classpath:net/shibboleth/metadata/beans.xml"/>
+
+    <bean class="net.shibboleth.shared.spring.config.IdentifiableBeanPostProcessor"/>
+
+    <bean id="litmusTest" parent="mda.ScopeValidationStage">
+        <property name="validators">
+            <list>
+                <bean parent="mda.RejectStringRegexValidator"
+                    p:regex="" p:message="scope element must not be empty"/>
+                <bean parent="mda.RejectStringRegexValidator"
+                    p:regex=".*\s.*" p:message="scope '%s' includes white space"/>
+                <bean parent="mda.AsDomainNameStringValidator"
+                    p:message="scope is not a valid domain name: %s">
+                    <property name="validators">
+                        <list>
+                            <!-- DNS name validators -->
+                            <bean parent="mda.RejectDomainNamePublicSuffixValidator"
+                                p:message="scope is a public suffix: '%s'"/>
+                            <bean parent="mda.RejectDomainNameNotUnderPublicSuffixValidator"
+                                p:message="scope is not under a public suffix: '%s'"/>
+                        </list>
+                    </property>
+                </bean>
+            </list>
+        </property>
+        <property name="regexpValidators">
+            <list>
+                <bean parent="mda.RejectStringRegexValidator"
+                    p:regex="" p:message="scope element must not be empty"/>
+                <bean parent="mda.RejectStringRegexValidator"
+                    p:regex=".*\s.*" p:message="scope '%s' includes white space"/>
+                <bean parent="mda.RejectStringRegexValidator"
+                    p:regex="[^^].*" p:message="regex scope '%s' does not start with an anchor ('^')"/>
+                <bean parent="mda.RejectStringRegexValidator"
+                    p:regex=".*[^$]" p:message="regex scope '%s' does not end with an anchor ('$')"/>
+                <bean parent="mda.AsLiteralTailStringValidator"
+                    p:message="regular expression '%s' does not end with a literal tail">
+                    <property name="validators">
+                        <!-- validators to apply to the literal tail -->
+                        <list>
+                            <bean parent="mda.AsDomainNameStringValidator"
+                                p:message="literal tail is not a valid domain name: %s">
+                                <property name="validators">
+                                    <list>
+                                        <!-- DNS name validators for the literal tail -->
+                                        <bean parent="mda.RejectDomainNamePublicSuffixValidator"
+                                            p:message="literal tail is a public suffix: '%s'"/>
+                                        <bean parent="mda.RejectDomainNameNotUnderPublicSuffixValidator"
+                                            p:message="literal tail is not under a public suffix: '%s'"/>
+                                    </list>
+                                </property>
+                            </bean>
+                        </list>
+                    </property>
+                </bean>
+            </list>
+        </property>
+    </bean>
+
+</beans>

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


More information about the commits mailing list