Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add ScopeValidationStage
iay committed Oct 11, 2017
1 parent bc2c180 commit 8001a01
Showing 7 changed files with 421 additions and 0 deletions.
12 changes: 12 additions & 0 deletions pom.xml
@@ -24,6 +24,7 @@
<mda.version>0.9.2</mda.version>
<ukf-mda.version>0.9.4</ukf-mda.version>
<java-support.version>7.2.0</java-support.version>
<spring-extensions.version>5.2.0</spring-extensions.version>
<checkstyle.version>7.6</checkstyle.version>
</properties>

@@ -56,6 +57,12 @@

<!-- Test dependencies -->

<dependency>
<groupId>net.shibboleth.ext</groupId>
<artifactId>spring-extensions</artifactId>
<version>${spring-extensions.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>uk.org.ukfederation</groupId>
<artifactId>ukf-mda</artifactId>
@@ -78,6 +85,11 @@
<artifactId>spring-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${spring.groupId}</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>

<!-- Managed Dependencies -->

@@ -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 <code>shibmd:Scope</code>
* values.
*
* A separate collection of validators is used for the case of the <code>regexp</code>
* attribute being <code>true</code> and <code>false</code>.
*/
public class ScopeValidationStage extends AbstractDOMValidationStage<String> {

/** The sequence of validators to apply to <code>regexp</code> scopes. */
@Nonnull
private 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 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 List<Validator<String>> 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();
}

}
@@ -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() {

}
}
@@ -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;
7 changes: 7 additions & 0 deletions src/main/resources/uk/org/iay/incommon/mda/beans.xml
@@ -31,6 +31,13 @@
<bean id="inc.InCommonEntityOrderingStrategy" abstract="true"
class="uk.org.iay.incommon.mda.dom.saml.InCommonEntityOrderingStrategy"/>

<!--
Stage beans.
-->

<bean id="inc.ScopeValidationStage" abstract="true" parent="inc.stage_parent"
class="uk.org.iay.incommon.mda.dom.saml.shib.ScopeValidationStage"/>

<!--
Validator beans.
-->
@@ -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<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 {
@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 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 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 Collection<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);
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<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");
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

}
}
@@ -0,0 +1,75 @@
<?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:uk/org/iay/incommon/mda/beans.xml"/>

<bean class="net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor"/>

<bean id="litmusTest" parent="inc.stage_parent"
class="uk.org.iay.incommon.mda.dom.saml.shib.ScopeValidationStage">
<property name="validators">
<list>
<bean parent="inc.RejectStringRegexValidator"
p:regex="" p:message="scope element must not be empty"/>
<bean parent="inc.RejectStringRegexValidator"
p:regex=".*\s.*" p:message="scope '%s' includes white space"/>
<bean parent="inc.RejectStringRegexValidator"
p:regex=".*\p{Upper}.*" p:message="scope '%s' includes upper-case characters"/>
<bean parent="inc.AsDomainNameStringValidator"
p:message="scope is not a valid domain name: %s">
<property name="validators">
<list>
<!-- DNS name validators -->
<bean parent="inc.RejectDomainNamePublicSuffixValidator"
p:message="scope is a public suffix: '%s'"/>
<bean parent="inc.RejectDomainNameNotUnderPublicSuffixValidator"
p:message="scope is not under a public suffix: '%s'"/>
</list>
</property>
</bean>
</list>
</property>
<property name="regexpValidators">
<list>
<bean parent="inc.RejectStringRegexValidator"
p:regex="" p:message="scope element must not be empty"/>
<bean parent="inc.RejectStringRegexValidator"
p:regex=".*\s.*" p:message="scope '%s' includes white space"/>
<bean parent="inc.RejectStringRegexValidator"
p:regex="[^^].*" p:message="regex scope '%s' does not start with an anchor ('^')"/>
<bean parent="inc.RejectStringRegexValidator"
p:regex=".*[^$]" p:message="regex scope '%s' does not end with an anchor ('$')"/>
<bean parent="inc.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="inc.RejectStringRegexValidator"
p:regex=".*\p{Upper}.*" p:message="literal tail '%s' includes upper-case characters"/>
<bean parent="inc.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="inc.RejectDomainNamePublicSuffixValidator"
p:message="literal tail is a public suffix: '%s'"/>
<bean parent="inc.RejectDomainNameNotUnderPublicSuffixValidator"
p:message="literal tail is not under a public suffix: '%s'"/>
</list>
</property>
</bean>
</list>
</property>
</bean>
</list>
</property>
</bean>

</beans>

0 comments on commit 8001a01

Please sign in to comment.