diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..73ca4bfdf --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,52 @@ +pipeline { + agent any + options { + disableConcurrentBuilds() + buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '10')) + } + stages { + stage('Build') { + steps { + sh './gradlew clean build' + } + post { + always { + junit 'backend/build/test-results/**/*.xml' + jacoco execPattern: '**/build/jacoco/test.exec' + } + } + } + stage('Build Docker images') { + when { + expression { + return GIT_BRANCH in ['master'] + } + } + steps { + sh '''./gradlew docker + ''' + } + } + stage('Deploy') { + when { + expression { + return GIT_BRANCH in ['master'] + } + } + steps { + sh ''' + docker stop shibui || true && docker rm shibui || true + docker run -d --restart always --name shibui -p 8080:8080 -v /etc/shibui/application.properties:/application.properties unicon/shibui:latest + ''' + } + } + } + post { + failure { + step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: emailextrecipients([[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']])]) + } + success { + emailext body: '''${SCRIPT, template="groovy-text.template"}''', recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']], subject: '[SHIBUI] Build Success' + } + } +} \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 000000000..cec9c4c44 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM gcr.io/distroless/java + +ARG JAR_FILE + +COPY ${JAR_FILE} app.jar + +EXPOSE 8080 + +CMD ["app.jar"] \ No newline at end of file diff --git a/backend/build.gradle b/backend/build.gradle index e6cf12818..bf62e58c7 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -5,6 +5,7 @@ plugins { id 'com.gorylenko.gradle-git-properties' version '1.4.21' id 'net.researchgate.release' version '2.6.0' id 'io.franzbecker.gradle-lombok' version '1.13' + id 'com.palantir.docker' version '0.20.1' } apply plugin: 'io.spring.dependency-management' @@ -208,3 +209,13 @@ jacocoTestReport { html.destination = file("${buildDir}/jacocoHtml") } } + +tasks.docker.dependsOn tasks.build +docker { + name 'unicon/shibui' + tags 'latest' + pull true + noCache true + files tasks.bootWar.outputs + buildArgs(['JAR_FILE': 'shibui.war']) +} \ No newline at end of file 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 a4a8451fd..f8eb32a38 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 @@ -11,18 +11,16 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMet import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.Refilterable import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import groovy.util.logging.Slf4j import groovy.xml.DOMBuilder import groovy.xml.MarkupBuilder -import net.shibboleth.utilities.java.support.logic.ScriptedPredicate -import net.shibboleth.utilities.java.support.resolver.ResolverException import net.shibboleth.utilities.java.support.scripting.EvaluableScript 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 @@ -52,10 +50,10 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { // TODO: enhance @Override - void reloadFilters(String metadataResolverName) { - ChainingMetadataResolver chainingMetadataResolver = (ChainingMetadataResolver) metadataResolver - MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { it.id == metadataResolverName } - edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByName(metadataResolverName) + void reloadFilters(String metadataResolverResourceId) { + OpenSamlChainingMetadataResolver chainingMetadataResolver = (OpenSamlChainingMetadataResolver) metadataResolver + MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { it.id == metadataResolverResourceId } + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByResourceId(metadataResolverResourceId) if (targetMetadataResolver && targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { MetadataFilterChain metadataFilterChain = (MetadataFilterChain) targetMetadataResolver.getMetadataFilter() @@ -94,12 +92,11 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { metadataFilterChain.setFilters(metadataFilters) } - if (metadataResolver instanceof RefreshableMetadataResolver) { - try { - ((RefreshableMetadataResolver) metadataResolver).refresh() - } catch (ResolverException e) { - log.warn("error refreshing metadataResolver " + metadataResolverName, e) - } + if (targetMetadataResolver != null && targetMetadataResolver instanceof Refilterable) { + (Refilterable) targetMetadataResolver.refilter() + } else { + //TODO: Do something here if we need to refilter other non-Batch resolvers + log.warn("Target resolver is not a Refilterable resolver. Skipping refilter()") } } 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 c9b6493fa..e17c33b0e 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 @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; +import com.fasterxml.jackson.databind.Module; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; @@ -168,4 +169,9 @@ public DirectoryService directoryService() { public LuceneUtility luceneUtility(DirectoryService directoryService) { return new LuceneUtility(directoryService); } + + @Bean + public Module stringTrimModule() { + return new StringTrimModule(); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java new file mode 100644 index 000000000..6380e0018 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java @@ -0,0 +1,17 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterServiceImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Configuration +public class MetadataResolverConverterConfiguration { + @Bean + public MetadataResolverConverterService metadataResolverConverterService() { + return new MetadataResolverConverterServiceImpl(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/PlaceholderResolverComponentsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/PlaceholderResolverComponentsConfiguration.java new file mode 100644 index 000000000..bd391e0d5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/PlaceholderResolverComponentsConfiguration.java @@ -0,0 +1,21 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import edu.internet2.tier.shibboleth.admin.ui.service.TokenPlaceholderValueResolvingService; +import edu.internet2.tier.shibboleth.admin.util.TokenPlaceholderResolvers; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; + +@Configuration +public class PlaceholderResolverComponentsConfiguration { + + @Bean + public TokenPlaceholderValueResolvingService tokenPlaceholderValueResolvingService(ConfigurableEnvironment env) { + return TokenPlaceholderValueResolvingService.shibbolethPlaceholderPrefixAware(env.getPropertySources()); + } + + @Bean + public TokenPlaceholderResolvers tokenPlaceholderResolvers(TokenPlaceholderValueResolvingService service) { + return new TokenPlaceholderResolvers(service); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/StringTrimModule.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/StringTrimModule.java new file mode 100644 index 000000000..8c04ab071 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/StringTrimModule.java @@ -0,0 +1,26 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import java.io.IOException; + +/** + * @author Bill Smith (wsmith@unicon.net) + * + * Adapted from Maciej Marczuk's answer on Stack Overflow here: + * https://stackoverflow.com/questions/6852213/can-jackson-be-configured-to-trim-leading-trailing-whitespace-from-all-string-pr/33765854#33765854 + */ +public class StringTrimModule extends SimpleModule { + + public StringTrimModule() { + addDeserializer(String.class, new StdScalarDeserializer(String.class) { + @Override + public String deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException { + return jsonParser.getValueAsString().trim(); + } + }); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/postprocessors/IdpHomeValueSettingEnvironmentPostProcessor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/postprocessors/IdpHomeValueSettingEnvironmentPostProcessor.java new file mode 100644 index 000000000..8f8bd3499 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/postprocessors/IdpHomeValueSettingEnvironmentPostProcessor.java @@ -0,0 +1,49 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration.postprocessors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +/** + * Spring Boot Environment Post Processor setting the value for idp.home property to a temp directory + * if no IDP_HOME environment variable has been set already. + * + * @author Dmitriy Kopylenko + */ +public class IdpHomeValueSettingEnvironmentPostProcessor implements EnvironmentPostProcessor { + + private static final String IDP_HOME_PROP = "idp.home"; + + private static final String METADATA_DIR = "metadata"; + + private static final Logger LOGGER = LoggerFactory.getLogger(IdpHomeValueSettingEnvironmentPostProcessor.class); + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if(environment.getProperty(IDP_HOME_PROP) == null) { + Map map = new HashMap<>(1); + try { + Path tempDir = Files.createTempDirectory(null); + tempDir.toFile().deleteOnExit(); + String tempDirName = tempDir.toAbsolutePath().toString(); + Path tempMetadataSubDir = Paths.get(tempDirName, METADATA_DIR); + Files.createDirectories(tempMetadataSubDir); + map.put(IDP_HOME_PROP, tempDirName); + } catch (IOException e) { + LOGGER.error(e.getMessage()); + throw new RuntimeException(e); + } + environment.getPropertySources().addLast(new MapPropertySource("idp.home.propertysource", map)); + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java index 632809bea..431632023 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java @@ -88,8 +88,7 @@ public ResponseEntity create(@PathVariable String metadataResolverId, @Reques MetadataResolver persistedMr = repository.save(metadataResolver); // we reload the filters here after save - metadataResolverService.reloadFilters(persistedMr.getName()); - refreshOrInitResolver(metadataResolver); + metadataResolverService.reloadFilters(persistedMr.getResourceId()); MetadataFilter persistedFilter = newlyPersistedFilter(persistedMr.getMetadataFilters().stream(), createdFilter.getResourceId()); @@ -98,33 +97,6 @@ public ResponseEntity create(@PathVariable String metadataResolverId, @Reques .body(persistedFilter); } - private void refreshOrInitResolver(MetadataResolver resolver) { - List resolvers = ((ChainingMetadataResolver) chainingMetadataResolver).getResolvers(); - resolvers.stream().filter(it -> it.getId().equals(resolver.getResourceId())).forEach(it -> { - if (it instanceof RefreshableMetadataResolver) { - try { - ((RefreshableMetadataResolver) it).refresh(); - } catch (ResolverException e) { - //TODO what should we do if we can't refresh? - } - } else if (it instanceof OpenSamlFunctionDrivenDynamicHTTPMetadataResolver) { - try { - ((OpenSamlFunctionDrivenDynamicHTTPMetadataResolver) it).refresh(); - } catch (ComponentInitializationException e) { - //TODO what should we do if we can't refresh? - } - } else if (it instanceof OpenSamlLocalDynamicMetadataResolver) { - try { - ((OpenSamlLocalDynamicMetadataResolver) it).refresh(); - } catch (ComponentInitializationException e) { - //TODO what should we do if we can't refresh? - } - } else { - //TODO we shouldn't get here, but if we do... throw exception? - } - }); - } - @PutMapping("/Filters/{resourceId}") public ResponseEntity update(@PathVariable String metadataResolverId, @PathVariable String resourceId, @@ -158,8 +130,7 @@ public ResponseEntity update(@PathVariable String metadataResolverId, MetadataFilter persistedFilter = filterRepository.save(filterTobeUpdated); // TODO: this is wrong - metadataResolverService.reloadFilters(metadataResolver.getName()); - refreshOrInitResolver(metadataResolver); + metadataResolverService.reloadFilters(metadataResolver.getResourceId()); return ResponseEntity.ok().body(persistedFilter); } 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 4d7d7aaac..47458273d 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 @@ -3,15 +3,16 @@ import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; +import edu.internet2.tier.shibboleth.admin.util.OpenSamlChainingMetadataResolverUtil; import lombok.extern.slf4j.Slf4j; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.resolver.ResolverException; -import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -36,7 +37,6 @@ import java.io.IOException; import java.io.StringWriter; import java.net.URI; -import java.util.ArrayList; import java.util.List; import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult; @@ -118,19 +118,14 @@ public ResponseEntity create(@RequestBody MetadataResolver newResolver) throw MetadataResolver persistedResolver = resolverRepository.save(newResolver); positionOrderContainerService.appendPositionOrderForNew(persistedResolver); - updateChainingMetadataResolver(persistedResolver); + //TODO: currently, the update call might explode, but the save works.. in which case, the UI never gets + // n valid response. This operation is not atomic. Should we return an error here? + org.opensaml.saml.metadata.resolver.MetadataResolver openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(persistedResolver); + OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); return ResponseEntity.created(getResourceUriFor(persistedResolver)).body(persistedResolver); } - private void updateChainingMetadataResolver(MetadataResolver persistedResolver) throws IOException, ResolverException, ComponentInitializationException { - org.opensaml.saml.metadata.resolver.MetadataResolver openSamlResolver = metadataResolverConverterService.convertToOpenSamlRepresentation(persistedResolver); - List resolverList = new ArrayList<>(((ChainingMetadataResolver) chainingMetadataResolver).getResolvers()); - resolverList.removeIf(resolver -> resolver.getId().equals(persistedResolver.getResourceId())); - resolverList.add(openSamlResolver); - ((ChainingMetadataResolver) chainingMetadataResolver).setResolvers(resolverList); - } - @PutMapping("/MetadataResolvers/{resourceId}") @Transactional public ResponseEntity update(@PathVariable String resourceId, @RequestBody MetadataResolver updatedResolver) throws IOException, ResolverException, ComponentInitializationException { @@ -153,7 +148,8 @@ public ResponseEntity update(@PathVariable String resourceId, @RequestBody Me MetadataResolver persistedResolver = resolverRepository.save(updatedResolver); - updateChainingMetadataResolver(persistedResolver); + org.opensaml.saml.metadata.resolver.MetadataResolver openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(persistedResolver); + OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); return ResponseEntity.ok(persistedResolver); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlChainingMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlChainingMetadataResolver.java index cfea6d89c..3213736a6 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlChainingMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlChainingMetadataResolver.java @@ -29,7 +29,7 @@ public class OpenSamlChainingMetadataResolver extends ChainingMetadataResolver { @Nonnull @NonnullElements private List mutableResolvers; public OpenSamlChainingMetadataResolver() { - this.mutableResolvers = Collections.emptyList(); + this.mutableResolvers = new ArrayList<>(); } public OpenSamlChainingMetadataResolver(@Nonnull List mutableResolvers) { @@ -39,7 +39,7 @@ public OpenSamlChainingMetadataResolver(@Nonnull List mutableR @Override public void setResolvers(@Nonnull @NonnullElements final List newResolvers) { if (newResolvers == null || newResolvers.isEmpty()) { - mutableResolvers = Collections.emptyList(); + mutableResolvers = new ArrayList<>(); return; } @@ -78,7 +78,7 @@ protected void doInitialize() throws ComponentInitializationException { super.doInitialize(); if (mutableResolvers == null) { log.warn("OpenSamlChainingMetadataResolver was not configured with any member MetadataResolvers"); - mutableResolvers = Collections.emptyList(); + mutableResolvers = new ArrayList<>(); } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFileBackedHTTPMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFileBackedHTTPMetadataResolver.java index 5f4c10905..f1e551b8e 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFileBackedHTTPMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFileBackedHTTPMetadataResolver.java @@ -8,16 +8,24 @@ import org.apache.http.impl.client.HttpClients; import org.apache.lucene.index.IndexWriter; import org.joda.time.DateTime; +import org.opensaml.saml.metadata.resolver.filter.FilterException; +import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain; import org.opensaml.saml.metadata.resolver.impl.FileBackedHTTPMetadataResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import static edu.internet2.tier.shibboleth.admin.util.DurationUtility.toMillis; +import static edu.internet2.tier.shibboleth.admin.util.TokenPlaceholderResolvers.placeholderResolverService; /** * @author Bill Smith (wsmith@unicon.net) */ -public class OpenSamlFileBackedHTTPMetadataResolver extends FileBackedHTTPMetadataResolver { +public class OpenSamlFileBackedHTTPMetadataResolver extends FileBackedHTTPMetadataResolver implements Refilterable { + + private static final Logger logger = LoggerFactory.getLogger(OpenSamlFileBackedHTTPMetadataResolver.class); + private IndexWriter indexWriter; private FileBackedHttpMetadataResolver sourceResolver; @@ -38,10 +46,15 @@ public OpenSamlFileBackedHTTPMetadataResolver(ParserPool parserPool, OpenSamlMetadataResolverConstructorHelper.updateOpenSamlMetadataResolverFromReloadableMetadataResolverAttributes( this, sourceResolver.getReloadableMetadataResolverAttributes(), parserPool); - this.setBackupFile(sourceResolver.getBackingFile()); - this.setBackupFileInitNextRefreshDelay(toMillis(sourceResolver.getBackupFileInitNextRefreshDelay())); + this.setBackupFile(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(sourceResolver.getBackingFile())); + this.setBackupFileInitNextRefreshDelay(toMillis(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(sourceResolver.getBackupFileInitNextRefreshDelay()))); + this.setInitializeFromBackupFile(sourceResolver.getInitializeFromBackupFile()); + this.setMetadataFilter(new MetadataFilterChain()); + //TODO: Where does this get set in OpenSAML land? // sourceResolver.getMetadataURL(); } @@ -63,8 +76,20 @@ protected void processConditionalRetrievalHeaders(HttpResponse response) { protected void initMetadataResolver() throws ComponentInitializationException { super.initMetadataResolver(); + delegate.addIndexedDescriptorsFromBackingStore(this.getBackingStore(), this.sourceResolver.getResourceId(), indexWriter); } + + /** + * {@inheritDoc} + */ + public void refilter() { + try { + this.getBackingStore().setCachedFilteredMetadata(filterMetadata(getCachedOriginalMetadata())); + } catch (FilterException e) { + logger.error("An error occurred while attempting to filter metadata!", e); + } + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFilesystemMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFilesystemMetadataResolver.java index ad3ee65d9..b4fb6d578 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFilesystemMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlFilesystemMetadataResolver.java @@ -5,7 +5,11 @@ import net.shibboleth.utilities.java.support.xml.ParserPool; import org.apache.lucene.index.IndexWriter; import org.joda.time.DateTime; +import org.opensaml.saml.metadata.resolver.filter.FilterException; +import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain; import org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.File; @@ -13,7 +17,10 @@ /** * @author Bill Smith (wsmith@unicon.net) */ -public class OpenSamlFilesystemMetadataResolver extends FilesystemMetadataResolver { +public class OpenSamlFilesystemMetadataResolver extends FilesystemMetadataResolver implements Refilterable { + + private static final Logger logger = LoggerFactory.getLogger(OpenSamlFilesystemMetadataResolver.class); + private IndexWriter indexWriter; private edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver sourceResolver; private OpenSamlMetadataResolverDelegate delegate; @@ -31,6 +38,8 @@ public OpenSamlFilesystemMetadataResolver(ParserPool parserPool, OpenSamlMetadataResolverConstructorHelper.updateOpenSamlMetadataResolverFromReloadableMetadataResolverAttributes( this, sourceResolver.getReloadableMetadataResolverAttributes(), parserPool); + + this.setMetadataFilter(new MetadataFilterChain()); } // TODO: this is still probably not the best way to do this? @@ -48,4 +57,15 @@ protected void initMetadataResolver() throws ComponentInitializationException { this.sourceResolver.getResourceId(), indexWriter); } + + /** + * {@inheritDoc} + */ + public void refilter() { + try { + this.getBackingStore().setCachedFilteredMetadata(filterMetadata(getCachedOriginalMetadata())); + } catch (FilterException e) { + logger.error("An error occurred while attempting to filter metadata!", e); + } + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverConstructorHelper.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverConstructorHelper.java index c5a6845e4..d52e47448 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverConstructorHelper.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverConstructorHelper.java @@ -9,6 +9,7 @@ import org.opensaml.saml.metadata.resolver.impl.AbstractReloadingMetadataResolver; import static edu.internet2.tier.shibboleth.admin.util.DurationUtility.toMillis; +import static edu.internet2.tier.shibboleth.admin.util.TokenPlaceholderResolvers.placeholderResolverService; /** * @author Bill Smith (wsmith@unicon.net) @@ -21,11 +22,14 @@ public static void updateOpenSamlMetadataResolverFromDynamicMetadataResolverAttr AbstractDynamicMetadataResolver dynamicMetadataResolver = (AbstractDynamicMetadataResolver) metadataResolver; if (attributes.getBackgroundInitializationFromCacheDelay() != null) { - dynamicMetadataResolver.setBackgroundInitializationFromCacheDelay(toMillis(attributes.getBackgroundInitializationFromCacheDelay())); + dynamicMetadataResolver + .setBackgroundInitializationFromCacheDelay(toMillis(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(attributes.getBackgroundInitializationFromCacheDelay()))); } if (attributes.getCleanupTaskInterval() != null) { - dynamicMetadataResolver.setCleanupTaskInterval(toMillis(attributes.getCleanupTaskInterval())); + dynamicMetadataResolver.setCleanupTaskInterval(toMillis(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(attributes.getCleanupTaskInterval()))); } if (attributes.getInitializeFromPersistentCacheInBackground()) { @@ -33,19 +37,23 @@ public static void updateOpenSamlMetadataResolverFromDynamicMetadataResolverAttr } if (attributes.getMaxCacheDuration() != null) { - dynamicMetadataResolver.setMaxCacheDuration(toMillis(attributes.getMaxCacheDuration())); + dynamicMetadataResolver.setMaxCacheDuration(toMillis(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(attributes.getMaxCacheDuration()))); } if (attributes.getMaxIdleEntityData() != null) { - dynamicMetadataResolver.setMaxIdleEntityData(toMillis(attributes.getMaxIdleEntityData())); + dynamicMetadataResolver.setMaxIdleEntityData(toMillis(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(attributes.getMaxIdleEntityData()))); } if (attributes.getMinCacheDuration() != null) { - dynamicMetadataResolver.setMinCacheDuration(toMillis(attributes.getMinCacheDuration())); + dynamicMetadataResolver.setMinCacheDuration(toMillis(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(attributes.getMinCacheDuration()))); } if (attributes.getBackgroundInitializationFromCacheDelay() != null) { - dynamicMetadataResolver.setBackgroundInitializationFromCacheDelay(toMillis(attributes.getBackgroundInitializationFromCacheDelay())); + dynamicMetadataResolver.setBackgroundInitializationFromCacheDelay(toMillis(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(attributes.getBackgroundInitializationFromCacheDelay()))); } if (attributes.getRefreshDelayFactor() != null) { @@ -87,13 +95,17 @@ public static void updateOpenSamlMetadataResolverFromReloadableMetadataResolverA if (attributes != null) { if (attributes.getExpirationWarningThreshold() != null) { - reloadingMetadataResolver.setExpirationWarningThreshold(toMillis(attributes.getExpirationWarningThreshold())); + reloadingMetadataResolver + .setExpirationWarningThreshold(toMillis(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(attributes.getExpirationWarningThreshold()))); } if (attributes.getMaxRefreshDelay() != null) { - reloadingMetadataResolver.setMaxRefreshDelay(toMillis(attributes.getMaxRefreshDelay())); + reloadingMetadataResolver.setMaxRefreshDelay(toMillis(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(attributes.getMaxRefreshDelay()))); } if (attributes.getMinRefreshDelay() != null) { - reloadingMetadataResolver.setMinRefreshDelay(toMillis(attributes.getMinRefreshDelay())); + reloadingMetadataResolver.setMinRefreshDelay(toMillis(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(attributes.getMinRefreshDelay()))); } if (attributes.getResolveViaPredicatesOnly() != null) { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlResourceBackedMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlResourceBackedMetadataResolver.java index 67cde7971..0a4b2a7f2 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlResourceBackedMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlResourceBackedMetadataResolver.java @@ -5,7 +5,11 @@ import net.shibboleth.utilities.java.support.xml.ParserPool; import org.apache.lucene.index.IndexWriter; import org.joda.time.DateTime; +import org.opensaml.saml.metadata.resolver.filter.FilterException; +import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain; import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.IOException; @@ -13,7 +17,10 @@ /** * @author Bill Smith (wsmith@unicon.net) */ -public class OpenSamlResourceBackedMetadataResolver extends ResourceBackedMetadataResolver { +public class OpenSamlResourceBackedMetadataResolver extends ResourceBackedMetadataResolver implements Refilterable { + + private static final Logger logger = LoggerFactory.getLogger(OpenSamlResourceBackedMetadataResolver.class); + private IndexWriter indexWriter; private edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver sourceResolver; private OpenSamlMetadataResolverDelegate delegate; @@ -31,6 +38,9 @@ public OpenSamlResourceBackedMetadataResolver(ParserPool parserPool, OpenSamlMetadataResolverConstructorHelper.updateOpenSamlMetadataResolverFromReloadableMetadataResolverAttributes( this, sourceResolver.getReloadableMetadataResolverAttributes(), parserPool); + + //TODO: check if this is the right thing to do + this.setMetadataFilter(new MetadataFilterChain()); } // TODO: this is still probably not the best way to do this? @@ -48,4 +58,15 @@ protected void initMetadataResolver() throws ComponentInitializationException { this.sourceResolver.getResourceId(), indexWriter); } + + /** + * {@inheritDoc} + */ + public void refilter() { + try { + this.getBackingStore().setCachedFilteredMetadata(filterMetadata(getCachedOriginalMetadata())); + } catch (FilterException e) { + logger.error("An error occurred while attempting to filter metadata!", e); + } + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/Refilterable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/Refilterable.java new file mode 100644 index 000000000..339a3b8e3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/Refilterable.java @@ -0,0 +1,16 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml; + +/** + * Indicates that the resolver implementing this interface is a resolver that allows for its metadata to be + * filtered multiple times. + * + * @author Bill Smith (wsmith@unicon.net) + */ +public interface Refilterable { + + /** + * Reapply this resolver's filters to its cached, unfiltered metadata, and set the result back to its cached, + * filtered metadata. + */ + void refilter(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java index 4ae0d07d2..1ddcbb9a4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java @@ -28,10 +28,11 @@ import java.io.IOException; import java.net.URL; +import static edu.internet2.tier.shibboleth.admin.util.TokenPlaceholderResolvers.placeholderResolverService; + /** * @author Bill Smith (wsmith@unicon.net) */ -@Service public class MetadataResolverConverterServiceImpl implements MetadataResolverConverterService { @Autowired @@ -60,7 +61,8 @@ private OpenSamlFileBackedHTTPMetadataResolver convertToOpenSamlRepresentation(F private OpenSamlFilesystemMetadataResolver convertToOpenSamlRepresentation(FilesystemMetadataResolver resolver) throws IOException, ResolverException, ComponentInitializationException { IndexWriter indexWriter = indexWriterService.getIndexWriter(resolver.getResourceId()); - URL url = Thread.currentThread().getContextClassLoader().getResource(resolver.getMetadataFile()); + URL url = Thread.currentThread().getContextClassLoader().getResource(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(resolver.getMetadataFile())); File metadataFile = new File(url.getPath()); OpenSamlFilesystemMetadataResolver openSamlResolver = new OpenSamlFilesystemMetadataResolver(openSamlObjects.getParserPool(), @@ -76,7 +78,8 @@ private OpenSamlLocalDynamicMetadataResolver convertToOpenSamlRepresentation(Loc XMLObjectLoadSaveManager manager = null; try { - manager = new FilesystemLoadSaveManager(resolver.getSourceDirectory()); + manager = new FilesystemLoadSaveManager(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(resolver.getSourceDirectory())); } catch (ConstraintViolationException e) { // the base directory string instance was null or empty //TODO: What should we do here? Currently, this causes a test to fail. @@ -96,7 +99,8 @@ private OpenSamlResourceBackedMetadataResolver convertToOpenSamlRepresentation(R //TODO: What sort of resource type should be created here? URL? break; case CLASSPATH: - resource = ResourceHelper.of(new ClassPathResource(resolver.getClasspathMetadataResource().getFile())); + resource = ResourceHelper.of(new ClassPathResource(placeholderResolverService() + .resolveValueFromPossibleTokenPlaceholder(resolver.getClasspathMetadataResource().getFile()))); break; default: throw new RuntimeException("Unsupported resource type!"); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/ShibbolethPlaceholderTokenAwareValueResolvingService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/ShibbolethPlaceholderTokenAwareValueResolvingService.java new file mode 100644 index 000000000..5ae9a60cc --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/ShibbolethPlaceholderTokenAwareValueResolvingService.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.PropertySources; +import org.springframework.core.env.PropertySourcesPropertyResolver; + + +/** + * Implementation of {@link TokenPlaceholderValueResolvingService} based on Spring Framework's property resolver which + * understands Shibboleth Idp custom placeholder prefix of %{ and can resolve property values from these + * placeholders against existing property sources. + * + * @author Dmitriy Kopylenko + */ +public class ShibbolethPlaceholderTokenAwareValueResolvingService implements TokenPlaceholderValueResolvingService { + + private PropertyResolver propertyResolver; + + ShibbolethPlaceholderTokenAwareValueResolvingService(PropertySources propertySources) { + PropertySourcesPropertyResolver propertySourcesPropertyResolver = new PropertySourcesPropertyResolver(propertySources); + propertySourcesPropertyResolver.setPlaceholderPrefix("%{"); + this.propertyResolver = propertySourcesPropertyResolver; + } + + @Override + public String resolveValueFromPossibleTokenPlaceholder(String potentialTokenPlaceholder) { + return potentialTokenPlaceholder != null + ? this.propertyResolver.resolveRequiredPlaceholders(potentialTokenPlaceholder) + : potentialTokenPlaceholder; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/TokenPlaceholderValueResolvingService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/TokenPlaceholderValueResolvingService.java new file mode 100644 index 000000000..19d8217ff --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/TokenPlaceholderValueResolvingService.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.PropertySources; + +/** + * Generic API to resolve values from arbitrary tokenized placeholders such as '%{token.placeholder}' etc. + * + * @author Dmitriy Kopylenko + */ +@FunctionalInterface +public interface TokenPlaceholderValueResolvingService { + + String resolveValueFromPossibleTokenPlaceholder(String potentialTokenPlaceholder); + + + static TokenPlaceholderValueResolvingService shibbolethPlaceholderPrefixAware(PropertySources propertySources) { + return new ShibbolethPlaceholderTokenAwareValueResolvingService(propertySources); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/OpenSamlChainingMetadataResolverUtil.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/OpenSamlChainingMetadataResolverUtil.java new file mode 100644 index 000000000..81b8f3675 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/OpenSamlChainingMetadataResolverUtil.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver; +import org.opensaml.saml.metadata.resolver.MetadataResolver; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +public class OpenSamlChainingMetadataResolverUtil { + + public static void updateChainingMetadataResolver(OpenSamlChainingMetadataResolver chainingMetadataResolver, MetadataResolver openSamlResolver) { + List resolverList = new ArrayList<>(chainingMetadataResolver.getResolvers()); + resolverList.removeIf(resolver -> resolver.getId().equals(openSamlResolver.getId())); + resolverList.add(openSamlResolver); + chainingMetadataResolver.setResolvers(resolverList); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/TokenPlaceholderResolvers.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/TokenPlaceholderResolvers.java new file mode 100644 index 000000000..9ac8947b7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/TokenPlaceholderResolvers.java @@ -0,0 +1,21 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import edu.internet2.tier.shibboleth.admin.ui.service.TokenPlaceholderValueResolvingService; + +/** + * Accessor facade class to expose {@link TokenPlaceholderValueResolvingService} to non-Spring-managed classes. + * + * @author Dmitriy Kopylenko + */ +public class TokenPlaceholderResolvers { + + private static TokenPlaceholderValueResolvingService placeholderResolverService; + + public TokenPlaceholderResolvers(TokenPlaceholderValueResolvingService service) { + TokenPlaceholderResolvers.placeholderResolverService = service; + } + + public static TokenPlaceholderValueResolvingService placeholderResolverService() { + return placeholderResolverService; + } +} diff --git a/backend/src/main/resources/META-INF/spring.factories b/backend/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..fc0a891d0 --- /dev/null +++ b/backend/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ + edu.internet2.tier.shibboleth.admin.ui.configuration.postprocessors.IdpHomeValueSettingEnvironmentPostProcessor \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy index 06f9d0ca0..7f7beefe7 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy @@ -3,12 +3,14 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import edu.internet2.tier.shibboleth.admin.util.OpenSamlChainingMetadataResolverUtil import groovy.json.JsonOutput import groovy.json.JsonSlurper -import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -38,6 +40,12 @@ class MetadataFiltersControllerIntegrationTests extends Specification { @Autowired AttributeUtility attributeUtility + @Autowired + MetadataResolverConverterService metadataResolverConverterService + + @Autowired + MetadataResolver chainingMetadataResolver + ObjectMapper mapper TestObjectGenerator generator @@ -63,7 +71,8 @@ class MetadataFiltersControllerIntegrationTests extends Specification { def filterResourceId = resolver.metadataFilters[0].resourceId def resolverResourceId = resolver.resourceId metadataResolverRepository.save(resolver) - + MetadataResolver openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(resolver) + OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation) when: 'GET request is made with resource Id matching the existing filter' def result = this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId/Filters/$filterResourceId", String) @@ -86,7 +95,8 @@ class MetadataFiltersControllerIntegrationTests extends Specification { def filterResourceId = resolver.metadataFilters[0].resourceId def resolverResourceId = resolver.resourceId metadataResolverRepository.save(resolver) - + MetadataResolver openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(resolver) + OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation) when: 'GET request is made with resource Id matching the existing filter' def result = this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId/Filters/$filterResourceId", String) @@ -182,7 +192,7 @@ class MetadataFiltersControllerIntegrationTests extends Specification { static class Config { @Bean MetadataResolver metadataResolver() { - new ChainingMetadataResolver().with { + new OpenSamlChainingMetadataResolver().with { it.id = 'tester' it.initialize() return it diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy index 221c5010e..c4453305f 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy @@ -7,12 +7,12 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFil import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import groovy.json.JsonOutput import groovy.json.JsonSlurper -import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -150,13 +150,28 @@ class MetadataResolversControllerIntegrationTests extends Specification { result.statusCodeValue == 404 } + @DirtiesContext + def "SHIBUI-839 - POST resolver with spaces in the provider name results in trimmed name"() { + given: + def resolver = generator.buildRandomMetadataResolverOfType('DynamicHttp') + resolver.name = ' This name has spaces ' + def expectedName = 'This name has spaces' + + when: + def result = this.restTemplate.postForEntity(BASE_URI, createRequestHttpEntityFor { mapper.writeValueAsString(resolver) }, String) + + then: + def metadataResolverMap = new JsonSlurper().parseText(result.body) + metadataResolverMap.name == expectedName + } + @Unroll @DirtiesContext def "POST new concrete MetadataResolver of type #resolverType -> /api/MetadataResolvers"(String resolverType) { given: 'New MetadataResolver JSON representation' def resolver = generator.buildRandomMetadataResolverOfType(resolverType) String sourceDirectory - if (resolverType.equals('Localdynamic')) { + if (resolverType.equals('LocalDynamic')) { sourceDirectory = ((LocalDynamicMetadataResolver) resolver).sourceDirectory } @@ -315,7 +330,7 @@ class MetadataResolversControllerIntegrationTests extends Specification { static class Config { @Bean MetadataResolver metadataResolver() { - new ChainingMetadataResolver() + new OpenSamlChainingMetadataResolver() } } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy index 79a539086..5fe53b85b 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes @@ -18,6 +19,7 @@ import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.data.jpa.repository.config.EnableJpaRepositories @@ -31,7 +33,7 @@ import java.nio.file.Files * @author Bill Smith (wsmith@unicon.net) */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, MyConfig]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, MyConfig, PlaceholderResolverComponentsConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) @@ -58,14 +60,13 @@ class EntityDescriptorTest extends Specification { def "entity descriptors properly marshall to xml"() { given: - def tempDir = Files.createTempDirectory('test') ((OpenSamlChainingMetadataResolver)metadataResolver).resolvers = [ new OpenSamlFileBackedHTTPMetadataResolver( openSamlObjects.parserPool, indexWriterService.getIndexWriter('testme'), new FileBackedHttpMetadataResolver( metadataURL: 'https://idp.unicon.net/idp/shibboleth', - backingFile: "${tempDir.toString()}/test.xml", + backingFile: "%{idp.home}/metadata/test.xml", reloadableMetadataResolverAttributes: new ReloadableMetadataResolverAttributes(), httpMetadataResolverAttributes: new HttpMetadataResolverAttributes() ) 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 381ab3a18..316bfebbd 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 @@ -2,15 +2,16 @@ 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.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConverterConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration 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.RequiredValidUntilFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.SvnMetadataResource +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.repository.MetadataResolverRepository - import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import groovy.xml.DOMBuilder @@ -39,10 +40,9 @@ import spock.lang.Specification import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedXmlIsTheSameAsExpectedXml - @SpringBootTest @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, MetadataResolverConverterConfiguration, SearchConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) @@ -65,6 +65,9 @@ class JPAMetadataResolverServiceImplTests extends Specification { @Autowired AttributeUtility attributeUtility + @Autowired + MetadataResolverConverterService mdrConverterService + TestObjectGenerator testObjectGenerator DOMBuilder domBuilder @@ -111,8 +114,13 @@ class JPAMetadataResolverServiceImplTests extends Specification { ''' when: - def mdr = new edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver().with { + def mdr = new edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver().with { + it.resourceId = "testme" it.name = "testme" + it.classpathMetadataResource = new ClasspathMetadataResource().with { + it.file = "metadata/aggregate.xml" + it + } it.metadataFilters.add(new EntityAttributesFilter().with { it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY @@ -125,12 +133,14 @@ class JPAMetadataResolverServiceImplTests extends Specification { return it } metadataResolverRepository.save(mdr) + ((OpenSamlChainingMetadataResolver) metadataResolver).getResolvers().add(mdrConverterService.convertToOpenSamlRepresentation(mdr)) metadataResolverService.reloadFilters("testme") then: assert metadataResolverRepository.findAll().size() > 0 def ed = metadataResolver.resolveSingle(new CriteriaSet(new EntityIdCriterion('http://test.scaldingspoon.org/test1'))) def resultString = openSamlObjects.marshalToXmlString(ed) + println(resultString) def diff = DiffBuilder.compare(Input.fromString(expectedXML)).withTest(Input.fromString(resultString)).ignoreComments().ignoreWhitespace().build() !diff.hasDifferences() } @@ -302,9 +312,10 @@ class JPAMetadataResolverServiceImplTests extends Specification { it } - return new ChainingMetadataResolver().with { + return new OpenSamlChainingMetadataResolver().with { it.id = 'chain' - it.resolvers = [aggregate] + //it.resolvers = [aggregate] +// it.resolvers = [] it.initialize() it } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/TokenPlaceholderValueResolvingServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/TokenPlaceholderValueResolvingServiceTests.groovy new file mode 100644 index 000000000..8e3ba09f7 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/TokenPlaceholderValueResolvingServiceTests.groovy @@ -0,0 +1,81 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.core.env.ConfigurableEnvironment +import org.springframework.test.context.ContextConfiguration + +import spock.lang.Specification +import spock.lang.Subject + +/** + * @author Dmitriy Kopylenko + */ +@ContextConfiguration(classes = [PlaceholderResolverComponentsConfiguration]) +class TokenPlaceholderValueResolvingServiceTests extends Specification { + + @Autowired + @Subject + TokenPlaceholderValueResolvingService serviceUnderTest + + @Autowired + ConfigurableEnvironment environment + + static final IDP_HOME = '/tmp/test/idp' + static final REFRESH_INTERVAL = 'PT5M' + static final PLAIN_VALUE = 'Plain String value' + + def setup() { + def propPairs = ["idp.home=$IDP_HOME".toString(), "refresh.interval=$REFRESH_INTERVAL".toString()] + TestPropertyValues.of(propPairs).applyTo(environment) + } + + def "resolves correctly existing properties from well-formed shibboleth idp style placeholder tokens: %{}"() { + when: 'Valid placeholder token is passed in for which property values are defined' + def idpHome = serviceUnderTest.resolveValueFromPossibleTokenPlaceholder('%{idp.home}') + def refreshInterval = serviceUnderTest.resolveValueFromPossibleTokenPlaceholder('%{refresh.interval}') + + then: 'Correct property value resolution is performed' + idpHome == IDP_HOME + refreshInterval == REFRESH_INTERVAL + } + + def "returns value as is if no well-formed shibboleth idp style placeholder tokens: %{} are passed in"() { + when: 'Plain value without placeholder token is passed in' + def idpHome = serviceUnderTest.resolveValueFromPossibleTokenPlaceholder(IDP_HOME) + def plainValue = serviceUnderTest.resolveValueFromPossibleTokenPlaceholder(PLAIN_VALUE) + + then: 'Value returned as is' + idpHome == IDP_HOME + plainValue == PLAIN_VALUE + + when: 'Malformed placeholder value is passed in' + plainValue = serviceUnderTest.resolveValueFromPossibleTokenPlaceholder('%{malformed.value') + + then: + plainValue == '%{malformed.value' + } + + def "Throws IllegalArgumentException for unresolvable properties"() { + when: 'Valid placeholder token is passed in for which property values are undefined' + serviceUnderTest.resolveValueFromPossibleTokenPlaceholder("%{i.am.not.defined}") + + then: + thrown IllegalArgumentException + + when: 'Combination of resolvable and unresolvable tokens are passed in' + serviceUnderTest.resolveValueFromPossibleTokenPlaceholder("%{idp.home}/%{i.am.not.defined}") + + then: + thrown IllegalArgumentException + } + + def "resolves correctly combination of existing properties from well-formed shibboleth idp style placeholder tokens: %{}"() { + when: 'Valid placeholder token is passed in for which property values are defined' + def combinedValue = serviceUnderTest.resolveValueFromPossibleTokenPlaceholder('%{idp.home} AND %{refresh.interval}') + + then: 'Correct combined property values resolution is performed' + combinedValue == "$IDP_HOME AND $REFRESH_INTERVAL" + } +} 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 7510f9929..0c7fe9abf 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 @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.util import edu.internet2.tier.shibboleth.admin.ui.domain.* import edu.internet2.tier.shibboleth.admin.ui.domain.filters.* +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget.EntityAttributesFilterTargetType 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.frontend.RelyingPartyOverridesRepresentation @@ -321,11 +322,23 @@ class TestObjectGenerator { return representation } + String buildEntityAttributesFilterTargetValueByType(EntityAttributesFilterTargetType type) { + switch (type) { + case EntityAttributesFilterTargetType.ENTITY: + return generator.randomString() + case EntityAttributesFilterTargetType.CONDITION_SCRIPT: + return "eval(true);" + case EntityAttributesFilterTargetType.REGEX: + return "/foo.*/" + } + } + EntityAttributesFilterTarget buildEntityAttributesFilterTarget() { EntityAttributesFilterTarget entityAttributesFilterTarget = new EntityAttributesFilterTarget() - entityAttributesFilterTarget.setSingleValue(generator.randomString()) entityAttributesFilterTarget.setEntityAttributesFilterTargetType(randomFilterTargetType()) + entityAttributesFilterTarget.setSingleValue( + buildEntityAttributesFilterTargetValueByType(entityAttributesFilterTarget.getEntityAttributesFilterTargetType())) return entityAttributesFilterTarget } @@ -353,8 +366,10 @@ class TestObjectGenerator { FilterTargetRepresentation buildFilterTargetRepresentation() { FilterTargetRepresentation representation = new FilterTargetRepresentation() - representation.setValue(generator.randomStringList()) representation.setType(randomFilterTargetType().toString()) + representation.setValue([ + buildEntityAttributesFilterTargetValueByType(EntityAttributesFilterTargetType.valueOf(representation.getType())) + ]) return representation } @@ -456,13 +471,10 @@ class TestObjectGenerator { new FileBackedHttpMetadataResolver().with { it.name = 'HTTPMetadata' it.xmlId = 'HTTPMetadata' - it.backingFile = 'unicon.xml' + it.backingFile = '%{idp.home}/metadata/metadata.xml' it.metadataURL = 'https://idp.unicon.net/idp/shibboleth' it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes().with { - it.minRefreshDelay = 'PT5M' - it.maxRefreshDelay = 'PT1H' - it.refreshDelayFactor = 0.75 it } it diff --git a/backend/src/test/resources/conf/278.2.xml b/backend/src/test/resources/conf/278.2.xml index 2b71b5ed1..ad3cb6c44 100644 --- a/backend/src/test/resources/conf/278.2.xml +++ b/backend/src/test/resources/conf/278.2.xml @@ -38,11 +38,9 @@ + backingFile="%{idp.home}/metadata/metadata.xml" + metadataURL="https://idp.unicon.net/idp/shibboleth"> + diff --git a/backend/src/test/resources/conf/278.xml b/backend/src/test/resources/conf/278.xml index b244bf6b6..cb5317531 100644 --- a/backend/src/test/resources/conf/278.xml +++ b/backend/src/test/resources/conf/278.xml @@ -31,11 +31,8 @@ + backingFile="%{idp.home}/metadata/metadata.xml" + metadataURL="https://idp.unicon.net/idp/shibboleth"> diff --git a/backend/src/test/resources/conf/532.xml b/backend/src/test/resources/conf/532.xml index bf4ce340b..b2be43498 100644 --- a/backend/src/test/resources/conf/532.xml +++ b/backend/src/test/resources/conf/532.xml @@ -7,9 +7,7 @@ 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"> + backingFile="%{idp.home}/metadata/metadata.xml" + metadataURL="https://idp.unicon.net/idp/shibboleth" /> +