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/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/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 aa5f45745..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 @@ -4,14 +4,18 @@ import com.google.common.collect.Collections2; import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.component.ComponentSupport; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; import net.shibboleth.utilities.java.support.resolver.ResolverException; 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.saml2.metadata.EntityDescriptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -25,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) { @@ -35,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; } @@ -49,12 +53,32 @@ public List getResolvers() { return mutableResolvers; } + @Override + @Nonnull public Iterable resolve(@Nullable final CriteriaSet criteria) throws ResolverException { + ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this); + //Our overridden method uses a collection of mutable resolvers instead of regular resolvers + for (final MetadataResolver resolver : mutableResolvers) { + try { + final Iterable descriptors = resolver.resolve(criteria); + if (descriptors != null && descriptors.iterator().hasNext()) { + return descriptors; + } + } catch (final ResolverException e) { + log.warn("Error retrieving metadata from resolver of type {}, proceeding to next resolver", + resolver.getClass().getName(), e); + continue; + } + } + + return Collections.emptyList(); + } + @Override 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..d0a9b6ff4 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,7 +8,11 @@ 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; @@ -17,7 +21,10 @@ /** * @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; @@ -42,6 +49,8 @@ public OpenSamlFileBackedHTTPMetadataResolver(ParserPool parserPool, this.setBackupFileInitNextRefreshDelay(toMillis(sourceResolver.getBackupFileInitNextRefreshDelay())); this.setInitializeFromBackupFile(sourceResolver.getInitializeFromBackupFile()); + this.setMetadataFilter(new MetadataFilterChain()); + //TODO: Where does this get set in OpenSAML land? // sourceResolver.getMetadataURL(); } @@ -67,4 +76,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/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/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..e1ed54f82 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 @@ -31,7 +31,6 @@ /** * @author Bill Smith (wsmith@unicon.net) */ -@Service public class MetadataResolverConverterServiceImpl implements MetadataResolverConverterService { @Autowired 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/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 9775160bd..69efa8133 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 @@ -21,6 +21,7 @@ import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.context.annotation.Bean import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders +import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.ActiveProfiles import spock.lang.Specification import spock.lang.Unroll @@ -150,6 +151,7 @@ class MetadataResolversControllerIntegrationTests extends Specification { } @Unroll + @DirtiesContext def "POST new concrete MetadataResolver of type #resolverType -> /api/MetadataResolvers"(String resolverType) { given: 'New MetadataResolver JSON representation' def resolver = generator.buildRandomMetadataResolverOfType(resolverType) @@ -313,7 +315,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/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/util/TestObjectGenerator.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy index 7510f9929..13dc8a554 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 } @@ -460,9 +475,6 @@ class TestObjectGenerator { 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..5a34f756b 100644 --- a/backend/src/test/resources/conf/278.2.xml +++ b/backend/src/test/resources/conf/278.2.xml @@ -39,10 +39,7 @@ + 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..a337f72d7 100644 --- a/backend/src/test/resources/conf/278.xml +++ b/backend/src/test/resources/conf/278.xml @@ -32,10 +32,7 @@ + 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..85fead908 100644 --- a/backend/src/test/resources/conf/532.xml +++ b/backend/src/test/resources/conf/532.xml @@ -8,8 +8,5 @@ + metadataURL="https://idp.unicon.net/idp/shibboleth" /> diff --git a/ui/README.md b/ui/README.md index a24915b12..0e52b0fc9 100644 --- a/ui/README.md +++ b/ui/README.md @@ -30,6 +30,66 @@ For Providers and Filters, the forms in SHIB-UI are built based on the standard [Bootstrap](http://getbootstrap.com/) 4 is used for the css framework in SHIB-UI, and provides our base theme, a responsive grid system, consistent styling across all major browsers, and pre-styled components which are connected to Angular using ng-bootstrap, a 3rd party framework. +# Customizing UI + +Within the `ui > src` folder, there are two files which can be used to customize the user interface quickly. + +### Color Scheme +The `Brand.scss` contains the palette information used by bootstrap to color the user interface buttons and other components. Uncomment the color you wish to override and change the hex value accordingly and rebuild the user interface. + +For example: + +`// $blue: #00355f;` + +change to... + +`$blue: #0000FF;` + +If you wish to change the __"primary"__ color or any other mapping according to Bootstrap's conventions, there is a block of mappings at the bottom of the `Brand.scss` file to accomodate this as well. Uncomment and change the value allocated to the corresponding aspect of the theme you wish to change. + +The supported values are: + +`$brand-primary: $blue;` - The primary color used throughout the site. Default: dark blue. + +`$brand-secondary: $gray-700;` - An accept color used throughout the site. Default: dark gray. + +`$brand-success: $green;` - Indicates success or some other positive action. Default: green. + +`$brand-info: $light-blue;` - Indicates some sort of informative text or action for the user. Default: light blue. + +`$brand-warning: $yellow;` - Indicates a warning to the user. Default: yellow. + +`$brand-danger: $red;` - Indicates a dangerous action or error. Default: red. + +`$brand-light: $light-grey;` - Color used for accents, text, etc throughout application. Default: light grey. + +`$brand-dark: $dark-grey;` - Color used for accents, text, etc throughout application. Default: dark grey. + +### Logos / Footer +The `Brand.ts` contains the links, text, and image paths to modify the logos, text, and links found within the header and footer. Override the values within this Typescript file and rebuild the user interface to view changes. If the local server should host these files, they should be added to the assets folder. + +For example: + +`export const brand = {};` + +change to... + +`export const brand = { + header: 'Metadata Management' +};` + +### Sitewide text +Finally, this project has support for internationalization (i18n). In the folder `backend > src > main > resources > i18n` folder there is a group of files which can be used to modify the text seen throughout the application. In order to change the default value, you can override the text in the messages_en file with the desired text. + +For example: + +`action.logout=Logout` + +change to... + +`action.logout=Sign Out` + + # Development ## Scaffolding diff --git a/ui/karma.conf.js b/ui/karma.conf.js index 1000f8daf..b69d2eda1 100644 --- a/ui/karma.conf.js +++ b/ui/karma.conf.js @@ -8,7 +8,7 @@ module.exports = function (config) { frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), - require('karma-phantomjs-launcher'), + require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), require('karma-spec-reporter'), @@ -46,13 +46,7 @@ module.exports = function (config) { colors: true, logLevel: config.LOG_WARN, autoWatch: false, - browsers: ['PhantomJS_custom'], - customLaunchers: { - 'PhantomJS_custom': { - base: 'PhantomJS', - flags: ['--disk-cache=false'] - } - }, + browsers: ['ChromeHeadless'], singleRun: true }); }; diff --git a/ui/package.json b/ui/package.json index 6dc630ba9..7865053ba 100644 --- a/ui/package.json +++ b/ui/package.json @@ -66,7 +66,6 @@ "karma-phantomjs-launcher": "^1.0.4", "karma-spec-reporter": "0.0.31", "path": "^0.12.7", - "phantomjs-prebuilt": "^2.1.15", "protractor": "~5.1.2", "ts-node": "~3.2.0", "tslint": "~5.3.2", diff --git a/ui/src/app/app.brand.ts b/ui/src/app/app.brand.ts new file mode 100644 index 000000000..a17c34cc0 --- /dev/null +++ b/ui/src/app/app.brand.ts @@ -0,0 +1,44 @@ +import { brand as customBrand } from '../brand'; +import { Brand } from './core/model/brand'; + +export const brand: Brand = { + header: { + title: 'Source Management' + }, + logo: { + default: '/assets/shibboleth_logowordmark_color.png', + small: '/assets/shibboleth_icon_color_130x130.png', + large: '/assets/shibboleth_logowordmark_color.png', + alt: 'Shibboleth Logo - Click to be directed to www.shibboleth.net', + link: { + label: 'Shibboleth', + url: 'https://www.shibboleth.net/' + } + }, + footer: { + links: [ + { + label: 'Home Page', + url: 'https://www.shibboleth.net/', + description: 'Shibboleth.net open-source community home page' + }, + { + label: 'Wiki', + url: 'https://wiki.shibboleth.net/', + description: 'Shibboleth.net open-source community wiki' + }, + { + label: 'Issue Tracker', + url: 'https://issues.shibboleth.net/', + description: 'Shibboleth.net open-source community issue tracker' + }, + { + label: 'Mailing List', + url: 'https://www.shibboleth.net/community/lists/', + description: 'Shibboleth.net open-source community mailing list' + } + ], + text: 'Links to Shibboleth resources:' + }, + ...customBrand +}; diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 17b5742e3..694a691ab 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -1,10 +1,10 @@