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 index ed3f794c0..691828526 100644 --- 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 @@ -9,6 +9,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteList import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.algorithm.AlgorithmFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.opensaml.OpenSamlNameIdFormatFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver @@ -88,6 +89,16 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } + void constructXmlNodeForFilter(AlgorithmFilter filter, def markupBuilderDelegate) { + if (!filter.isFilterEnabled()) { return } + markupBuilderDelegate.MetadataFilter('xsi:type': 'Algorithm') { + // TODO: enhance. currently this does weird things with namespaces + filter.unknownXMLObjects.each { xmlObject -> + mkp.yieldUnescaped(openSamlObjects.marshalToXmlString(xmlObject, false)) + } + } + } + void constructXmlNodeForFilter(EntityAttributesFilter filter, def markupBuilderDelegate) { if (!filter.isFilterEnabled()) { return } markupBuilderDelegate.MetadataFilter('xsi:type': 'EntityAttributes') { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/algorithm/AlgorithmFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/algorithm/AlgorithmFilter.java new file mode 100644 index 000000000..85a4d9129 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/algorithm/AlgorithmFilter.java @@ -0,0 +1,59 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.filters.algorithm; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractXMLObject; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.envers.Audited; +import org.opensaml.core.xml.ElementExtensibleXMLObject; +import org.opensaml.core.xml.XMLObject; + +import javax.annotation.Nonnull; +import javax.persistence.CascadeType; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.persistence.OrderColumn; +import javax.xml.namespace.QName; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Following the pattern of AbstractElementExtensibleXMLObject - this XML type can hold a couple of different types of XML objects + */ +@Entity +@Audited +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = true) +public class AlgorithmFilter extends MetadataFilter { + @OneToMany(cascade = CascadeType.ALL) + @OrderColumn + private List unknownXMLObjects = new ArrayList<>(); + + public void addUnknownXMLObject(AbstractXMLObject xmlObject) { + this.unknownXMLObjects.add(xmlObject); + } + + @Nonnull + public List getUnknownXMLObjects() { + return (List) (List) this.unknownXMLObjects; + } + + private AlgorithmFilter updateConcreteFilterTypeData(AlgorithmFilter filterToBeUpdated) { + for (XMLObject o : getUnknownXMLObjects()) { + filterToBeUpdated.addUnknownXMLObject((AbstractXMLObject) o); + } + return filterToBeUpdated; + } + + @Override + public MetadataFilter updateConcreteFilterTypeData(MetadataFilter filterToBeUpdated) { + return updateConcreteFilterTypeData((AlgorithmFilter) filterToBeUpdated); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/algorithm/Entity.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/algorithm/Entity.java new file mode 100644 index 000000000..9a741f6e8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/algorithm/Entity.java @@ -0,0 +1,24 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.filters.algorithm; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractXMLObject; + +import javax.annotation.Nullable; + +public class Entity extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSString { + private String uri; + + private Entity(){ + setElementLocalName("Entity"); + } + + @Nullable + @Override + public String getValue() { + return this.uri; + } + + @Override + public void setValue(@Nullable String newValue) { + this.uri = newValue; + } +} \ No newline at end of file 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 8a25b0855..bd149c9ff 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 @@ -88,11 +88,11 @@ public void init() throws ComponentInitializationException { this.unmarshallerFactory = registry.getUnmarshallerFactory(); } - public String marshalToXmlString(XMLObject ed, boolean includeXMLDeclaration) throws MarshallingException { - Marshaller marshaller = this.marshallerFactory.getMarshaller(ed); - ed.releaseDOM(); - ed.releaseChildrenDOM(true); - String entityDescriptorXmlString = null; + public String marshalToXmlString(XMLObject xmlObject, boolean includeXMLDeclaration) throws MarshallingException { + Marshaller marshaller = this.marshallerFactory.getMarshaller(xmlObject); + xmlObject.releaseDOM(); + xmlObject.releaseChildrenDOM(true); + String resultString = null; if (marshaller != null) { try (StringWriter writer = new StringWriter()) { Transformer transformer = TransformerFactory.newInstance().newTransformer(); @@ -101,22 +101,22 @@ public String marshalToXmlString(XMLObject ed, boolean includeXMLDeclaration) th if (!includeXMLDeclaration) { transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); } - transformer.transform(new DOMSource(marshaller.marshall(ed)), new StreamResult(writer)); - entityDescriptorXmlString = writer.toString(); + transformer.transform(new DOMSource(marshaller.marshall(xmlObject)), new StreamResult(writer)); + resultString = writer.toString(); } catch (TransformerException | IOException e) { logger.error(e.getMessage(), e); } } - if (entityDescriptorXmlString == null) { + if (resultString == null) { //Figure out the best way to deal with this case - throw new RuntimeException("Unable to marshal EntityDescriptor"); + throw new RuntimeException("Unable to marshal xmlObject"); } - return entityDescriptorXmlString; + return resultString; } - public String marshalToXmlString(XMLObject ed) throws MarshallingException { - return this.marshalToXmlString(ed, true); + public String marshalToXmlString(XMLObject xmlObject) throws MarshallingException { + return this.marshalToXmlString(xmlObject, true); } public EntityDescriptor unmarshalFromXml(byte[] entityDescriptorXml) throws Exception { @@ -152,4 +152,4 @@ public T buildDefaultInstanceOfType(Class type) { throw new RuntimeException("there was a problem building an instance", e); } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializerForTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializerForTest.groovy new file mode 100644 index 000000000..0a56abe54 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/opensaml/config/JPAXMLObjectProviderInitializerForTest.groovy @@ -0,0 +1,12 @@ +package edu.internet2.tier.shibboleth.admin.ui.opensaml.config + +import org.opensaml.core.xml.config.AbstractXMLObjectProviderInitializer + +class JPAXMLObjectProviderInitializerForTest extends AbstractXMLObjectProviderInitializer { + @Override + protected String[] getConfigResources() { + return new String[]{ + "/jpa-saml2-metadata-config.xml", + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy index 594ee6750..010ee9310 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy @@ -3,10 +3,12 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.EncryptionMethod import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.algorithm.Entity import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver @@ -16,6 +18,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.SvnMetadataResour import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.opensaml.config.JPAXMLObjectProviderInitializerForTest import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import groovy.xml.DOMBuilder @@ -40,7 +43,7 @@ import java.time.Instant import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedXmlIsTheSameAsExpectedXml -@ContextConfiguration(classes=[ JPAMRSIConfig, PlaceholderResolverComponentsConfiguration ]) +@ContextConfiguration(classes=[ JPAMRSIConfig, PlaceholderResolverComponentsConfiguration, JPAXMLObjectProviderInitializerForTest ]) class JPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTest { @Autowired @@ -137,6 +140,27 @@ class JPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTest { !diff.hasDifferences() } + def 'test generating AlgorithmFilter xml snippet'() { + given: + def filter = TestObjectGenerator.algorithmFilter() + EncryptionMethod encryptionMethod = new EncryptionMethod() + encryptionMethod.setAlgorithm("http://www.w3.org/2001/04/xmlenc#aes128-cbc") + encryptionMethod.setElementLocalName("EncryptionMethod") + filter.addUnknownXMLObject(encryptionMethod) + Entity entity = new Entity() + entity.setValue("https://broken.example.org/sp") + filter.addUnknownXMLObject(entity) + Entity entity2 = new Entity() + entity2.setValue("https://also-broken.example.org/sp") + filter.addUnknownXMLObject(entity2) + + when: + genXmlSnippet(markupBuilder) { JPAMetadataResolverServiceImpl.cast(metadataResolverService).constructXmlNodeForFilter(filter, it) } + + then: + generatedXmlIsTheSameAsExpectedXml('/conf/2268-simple.xml', domBuilder.parseText(writer.toString())) + } + def 'test generating EntityAttributesFilter xml snippet with condition script'() { given: def filter = testObjectGenerator.entityAttributesFilterWithConditionScript() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy index bf17b107e..276bbc17f 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy @@ -17,6 +17,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.algorithm.AlgorithmFilter import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterTargetRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource @@ -183,6 +184,14 @@ class TestObjectGenerator { randomFilter } + static AlgorithmFilter algorithmFilter() { + return new AlgorithmFilter().with { + it.name = "Algorithm" + it.enabled = true; + it + } + } + SignatureValidationFilter signatureValidationFilter() { new SignatureValidationFilter().with { it.name = 'SignatureValidation' diff --git a/backend/src/test/resources/META-INF/services/org.opensaml.core.config.Initializer b/backend/src/test/resources/META-INF/services/org.opensaml.core.config.Initializer new file mode 100644 index 000000000..c7c2e7cb3 --- /dev/null +++ b/backend/src/test/resources/META-INF/services/org.opensaml.core.config.Initializer @@ -0,0 +1 @@ +edu.internet2.tier.shibboleth.admin.ui.opensaml.config.JPAXMLObjectProviderInitializerForTest \ No newline at end of file diff --git a/backend/src/test/resources/conf/2268-simple.xml b/backend/src/test/resources/conf/2268-simple.xml new file mode 100644 index 000000000..2ffe731f1 --- /dev/null +++ b/backend/src/test/resources/conf/2268-simple.xml @@ -0,0 +1,7 @@ + + + + https://broken.example.org/sp + https://also-broken.example.org/sp + + \ No newline at end of file diff --git a/backend/src/test/resources/jpa-saml2-metadata-config.xml b/backend/src/test/resources/jpa-saml2-metadata-config.xml new file mode 100644 index 000000000..b008809ce --- /dev/null +++ b/backend/src/test/resources/jpa-saml2-metadata-config.xml @@ -0,0 +1,372 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file