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..8b8c27645 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 @@ -449,11 +449,11 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { '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' ) { - resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> - //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) - if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { + // We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) + // We do not want to include the custom type: ExternalMetadataResolver + if ((mr.type != 'BaseMetadataResolver') && (mr.type != 'ExternalMetadataResolver') && (mr.enabled)) { constructXmlNodeForResolver(mr, delegate) { //TODO: enhance def didNamespaceProtectionFilter = !(shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0) @@ -478,6 +478,44 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } + @Override + Document generateExternalMetadataFilterConfiguration() { + // TODO: this can probably be a better writer + new StringWriter().withCloseable { writer -> + def xml = new MarkupBuilder(writer) + xml.omitEmptyAttributes = true + xml.omitNullAttributes = true + + // CHARLESTODO - determine wrapping type here - possible: https://shibboleth.atlassian.net/wiki/spaces/IDP4/pages/1279033515/ByReferenceFilter + xml.MetadataProvider(id: 'ShibbolethIdPUIGeneratedMetadata', + 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' + ) { + + resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> + // Only include the custom type: ExternalMetadataResolver + if ((mr.type != 'ExternalMetadataResolver') && (mr.enabled)) { + constructXmlNodeForResolver(mr, delegate) { + //TODO: enhance + def didNamespaceProtectionFilter = !(shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0) + mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> + if (filter.isFilterEnabled()) { + doNamespaceProtectionFilter() + constructXmlNodeForFilter(filter, delegate) + } + } + doNamespaceProtectionFilter() + } + } + } + } + return DOMBuilder.newInstance().parseText(writer.toString()) + } + } + private String generateJavaScriptRegexScript(String regex) { return """ "use strict"; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index c897a31b5..fdc85e20f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; import edu.internet2.tier.shibboleth.admin.ui.scheduled.EntityDescriptorFilesScheduledTasks; +import edu.internet2.tier.shibboleth.admin.ui.scheduled.ExternalMetadataProvidersScheduledTasks; import edu.internet2.tier.shibboleth.admin.ui.scheduled.MetadataProvidersScheduledTasks; import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener; import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener; @@ -88,6 +89,14 @@ public MetadataProvidersScheduledTasks metadataProvidersScheduledTasks( return new MetadataProvidersScheduledTasks(resource, metadataResolverService, fileWritingService()); } + @Bean + @ConditionalOnProperty(name = "shibui.external.metadataProviders.target") + public ExternalMetadataProvidersScheduledTasks externalMetadataProvidersScheduledTasks( + @Value("${shibui.external.metadataProviders.target}") final Resource resource, + final MetadataResolverService metadataResolverService) { + return new ExternalMetadataProvidersScheduledTasks(resource, metadataResolverService, fileWritingService()); + } + @Bean public EntityIdsSearchService entityIdsSearchService(LuceneUtility luceneUtility, Analyzer fullTokenAnalyzer) { return new EntityIdsSearchServiceImpl(luceneUtility, fullTokenAnalyzer); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index 0f7b92338..080b8580a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -103,6 +103,20 @@ public ResponseEntity getXml() throws IOException, TransformerException { } } + @GetMapping(value = "/MetadataResolvers/External", produces = "application/xml") + @Transactional(readOnly = true) + public ResponseEntity getExternalXml() throws IOException, TransformerException { + // TODO: externalize + try (StringWriter writer = new StringWriter()) { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + transformer.transform(new DOMSource(metadataResolverService.generateExternalMetadataFilterConfiguration()), new StreamResult(writer)); + return ResponseEntity.ok(writer.toString()); + } + } + @GetMapping("/MetadataResolvers/{resourceId}") @Transactional(readOnly = true) public ResponseEntity getOne(@PathVariable String resourceId) { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ExternalMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ExternalMetadataResolver.java new file mode 100644 index 000000000..7c857aaa2 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ExternalMetadataResolver.java @@ -0,0 +1,4 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +public class ExternalMetadataResolver extends MetadataResolver { + } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/ExternalMetadataProvidersScheduledTasks.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/ExternalMetadataProvidersScheduledTasks.java new file mode 100644 index 000000000..b2e3c7d37 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/ExternalMetadataProvidersScheduledTasks.java @@ -0,0 +1,53 @@ +package edu.internet2.tier.shibboleth.admin.ui.scheduled; + +import edu.internet2.tier.shibboleth.admin.ui.service.FileWritingService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.WritableResource; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.annotation.Transactional; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.StringWriter; + +@Configuration +@ConditionalOnProperty("shibui.external.metadataProviders.target") +public class ExternalMetadataProvidersScheduledTasks { + private static final Logger logger = LoggerFactory.getLogger(ExternalMetadataProvidersScheduledTasks.class); + + private final Resource target; + private final MetadataResolverService metadataResolverService; + private final FileWritingService fileWritingService; + + public ExternalMetadataProvidersScheduledTasks(Resource target, MetadataResolverService metadataResolverService, FileWritingService fileWritingService) { + this.target = target; + this.metadataResolverService = metadataResolverService; + this.fileWritingService = fileWritingService; + } + + @Scheduled(fixedRateString = "${shibui.external.metadataProviders.taskRunRate:30000}") + @Transactional(readOnly = true) + public void generateMetadataProvidersFile() { + try (StringWriter os = new StringWriter()) { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + + transformer.transform(new DOMSource(metadataResolverService.generateExternalMetadataFilterConfiguration()), new StreamResult(os)); + this.fileWritingService.write((WritableResource)this.target, os.toString()); + } catch (IOException | TransformerException e) { + logger.error(e.getLocalizedMessage(), e); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java index 5fd205c20..d851cd021 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java @@ -7,6 +7,7 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; +import org.w3c.dom.Node; public interface MetadataResolverService { public MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException; @@ -16,4 +17,6 @@ public interface MetadataResolverService { public void reloadFilters(String metadataResolverName); public MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException; -} + + public Document generateExternalMetadataFilterConfiguration(); +} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 7943285f7..adcb452f0 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -80,6 +80,12 @@ shibui.nameid-filter-ui-schema-location=classpath:nameid-filter.schema.json # shibui.metadataProviders.target=file:/opt/shibboleth-idp/conf/shibui-metadata-providers.xml # shibui.metadataProviders.taskRunRate=30000 +# Set the following property to periodically write out external metadata providers (ie metadata-filters) configuration. +# There is no default value; the following is just an example +# @see - https://shibboleth.atlassian.net/wiki/spaces/IDP4/pages/1279033515/ByReferenceFilter +# shibui.external.metadataProviders.target=file:/opt/shibboleth-idp/conf/metadata-filters.xml +# shibui.external.metadataProviders.taskRunRate=30000 + # Email configuration (local mailhog) # spring.mail.host=mailhog # spring.mail.port=1025 diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy index c0a7d0e68..82bee21b2 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy @@ -96,6 +96,11 @@ class MetadataFiltersControllerTests extends AbstractBaseDataJpaTest { return null } + @Override + Document generateExternalMetadataFilterConfiguration() { + return null + } + @Override MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { // This won't get called