diff --git a/pom.xml b/pom.xml index cd0667f..3d78db8 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ 0.9.2 0.9.4 7.2.0 + 5.2.0 7.6 @@ -56,6 +57,12 @@ + + net.shibboleth.ext + spring-extensions + ${spring-extensions.version} + test + uk.org.ukfederation ukf-mda @@ -78,6 +85,11 @@ spring-core test + + ${spring.groupId} + spring-test + test + diff --git a/src/main/java/uk/org/iay/incommon/mda/dom/saml/shib/ScopeValidationStage.java b/src/main/java/uk/org/iay/incommon/mda/dom/saml/shib/ScopeValidationStage.java new file mode 100644 index 0000000..b7b4088 --- /dev/null +++ b/src/main/java/uk/org/iay/incommon/mda/dom/saml/shib/ScopeValidationStage.java @@ -0,0 +1,97 @@ +/* + * Licensed 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 uk.org.iay.incommon.mda.dom.saml.shib; + +import java.util.List; + +import javax.annotation.Nonnull; + +import org.w3c.dom.Element; + +import net.shibboleth.metadata.dom.AbstractDOMValidationStage; +import net.shibboleth.metadata.pipeline.StageProcessingException; +import net.shibboleth.metadata.validate.Validator; +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.xml.AttributeSupport; +import net.shibboleth.utilities.java.support.xml.ElementSupport; +import uk.org.iay.incommon.mda.validate.ValidatorSequence; + +/** + * Stage to apply a collection of validators to Shibboleth shibmd:Scope + * values. + * + * A separate collection of validators is used for the case of the regexp + * attribute being true and false. + */ +public class ScopeValidationStage extends AbstractDOMValidationStage { + + /** The sequence of validators to apply to regexp scopes. */ + @Nonnull + private ValidatorSequence regexpValidators = new ValidatorSequence<>(); + + /** + * Set the sequence of validators to apply to each regexp scope. + * + * @param newValidators the list of validators to set + */ + public void setRegexpValidators(@Nonnull final List> newValidators) { + regexpValidators.setValidators(newValidators); + } + + /** + * Gets the sequence of validators being applied to each regexp scope. + * + * @return list of validators + */ + @Nonnull + public List> getRegexpValidators() { + return regexpValidators.getValidators(); + } + + @Override + protected boolean applicable(@Nonnull final Element element) { + return ElementSupport.isElementNamed(element, ShibbolethMetadataSupport.SCOPE_NAME); + } + + @Override + protected void visit(@Nonnull final Element element, @Nonnull final TraversalContext context) + throws StageProcessingException { + final String text = element.getTextContent(); + final Boolean isRegexp = AttributeSupport.getAttributeValueAsBoolean( + AttributeSupport.getAttribute(element, ShibbolethMetadataSupport.REGEXP_ATTRIB_NAME)); + if (isRegexp == null || !isRegexp.booleanValue()) { + // non-regexp Scope, apply normal validators + applyValidators(text, context); + } else { + // regexp Scope, apply secondary validators + regexpValidators.validate(text, context.getItem(), getId()); + } + } + + @Override + protected void doDestroy() { + regexpValidators.destroy(); + regexpValidators = null; + super.doDestroy(); + } + + @Override + protected void doInitialize() throws ComponentInitializationException { + super.doInitialize(); + regexpValidators.setId(getId()); + regexpValidators.initialize(); + } + +} diff --git a/src/main/java/uk/org/iay/incommon/mda/dom/saml/shib/ShibbolethMetadataSupport.java b/src/main/java/uk/org/iay/incommon/mda/dom/saml/shib/ShibbolethMetadataSupport.java new file mode 100644 index 0000000..3e89463 --- /dev/null +++ b/src/main/java/uk/org/iay/incommon/mda/dom/saml/shib/ShibbolethMetadataSupport.java @@ -0,0 +1,40 @@ +/* + * Licensed 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 uk.org.iay.incommon.mda.dom.saml.shib; + +import javax.annotation.concurrent.ThreadSafe; +import javax.xml.namespace.QName; + +/** Helper class for dealing with Shibboleth metadata. */ +@ThreadSafe +public final class ShibbolethMetadataSupport { + + /** Shibboleth metadata namespace URI. */ + public static final String SHIBMD_NS = "urn:mace:shibboleth:metadata:1.0"; + + /** Default Shibboleth metadata namespace prefix. */ + public static final String SHIBMD_PREFIX = "shibmd"; + + /** Scope element name. */ + public static final QName SCOPE_NAME = new QName(SHIBMD_NS, "Scope", SHIBMD_PREFIX); + + /** regexp attribute name. */ + public static final QName REGEXP_ATTRIB_NAME = new QName("regexp"); + + /** Constructor. */ + private ShibbolethMetadataSupport() { + + } +} diff --git a/src/main/java/uk/org/iay/incommon/mda/dom/saml/shib/package-info.java b/src/main/java/uk/org/iay/incommon/mda/dom/saml/shib/package-info.java new file mode 100644 index 0000000..cc74a6a --- /dev/null +++ b/src/main/java/uk/org/iay/incommon/mda/dom/saml/shib/package-info.java @@ -0,0 +1,18 @@ +/* + * Licensed 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 beans dealing with Shibboleth SAML metadata. + */ +package uk.org.iay.incommon.mda.dom.saml.shib; diff --git a/src/main/resources/uk/org/iay/incommon/mda/beans.xml b/src/main/resources/uk/org/iay/incommon/mda/beans.xml index 6939823..ebc34d3 100644 --- a/src/main/resources/uk/org/iay/incommon/mda/beans.xml +++ b/src/main/resources/uk/org/iay/incommon/mda/beans.xml @@ -31,6 +31,13 @@ + + + + diff --git a/src/test/java/uk/org/iay/incommon/mda/dom/saml/shib/ScopeValidationStageLitmusTest.java b/src/test/java/uk/org/iay/incommon/mda/dom/saml/shib/ScopeValidationStageLitmusTest.java new file mode 100644 index 0000000..e9d631e --- /dev/null +++ b/src/test/java/uk/org/iay/incommon/mda/dom/saml/shib/ScopeValidationStageLitmusTest.java @@ -0,0 +1,172 @@ + +package uk.org.iay.incommon.mda.dom.saml.shib; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +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.utilities.java.support.xml.AttributeSupport; +import net.shibboleth.utilities.java.support.xml.ElementSupport; + +/** + * A litmus test for {@link ScopeValidationStage} involving a set of valid and invalid + * scope values, both for regular expression and plain cases. + * + * The configuration for the stage is taken from a Spring XML configuration file. + */ +@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 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 makeStage() throws Exception { + @SuppressWarnings("unchecked") + final Stage stage = applicationContext.getBean("litmusTest", Stage.class); + stage.initialize(); + return stage; + } + + /** Build a shibmd:Scope {@link Element}. */ + private Element buildScope(final 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 shibmd:Scope {@link Element}. */ + private 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 runTest(final Item item) throws Exception { + final Collection> coll = new ArrayList<>(); + coll.add(item); + stage.execute(coll); + final List 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 item = new DOMElementItem(buildDocument(value, isRegex)); + final List errors = runTest(item); + Assert.assertEquals(errors.size(), 0, "expected no errors for '" + value + "'[" + isRegex + "]"); + } + + /** 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 item = new DOMElementItem(buildDocument(value, isRegex)); + final List 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"); + bad("", "empty"); + bad(" "); + bad(" "); + bad(" example.org", "white space"); + bad("example.org ", "white space"); + bad("EXAMPLE.ORG", "upper-case"); + 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"); + + 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"); + // bad literal tail: upper case + badRegexp("^([a-zA-Z0-9-]{1,63}\\.){0,2}vho\\.aaf\\.edu\\.AU$", "includes upper-case characters"); + + // 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$"); + + // eduGAIN examples + + } +} diff --git a/src/test/resources/uk/org/iay/incommon/mda/dom/saml/shib/ScopeValidationStageLitmusTest-config.xml b/src/test/resources/uk/org/iay/incommon/mda/dom/saml/shib/ScopeValidationStageLitmusTest-config.xml new file mode 100644 index 0000000..9588cdd --- /dev/null +++ b/src/test/resources/uk/org/iay/incommon/mda/dom/saml/shib/ScopeValidationStageLitmusTest-config.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +