diff --git a/backend/build.gradle b/backend/build.gradle index 037b8046f..f1464e63c 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -75,6 +75,8 @@ dependencies { //For easy data mocking capabilities compile 'net.andreinc.mockneat:mockneat:0.1.4' + compile 'org.codehaus.groovy:groovy-all:2.4.15' + //So it works on Java 9 without explicitly requiring to load that module (needed by Hibernate) runtimeOnly 'javax.xml.bind:jaxb-api:2.3.0' diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy new file mode 100644 index 000000000..da474a717 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -0,0 +1,138 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import com.google.common.base.Predicate; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import groovy.xml.DOMBuilder +import groovy.xml.MarkupBuilder; +import net.shibboleth.utilities.java.support.resolver.ResolverException; +import org.opensaml.saml.common.profile.logic.EntityIdPredicate; +import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver; +import org.opensaml.saml.metadata.resolver.MetadataResolver; +import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver; +import org.opensaml.saml.metadata.resolver.filter.MetadataFilter; +import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.w3c.dom.Document; + +public class JPAMetadataResolverServiceImpl implements MetadataResolverService { + private static final Logger logger = LoggerFactory.getLogger(JPAMetadataResolverServiceImpl.class); + + @Autowired + private MetadataResolver metadataResolver + + @Autowired + private MetadataResolverRepository metadataResolverRepository + + @Autowired + private OpenSamlObjects openSamlObjects + + // TODO: enhance + @Override + public void reloadFilters(String metadataResolverName) { + ChainingMetadataResolver chainingMetadataResolver = (ChainingMetadataResolver)metadataResolver; + + // MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().stream().filter(r -> r.getId().equals(metadataResolverName)).findFirst().get(); + MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { it.id == metadataResolverName } + edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByName(metadataResolverName); + + if (targetMetadataResolver && targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { + MetadataFilterChain metadataFilterChain = (MetadataFilterChain)targetMetadataResolver.getMetadataFilter(); + + List metadataFilters = new ArrayList<>(); + + for (edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter metadataFilter : jpaMetadataResolver.getMetadataFilters()) { + if (metadataFilter instanceof EntityAttributesFilter) { + EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter; + + org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter(); + Map, Collection> rules = new HashMap<>(); + if (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType() == EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY) { + rules.put( + new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), + (List)(List)entityAttributesFilter.getAttributes() + ); + } + target.setRules(rules); + metadataFilters.add(target); + } + } + metadataFilterChain.setFilters(metadataFilters); + } + + if (metadataResolver instanceof RefreshableMetadataResolver) { + try { + ((RefreshableMetadataResolver)metadataResolver).refresh(); + } catch (ResolverException e) { + logger.warn("error refreshing metadataResolver " + metadataResolverName, e); + } + } + } + + // TODO: enhance + @Override + public Document generateConfiguration() { + // TODO: this can probably be a better writer + new StringWriter().withCloseable { writer -> + def xml = new MarkupBuilder(writer) + + xml.MetadataProvider(id: 'ShibbolethMetadata', + xmlns: 'urn:mace:shibboleth:2.0:metadata', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:type': 'ChainingMetadataProvider', + 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' + ) { + metadataResolverRepository.findAll().each { edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver mr -> + MetadataProvider(id: 'HTTPMetadata', + 'xsi:type': 'FileBackedHTTPMetadataProvider', + backingFile: '%{idp.home}/metadata/incommonmd.xml', + metadataURL: 'http://md.incommon.org/InCommon/InCommon-metadata.xml', + minRefreshDelay: 'PT5M', + maxRefreshDelay: 'PT1H', + refreshDelayFactor: '0.75' + ) { + MetadataFilter( + 'xsi:type': 'SignatureValidation', + 'requireSignedRoot': 'true', + 'certificateFile': '%{idp.home}/credentials/inc-md-cert.pem' + ) + MetadataFilter( + 'xsi:type': 'RequiredValidUntil', + 'maxValidityInterval': 'P14D' + ) + MetadataFilter( + 'xsi:type': 'EntityRoleWhiteList' + ) { + RetainedRole('md:SPSSODescriptor') + } + //TODO: enhance + mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter filter -> + if (filter instanceof EntityAttributesFilter) { + EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter)filter + MetadataFilter('xsi:type': 'EntityAttributes') { + // TODO: enhance. currently this does weird things with namespaces + entityAttributesFilter.attributes.each { attribute -> + mkp.yieldUnescaped(openSamlObjects.marshalToXmlString(attribute, false)) + } + if (entityAttributesFilter.entityAttributesFilterTarget.entityAttributesFilterTargetType == EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY) { + entityAttributesFilter.entityAttributesFilterTarget.value.each { + Entity(it) + } + } + } + } + } + } + } + } + + return DOMBuilder.newInstance().parseText(writer.toString()) + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java index 21cc6262c..18f20e013 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java @@ -86,7 +86,7 @@ public void init() throws ComponentInitializationException { this.unmarshallerFactory = registry.getUnmarshallerFactory(); } - public String marshalToXmlString(XMLObject ed) throws MarshallingException { + public String marshalToXmlString(XMLObject ed, boolean includeXMLDeclaration) throws MarshallingException { Marshaller marshaller = this.marshallerFactory.getMarshaller(ed); String entityDescriptorXmlString = null; if (marshaller != null) { @@ -94,6 +94,9 @@ public String marshalToXmlString(XMLObject ed) throws MarshallingException { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + if (!includeXMLDeclaration) { + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + } transformer.transform(new DOMSource(marshaller.marshall(ed)), new StreamResult(writer)); entityDescriptorXmlString = writer.toString(); } catch (TransformerException | IOException e) { @@ -108,6 +111,10 @@ public String marshalToXmlString(XMLObject ed) throws MarshallingException { return entityDescriptorXmlString; } + public String marshalToXmlString(XMLObject ed) throws MarshallingException { + return this.marshalToXmlString(ed, true); + } + public EntityDescriptor unmarshalFromXml(byte[] entityDescriptorXml) throws Exception { try (InputStream edIs = ByteSource.wrap(entityDescriptorXml).openBufferedStream()) { Document doc = this.parserPool.parse(edIs); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.java deleted file mode 100644 index 8cc907d98..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.java +++ /dev/null @@ -1,78 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.service; - -import com.google.common.base.Predicate; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget; -import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; -import net.shibboleth.utilities.java.support.resolver.ResolverException; -import org.opensaml.saml.common.profile.logic.EntityIdPredicate; -import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver; -import org.opensaml.saml.metadata.resolver.MetadataResolver; -import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver; -import org.opensaml.saml.metadata.resolver.filter.MetadataFilter; -import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain; -import org.opensaml.saml.saml2.core.Attribute; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.w3c.dom.Document; - -import java.util.*; - -public class JPAMetadataResolverServiceImpl implements MetadataResolverService { - private static final Logger logger = LoggerFactory.getLogger(JPAMetadataResolverServiceImpl.class); - - @Autowired - private MetadataResolver metadataResolver; - - @Autowired - private MetadataResolverRepository metadataResolverRepository; - - // TODO: enhance - @Override - public void reloadFilters(String metadataResolverName) { - ChainingMetadataResolver chainingMetadataResolver = (ChainingMetadataResolver)metadataResolver; - - MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().stream().filter(r -> r.getId().equals(metadataResolverName)).findFirst().get(); - edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByName(metadataResolverName); - - if (targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { - MetadataFilterChain metadataFilterChain = (MetadataFilterChain)targetMetadataResolver.getMetadataFilter(); - - List metadataFilters = new ArrayList<>(); - - for (edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter metadataFilter : jpaMetadataResolver.getMetadataFilters()) { - if (metadataFilter instanceof EntityAttributesFilter) { - EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter; - - org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter(); - Map, Collection> rules = new HashMap<>(); - if (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType() == EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY) { - rules.put( - new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), - (List)(List)entityAttributesFilter.getAttributes() - ); - } - target.setRules(rules); - metadataFilters.add(target); - } - } - metadataFilterChain.setFilters(metadataFilters); - } - - if (metadataResolver instanceof RefreshableMetadataResolver) { - try { - ((RefreshableMetadataResolver)metadataResolver).refresh(); - } catch (ResolverException e) { - logger.warn("error refreshing metadataResolver " + metadataResolverName, e); - } - } - } - - @Override - public Document generateConfiguration() { - //TODO: implement - return null; - } -} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy index 1d54f7aa5..12112d0b9 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy @@ -2,8 +2,12 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget +import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import org.apache.http.impl.client.HttpClients import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver @@ -26,12 +30,18 @@ import spock.lang.Specification @ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class IncommonJPAMetadataResolverServiceImplTests extends Specification { @Autowired MetadataResolverService metadataResolverService - def 'test generation of metadata-providers.xml'() { + @Autowired + MetadataResolverRepository metadataResolverRepository + + @Autowired + AttributeUtility attributeUtility + + def 'simple test generation of metadata-providers.xml'() { when: def output = metadataResolverService.generateConfiguration() @@ -39,6 +49,33 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { assert !DiffBuilder.compare(Input.fromStream(this.class.getResourceAsStream('/conf/278.xml'))).withTest(Input.fromDocument(output)).ignoreComments().ignoreWhitespace().build().hasDifferences() } + def 'test generation of metadata-providers.xml with filters'() { + when: + //TODO: this might break later + def mr = metadataResolverRepository.findAll().iterator().next() + mr.metadataFilters.add(new EntityAttributesFilter().with { + it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { + it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY + it.value = ['https://sp1.example.org'] + it + } + def attribute = attributeUtility.createAttributeWithArbitraryValues('here', null, 'there') + attribute.nameFormat = null + attribute.namespacePrefix = 'saml' + attribute.attributeValues.each { val -> + val.namespacePrefix = 'saml' + } + it.attributes = [attribute] + it + }) + metadataResolverRepository.save(mr) + + def output = metadataResolverService.generateConfiguration() + + then: + assert !DiffBuilder.compare(Input.fromStream(this.class.getResourceAsStream('/conf/278.2.xml'))).withTest(Input.fromDocument(output)).ignoreComments().ignoreWhitespace().build().hasDifferences() + } + //TODO: check that this configuration is sufficient @TestConfiguration static class TestConfig { diff --git a/backend/src/test/resources/conf/278.2.xml b/backend/src/test/resources/conf/278.2.xml new file mode 100644 index 000000000..d8305ada3 --- /dev/null +++ b/backend/src/test/resources/conf/278.2.xml @@ -0,0 +1,32 @@ + + + + + + + + md:SPSSODescriptor + + + + there + + https://sp1.example.org + + + + diff --git a/backend/src/test/resources/conf/278.xml b/backend/src/test/resources/conf/278.xml index 721a2ac80..e6e0e88d0 100644 --- a/backend/src/test/resources/conf/278.xml +++ b/backend/src/test/resources/conf/278.xml @@ -6,14 +6,21 @@ xmlns:security="urn:mace:shibboleth:2.0:security" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd - urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd - urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd - urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd"> + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + xsi:schemaLocation="urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd"> + metadataURL="http://md.incommon.org/InCommon/InCommon-metadata.xml" + minRefreshDelay="PT5M" + maxRefreshDelay="PT1H" + refreshDelayFactor="0.75"> + + + + md:SPSSODescriptor +